我给 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/



相关文章
|
27天前
|
安全 5G 语音技术
Cisco ASR 9000 Router IOS XR Release 7.11.2 MD - ASR 9000 系列聚合服务路由器系统软件
Cisco ASR 9000 Router IOS XR Release 7.11.2 MD - ASR 9000 系列聚合服务路由器系统软件
180 4
Cisco ASR 9000 Router IOS XR Release 7.11.2 MD - ASR 9000 系列聚合服务路由器系统软件
|
6月前
|
iOS开发
Cisco Catalyst 9800 Wireless Controller, IOS XE Release 17.17.1 ED - 思科无线控制器系统软件
Cisco Catalyst 9800 Wireless Controller, IOS XE Release 17.17.1 ED - 思科无线控制器系统软件
199 9
Cisco Catalyst 9800 Wireless Controller, IOS XE Release 17.17.1 ED - 思科无线控制器系统软件
|
云安全 安全 Cloud Native
Cisco Catalyst 8000 Series IOS XE 17.18.1a ED 发布 - 思科边缘平台系列系统软件
Cisco Catalyst 8000 Series IOS XE 17.18.1a ED - 思科边缘平台系列系统软件
75 0
|
运维 监控 安全
Cisco ISR 4000 Series IOS XE 17.18.1a ED 发布 - 思科 4000 系列集成服务路由器 IOS XE 系统软件
Cisco ISR 4000 Series IOS XE 17.18.1a ED - 思科 4000 系列集成服务路由器 IOS XE 系统软件
83 0
|
人工智能 监控 安全
思科 Catalyst 9000 交换产品系列 IOS XE 系统软件 17.18.1 ED
Cisco Catalyst 9000 Series Switches, IOS XE Release 17.18.1 ED
104 0
iOS你不知道的事--Crash分析
原文作者:Cooci_和谐学习_不急不躁原文地址:https://www.jianshu.com/p/56f96167a6e9 大家平时在开发过程中,经常会遇到Crash,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。
1356 0
|
测试技术 Android开发 开发者
遇到iOS Crash,该如何分析?
众所周知,Android App在机型兼容覆盖上面临非常大的挑战,一个根本原因就是由于Android生态的开放性,市场上充斥着数量巨大的各类Android设备,无法在浩如烟海的机型上做适配。 反观iOS生态,尽管整体上iOS App质量明显优于Android应用,但是从阿里云测移动质量中心的统计数据来看,依然有30%左右的iOS App存在崩溃、运行卡顿等严峻问题。
8794 0
|
iOS开发 开发者 C#
|
12月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。

热门文章

最新文章