我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(下)

简介: 我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(下)

更多精彩内容,欢迎观看:

我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(上)


六、总结根因


通过上述分析推演,iOS 16 键盘 Crash 的根因已查明,即-[UIKeyboardTaskQueue continueExecutionOnMainThread]方法内执行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]尝试加锁失败后,不return继续向下执行读写不安全内存以及解锁,导致存在锁失效的情况,使得UIKeyboardTaskQueue成员变量_deferredTasks数组在多线程下出现并发添加UIKeyboardTaskEntry实例而引起野指针,导致最终 Crash。


注:该根因除了导致数组读写异常而 Crash,也可能导致其他变量的状态不一致性,只是不一定表现为 Crash 而已,建议用本文方案修复。


解决方案(App 内置补丁源码)


明确根因后,解决方案就比较明确了,写一个 App 内置补丁代码使得-[UIKeyboardTaskQueue continueExecutionOnMainThread]方法内执行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]尝试加锁失败后,正常return即可。补丁方案有两个:


  1. 重写-[UIKeyboardTaskQueue continueExecutionOnMainThread]方法。在原汇编基础上新增一条指令,即在bl _objc_msgSend$tryLockWhenReadyForMainThread后添加一条汇编指令cbz w0, return_labelreturn_label对应源码return对应的汇编指令地址),如失败则return。但该方案涉及的原汇编指令较多,有 95 条汇编指令(见下文附件中 iOS 系统汇编),容易踩坑。
  2. 重写-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]方法。在该方法内如加锁失败则模拟两次return,回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]的上一个函数栈,改造的汇编指令较少,安全性较好,也确认了除-[UIKeyboardTaskQueue continueExecutionOnMainThread]调用外,无其他方法调用-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]


最终,支付宝 App 基于稳定性的考虑,采用第 2 种补丁方案修复键盘 Crash。


补丁原理



图 14 修复键盘 Crash 的补丁原理


-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]实现以下逻辑:

  • 如加锁成功,则return 1 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]方法的下一条指令继续执行;
  • 如加锁失败,则模拟return 2 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]的函数栈的上一层函数的地址继续执行,也就是模拟了从-[UIKeyboardTaskQueue continueExecutionOnMainThread]中执行return操作。


源码return语句,对应汇编的 4 步:

  1. 恢复fplr寄存器。fp(也称x29)记录当前帧的内存地址,lr(也称x30)记录从当前函数返回时跳转到哪个地址继续执行。运行时就是通过fplr寄存器,输出线程的函数栈的。如 Crash 函数栈,或从lldbbt输出的函数栈;
  2. 恢复callee-saved寄存器。即x19-x28的寄存器,try-catch的实现就涉及该类寄存器,一般按需执行;
  3. 恢复sp寄存器。sp记录当前帧的栈顶地址,,当前函数的局部变量所在的内存地址就在(fp, sp]之间;
  4. 执行ret指令。执行ret指令后,pc就指向lr寄存器的值,然后继续执行;


本文补丁方案的原理中,tryLock 失败时就是通过:恢复fplr寄存器 + 恢复callee-saved寄存器 + 恢复sp寄存器 + 再次恢复fplr寄存器 + 再次恢复callee-saved寄存器 + 再次恢复sp寄存器 +ret指令 来模拟在-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]方法内return 2 次直接返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]的函数栈的上一层函数的。


补丁实现


有两部分组成:

  1. 重写方法:对应 fix_UIKeyboardTaskQueue.S 文件;
  2. Hook 入口:对应 fix_UIKeyboardTaskQueue.m 文件;


重写方法


重写-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]方法实现,对应下文附件中补丁源码的 fix_UIKeyboardTaskQueue.S 文件。


图 15 重写 -[UIKeyboardTaskQueue tryLockWhenReadyForMainThread] 方法实现

Hook 入口


借助+ (void)load方法在 App 启动时执行的特点实现对-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]方法的 Hook,仅在 iOS 16 的 Arm64 架构上生效,对应下文附件中补丁源码的 fix_UIKeyboardTaskQueue.m 文件。


图 16 Hook 入口的代码


方案效果


于 2023.8.25 在支付宝 App 近期版本 10.5.16.6000 上全量开启解决方案的开关后,该版本上的 Crash 日 PV 已经降到 0 了


图 17 支付宝 App 近期版本 10.5.16.6000 上键盘 Crash 日 PV


同时,支付宝 App 的全量版本(包括所有历史版本)的键盘 Crash 日 PV 下降了近 90%,随着更多用户升级到支付宝 App 最新版本,预计会降到个位数



图 18 方案上线后键盘 Crash 日 PV 明显下降的趋势图


最终该方案由验收人确认有效,键盘 Crash 已解决,揭榜挑战成功,附上一张挑战成功捷报图收个尾。


图 19 蚂蚁内部的技术英雄榜捷报


附件


1、补丁源码


补丁源码包括两部分:fix_UIKeyboardTaskQueue.S 和 fix_UIKeyboardTaskQueue.m。使用时将该两文件直接内置在 App 中即可,也可在 App 启动时加开关控制 Hook 入口的时机。


#ifdef __arm64__////  fix_UIKeyboardTaskQueue.S//  fix_UIKeyboardTaskQueue////  Created by Alipay on 2023/8/10.//  Copyright © 2023 Alipay. All rights reserved.///**原实现-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]:ldr    x0, [x0, #0x10]mov    x2, #0x0b    "_objc_msgSend$tryLockWhenCondition:"*/// 重写实现.section__TEXT,__cstring,cstring_literalstryLockWhenCondition.str:
.asciz"tryLockWhenCondition:".text.align4.global_fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread.cfi_startproc_fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread:
stpx20, x19, [sp, #-0x20]!stpx29, x30, [sp, #0x10]
addx29, sp, #0x10movx19, x0                     ; selfadrpx0, tryLockWhenCondition.str@PAGEaddx0, x0, tryLockWhenCondition.str@PAGEOFFbl_sel_registerName           ; @selector(tryLockWhenCondition:)
movx1, x0ldrx0, [x19, #0x10]            ; _lockmovx2, #0x0bl_objc_msgSend               ; -[_locktryLockWhenCondition:0]
ldpx29, x30, [sp, #0x10]       ; 恢复fp和lrldpx20, x19, [sp], #0x20       ; 恢复callee-saved寄存器、并恢复spcbzx0, 1f// 如tryLock成功,则继续执行-[UIKeyboardTaskQueue continueExecutionOnMainThread]的指令ret// 如tryLock失败,则模拟从-[UIKeyboardTaskQueue continueExecutionOnMainThread] return,不再继续执行1:
ldpx29, x30, [sp, #0x20]       ; 恢复fp和lrldpx20, x19, [sp, #0x10]       ; 恢复callee-saved寄存器addsp, sp, #0x30               ; 恢复spautibsp                            ; AuthenticateInstructionaddressret.cfi_endproc#endif


////  fix_UIKeyboardTaskQueue.m//  fix_UIKeyboardTaskQueue////  Created by Alipay on 2023/9/4.//#ifdef __arm64__#import <UIKit/UIKit.h>#include <objc/runtime.h>@interfacefix_UIKeyboardTaskQueue : NSObject@end@implementationfix_UIKeyboardTaskQueue+ (void)load {
externBOOLfix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread(idself, SELselector);
if (@available(iOS16.0, *)) {
NSString*systemVersion= [[UIDevicecurrentDevice] systemVersion];
NSArray*verInfos= [systemVersioncomponentsSeparatedByString:@"."];
NSUIntegercount= [verInfoscount];
if (count>=2) {
if ([verInfos[0] isEqualToString:@"16"]) {
class_replaceMethod(objc_getClass("UIKeyboardTaskQueue"), sel_getUid("tryLockWhenReadyForMainThread"), (IMP)fix_UIKeyboardTaskQueue_tryLockWhenReadyForMainThread, "B16@0:8");
            }
        }
    }
}
@end#endif


2、Demo 关键源码


////  ViewController.m//  UIKeyboardTaskQueueDemo////  Created by Alipay on 2023/8/30.//#import "ViewController.h"#include <objc/runtime.h>// #import <DebugKit/DebugKit.h>@interfaceViewController ()
@end@implementationViewController {
NSMutableArray*_tasks;
NSMutableArray*_deferredTasks;
NSConditionLock*_lock;
}
- (void)viewDidLoad {
    [superviewDidLoad];
// 输出UIKeyboardTaskQueue的所有实例方法和类方法// dk_print_all_methods_of_class("UIKeyboardTaskQueue");// 输出UIKeyboardTaskQueue的所有property// dk_print_all_properties("UIKeyboardTaskQueue");// 输出UIKeyboardTaskQueue的所有ivars// dk_print_class_all_ivars("UIKeyboardTaskQueue");UITextView*textView= [[UITextViewalloc] initWithFrame:self.view.bounds];
    [self.viewaddSubview:textView];
    [selftest_crash];
    [selftest_ok];
}
- (void)unlock {
    [_lockunlockWithCondition:0];
}
- (void)lock {
    [_locklock];
}
- (BOOL)tryLock {
return [_locktryLockWhenCondition:0];
}
- (void)addEntry_ok {
    [selflock];
    [_deferredTasksaddObject:[[NSObjectalloc] init]];
NSLog(@"add,    %lu", _deferredTasks.count);
    [selfunlock];
}
- (void)removeEntry_crash {
    [selftryLock];
if (_deferredTasks.count) {
        [_tasksaddObject:[_deferredTasksobjectAtIndex:0]];
if (_deferredTasks.count) {
            [_deferredTasksremoveObjectAtIndex:0];
NSLog(@"remove, %lu", _deferredTasks.count);
//            NSLog(@"%@, %lu -[_deferredTasks removeObjectAtIndex:0]", [NSThread currentThread], _deferredTasks.count);        }
    }
    [selfunlock];
}
- (void)removeEntry_ok {
if (![selftryLock]) return;
if (_deferredTasks.count) {
        [_tasksaddObject:[_deferredTasksobjectAtIndex:0]];
if (_deferredTasks.count) {
            [_deferredTasksremoveObjectAtIndex:0];
NSLog(@"remove, %lu", _deferredTasks.count);
//            NSLog(@"%@, %lu -[_deferredTasks removeObjectAtIndex:0]", [NSThread currentThread], _deferredTasks.count);        }
    }
    [selfunlock];
}
- (void)test_crash {
// init_tasks= [NSMutableArrayarray];
_deferredTasks= [NSMutableArrayarray];
_lock= [[NSConditionLockalloc] initWithCondition:0];
for (inti=0; i<10000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [selfaddEntry_ok];
        });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//            dispatch_async(dispatch_get_main_queue(), ^{                [selfremoveEntry_crash];
//            });        });
    }
}
- (void)test_ok {
// init_tasks= [NSMutableArrayarray];
_deferredTasks= [NSMutableArrayarray];
_lock= [[NSConditionLockalloc] initWithCondition:0];
for (inti=0; i<1000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [selfaddEntry_ok];
        });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [selfremoveEntry_ok];
        });
    }
}
@end


3、脚本源码


#!/bin/sh# FileTEXT_FILE="$1";
# ClassCLASS_NAME="$2"; 
cat"$TEXT_FILE"|tr'\n''&'|sed's/&\-\[/\n\-\[/g'|grep"^\-\[$CLASS_NAME "|tr'&''\n';


4、iOS 系统汇编(关键方法)


将 iOS 16.6 的 iPhone 12 Pro Max(Hardware Mode: iPhone13 4)设备连接到 Xcode 后,按如下操作可获取到 UIKeyboardTaskQueue 类的实现汇编,即UIKitCore_20G75_arm64e_TEXT.txt 文件。


otool-s__TEXT__text-v~/Library/Developer/Xcode/iOS\DeviceSupport/16.6\\(20G75\)\arm64e/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore>~/Desktop/UIKitCore_20G75_arm64e_TEXT.txt./fetch_class_text_from_all.sh~/Desktop/UIKitCore_20G75_arm64e_TEXT.txtUIKeyboardTaskQueue>~/Desktop/UIKeyboardTaskQueue_20G75_arm64e_TEXT.txt


-[UIKeyboardTaskQueuecontinueExecutionOnMainThread]:
0000000189466fd0pacibsp0000000189466fd4subsp, sp, #0x300000000189466fd8stpx20, x19, [sp, #0x10]
0000000189466fdcstpx29, x30, [sp, #0x20]
0000000189466fe0addx29, sp, #0x200000000189466fe4movx19, x00000000189466fe8bl0x18c9df5e00000000189466feccmpw0, #0x10000000189466ff0b.ne0x1894670240000000189466ff4movx0, x190000000189466ff8bl_objc_msgSend$tryLockWhenReadyForMainThread0000000189466ffcldrx8, [x19, #0x28]
0000000189467000cbzx8, 0x1894670580000000189467004ldrx8, [x19, #0x30]
0000000189467008cbzx8, 0x1894670b4000000018946700cbl0x18c9df2f00000000189467010strx0, [sp, #0x8]
0000000189467014ldrx8, [x19, #0x30]
0000000189467018strxzr, [x19, #0x30]
000000018946701cbl0x18c9df1500000000189467020b0x1894670ac0000000189467024adrpx8, -26465 ; 0x182d060000000000189467028addx2, x8, #0xe19000000018946702cmovx0, x190000000189467030movx3, #0x00000000189467034movw4, #0x00000000189467038ldpx29, x30, [sp, #0x20]
000000018946703cldpx20, x19, [sp, #0x10]
0000000189467040addsp, sp, #0x300000000189467044autibsp0000000189467048eorx16, x30, x30, lsl#1000000018946704ctbzx16, #0x3e, 0x1894670540000000189467050brk#0xc4710000000189467054b"_objc_msgSend$performSelectorOnMainThread:withObject:waitUntilDone:"0000000189467058ldrx0, [x19, #0x18]
000000018946705cbl_objc_msgSend$count0000000189467060cbzx0, 0x1894670b80000000189467064adrpx8, 333019 ; 0x1da9420000000000189467068ldrx0, [x8, #0x500]
000000018946706cbl0x18c9dee300000000189467070movx2, x190000000189467074bl"_objc_msgSend$initWithExecutionQueue:"0000000189467078movx20, x0000000018946707cmovx0, x190000000189467080movx2, x200000000189467084bl"_objc_msgSend$setExecutionContext:"0000000189467088bl0x18c9df0a0000000018946708cldrx0, [x19, #0x18]
0000000189467090movx2, #0x00000000189467094bl"_objc_msgSend$objectAtIndex:"0000000189467098bl0x18c9deec0000000018946709cstrx0, [sp, #0x8]
00000001894670a0ldrx0, [x19, #0x18]
00000001894670a4movx2, #0x000000001894670a8bl"_objc_msgSend$removeObjectAtIndex:"00000001894670acldrx0, [sp, #0x8]
00000001894670b0b0x1894670b800000001894670b4movx0, #0x000000001894670b8strx0, [sp, #0x8]
00000001894670bcbl_objc_msgSend$originatingStack00000001894670c0bl0x18c9deec000000001894670c4movx20, x000000001894670c8movx0, x1900000001894670ccmovx2, x2000000001894670d0bl"_objc_msgSend$setActiveOriginator:"00000001894670d4bl0x18c9df0a000000001894670d8movx0, x1900000001894670dcbl_objc_msgSend$unlock00000001894670e0ldrx1, [sp, #0x8]
00000001894670e4ldrbw20, [x19, #0x8]
00000001894670e8movw8, #0x100000001894670ecstrbw8, [x19, #0x8]
00000001894670f0ldrx2, [x19, #0x28]
00000001894670f4cbzx1, 0x18946710800000001894670f8movx0, x100000001894670fcbl"_objc_msgSend$execute:"0000000189467100ldrx1, [sp, #0x8]
0000000189467104b0x18946710c0000000189467108cbzx2, 0x189467130000000018946710cstrbw20, [x19, #0x8]
0000000189467110ldpx29, x30, [sp, #0x20]
0000000189467114ldpx20, x19, [sp, #0x10]
0000000189467118addsp, sp, #0x30000000018946711cautibsp0000000189467120eorx16, x30, x30, lsl#10000000189467124tbzx16, #0x3e, 0x18946712c0000000189467128brk#0xc471000000018946712cb0x18c9df0600000000189467130ldrx0, [x19, #0x20]
0000000189467134bl_objc_msgSend$count0000000189467138ldrx1, [sp, #0x8]
000000018946713ccbzx0, 0x18946710c0000000189467140movx0, x190000000189467144bl_objc_msgSend$performDeferredTaskIfIdle0000000189467148b0x189467100-[UIKeyboardTaskQueuetryLockWhenReadyForMainThread]:
0000000189467738ldrx0, [x0, #0x10]
000000018946773cmovx2, #0x00000000189467740b"_objc_msgSend$tryLockWhenCondition:"-[UIKeyboardTaskQueueperformDeferredTaskIfIdle]:
0000000189c814b4pacibsp0000000189c814b8stpx20, x19, [sp, #-0x20]!0000000189c814bcstpx29, x30, [sp, #0x10]
0000000189c814c0addx29, sp, #0x100000000189c814c4movx19, x00000000189c814c8bl_objc_msgSend$lock0000000189c814ccmovx0, x190000000189c814d0bl_objc_msgSend$promoteDeferredTaskIfIdle0000000189c814d4movx0, x190000000189c814d8bl_objc_msgSend$unlock0000000189c814dcmovx0, x190000000189c814e0ldpx29, x30, [sp, #0x10]
0000000189c814e4ldpx20, x19, [sp], #0x200000000189c814e8autibsp0000000189c814eceorx16, x30, x30, lsl#10000000189c814f0tbzx16, #0x3e, 0x189c814f80000000189c814f4brk#0xc4710000000189c814f8b_objc_msgSend$continueExecutionOnMainThread-[UIKeyboardTaskQueuepromoteDeferredTaskIfIdle]:
0000000189c814fcpacibsp0000000189c81500subsp, sp, #0x300000000189c81504stpx20, x19, [sp, #0x10]
0000000189c81508stpx29, x30, [sp, #0x20]
0000000189c8150caddx29, sp, #0x200000000189c81510ldrx8, [x0, #0x28]
0000000189c81514cbzx8, 0x189c815280000000189c81518ldpx29, x30, [sp, #0x20]
0000000189c8151cldpx20, x19, [sp, #0x10]
0000000189c81520addsp, sp, #0x300000000189c81524retab0000000189c81528movx19, x00000000189c8152cldrx0, [x0, #0x20]
0000000189c81530bl_objc_msgSend$count0000000189c81534cbzx0, 0x189c815180000000189c81538ldrx0, [x19, #0x20]
0000000189c8153cmovx2, #0x00000000189c81540bl"_objc_msgSend$objectAtIndex:"0000000189c81544bl0x18c9deec00000000189c81548movx2, x00000000189c8154cstrx0, [sp, #0x8]
0000000189c81550ldrx0, [x19, #0x18]
0000000189c81554bl"_objc_msgSend$addObject:"0000000189c81558ldrx0, [x19, #0x20]
0000000189c8155cmovx2, #0x00000000189c81560bl"_objc_msgSend$removeObjectAtIndex:"0000000189c81564ldrx0, [sp, #0x8]
0000000189c81568ldpx29, x30, [sp, #0x20]
0000000189c8156cldpx20, x19, [sp, #0x10]
0000000189c81570addsp, sp, #0x300000000189c81574autibsp0000000189c81578eorx16, x30, x30, lsl#10000000189c8157ctbzx16, #0x3e, 0x189c815840000000189c81580brk#0xc4710000000189c81584b0x18c9df050-[UIKeyboardTaskQueueaddDeferredTask:]:
0000000189c815fcpacibsp0000000189c81600subsp, sp, #0x300000000189c81604stpx20, x19, [sp, #0x10]
0000000189c81608stpx29, x30, [sp, #0x20]
0000000189c8160caddx29, sp, #0x200000000189c81610movx19, x00000000189c81614bl0x18c9df2000000000189c81618movx20, x00000000189c8161cmovx0, x190000000189c81620bl_objc_msgSend$lock0000000189c81624adrpx8, 330945 ; 0x1da9420000000000189c81628ldrx0, [x8, #0x510]
0000000189c8162cbl0x18c9dee300000000189c81630movx2, x200000000189c81634bl"_objc_msgSend$initWithTask:"0000000189c81638strx0, [sp, #0x8]
0000000189c8163cbl0x18c9df0a00000000189c81640ldrx0, [x19, #0x20]
0000000189c81644ldrx2, [sp, #0x8]
0000000189c81648bl"_objc_msgSend$addObject:"0000000189c8164cmovx0, x190000000189c81650bl_objc_msgSend$unlock0000000189c81654movx0, x190000000189c81658bl_objc_msgSend$continueExecutionOnMainThread0000000189c8165cldrx0, [sp, #0x8]
0000000189c81660ldpx29, x30, [sp, #0x20]
0000000189c81664ldpx20, x19, [sp, #0x10]
0000000189c81668addsp, sp, #0x300000000189c8166cautibsp0000000189c81670eorx16, x30, x30, lsl#10000000189c81674tbzx16, #0x3e, 0x189c8167c0000000189c81678brk#0xc4710000000189c8167cb0x18c9df050


🔗 相关链接

[1] Arm64 寄存器说明:https://developer.arm.com/documentation/den0024/a/The-ABI-for-ARM-64-bit-Architecture/Register-use-in-the-AArch64-Procedure-Call-Standard/Parameters-in-general-purpose-registers

[2] Arm64 汇编指令集说明:https://documentation-service.arm.com/static/6023d5512cb3723f20208db2

[3] NSConditionLock 条件状态锁:https://developer.apple.com/documentation/foundation/nsconditionlock/



相关文章
|
4天前
|
搜索推荐 Android开发 iOS开发
探索安卓与iOS系统的用户界面设计哲学
现代移动操作系统的设计哲学不仅仅是技术的表现,更是用户体验与功能实现的结合。本文将深入分析安卓与iOS两大主流系统在用户界面设计方面的差异与共通之处,探讨它们背后的思维模式及其对用户体验的影响。 【7月更文挑战第11天】
|
1月前
|
存储 安全 编译器
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash
|
21天前
|
安全 搜索推荐 Android开发
探索安卓和iOS系统的优劣与特点
在移动操作系统领域,安卓和iOS一直是最热门的两个选择。本文将探讨安卓和iOS系统的优劣与特点,帮助读者更好地了解这两个操作系统,并为选择合适的移动设备提供参考。
22 0
|
2月前
iOS16系统根据PHAsset判断是否在云上
iOS16系统根据PHAsset判断是否在云上
32 1
|
2月前
|
安全 开发者 iOS开发
iOS16系统手机设置开启开发者模式才能安装ipa包
iOS16系统手机设置开启开发者模式才能安装ipa包
56 1
|
2月前
如何解决iOS16系统app首次启动总是弹出允许粘贴提示框问题
如何解决iOS16系统app首次启动总是弹出允许粘贴提示框问题
37 0
如何解决iOS16系统app首次启动总是弹出允许粘贴提示框问题
|
2月前
|
人工智能 vr&ar Android开发
安卓与iOS系统的发展趋势及影响分析
在移动互联网时代,安卓和iOS作为两大主流移动操作系统,在不断发展变化中展现出不同的特点和发展趋势。本文从技术性角度出发,分析了安卓和iOS系统的发展趋势,并探讨了它们对移动设备市场和用户体验的影响,帮助读者更好地理解当前移动操作系统的发展方向和未来可能的变化。
24 0
|
4天前
|
开发工具 Android开发 iOS开发
探索Android与iOS开发的差异与挑战
【7月更文挑战第11天】在移动应用开发的广阔天地中,Android和iOS两大平台如同双子星座般耀眼,各自拥有独特的开发生态和用户群体。本文将深入分析这两个平台的显著差异,从技术架构到开发工具,再到市场定位,揭示它们之间的异同。通过比较,我们不仅能够更好地理解各自的优势和局限,还能洞察未来移动应用开发的趋势。
|
7天前
|
Java Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【7月更文挑战第8天】在移动应用开发的广阔天地中,Android与iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发环境、用户界面设计、性能优化以及市场覆盖等方面的根本差异,并分析这些差异如何影响项目的成功。通过比较和分析,旨在为开发者在选择平台时提供更全面的视角,帮助他们根据项目需求和目标市场做出更明智的决策。