背景
iOS 17 发布后,我司的 App 新增了 n 个崩溃,其中的一个崩溃堆栈如下:
0 libobjc.A.dylib _objc_retain()
1 ContextKitExtraction +[CKContextContentProviderUIScene _bestVisibleStringForView:usingExecutor:]()
2 ContextKitExtraction +[CKContextContentProviderUIScene _donateContentsOfWindow:usingExecutor:withOptions:]()
3 ContextKitExtraction ___78+[CKContextContentProviderUIScene extractFromScene:usingExecutor:withOptions:]_block_invoke()
4 ContextKitExtraction ___64-[CKContextExecutor addWorkItemToQueue:withWorkItem:andContext:]_block_invoke()
5 libdispatch.dylib __dispatch_call_block_and_release()
6 libdispatch.dylib __dispatch_client_callout()
7 libdispatch.dylib __dispatch_main_queue_drain()
8 libdispatch.dylib __dispatch_main_queue_callback_4CF()
9 CoreFoundation ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
10 CoreFoundation ___CFRunLoopRun()
11 CoreFoundation _CFRunLoopRunSpecific()
12 GraphicsServices _GSEventRunModal()
13 UIKitCore -[UIApplication _run]()
14 UIKitCore _UIApplicationMain()
15 xxxx main(0)
16 dyld start()
崩溃发生在 ContextKitExtraction 里面,不包含任何业务相关的堆栈,所以这个崩溃在之前被误判为 iOS 17 的系统问题。另外崩溃的量级并不多,单日崩溃用户峰值 1000 左右,排查的优先级并不高,也没有分配人力去跟进。
直到有一天,线上用户反馈连续崩溃了 9 次,查看该用户的日志上报,罪魁祸首就这个堆栈。
排查过程
根据用户的行为日志,我们找到了高概率复现的操作路径。说实话,能够复现的崩溃都不是什么难题,但是这个崩溃踩的坑属实是太奇葩了,所以写到这里和大家分享下。
Xcode debug 复现崩溃:
查看 _bestVisibleStringForView
方法内崩溃地址上下文的汇编代码:
<+1488> 处: bl 0x1be14cb20
判断对象是否响应某个方法:
-> 0x1be14cb20: adrp x16, -242784
0x1be14cb24: add x16, x16, #0x84 ; objc_opt_respondsToSelector
0x1be14cb28: br x16
0x1be14cb2c: brk #0x1
0x1be14cb30: adrp x16, -242842
0x1be14cb34: add x16, x16, #0x70c ; objc_release
0x1be14cb38: br x16
0x1be14cb3c: brk #0x1
<+1492> 处 0x1b88cbfa0 <+1492>: tbz w0, #0x0, 0x1b88cc014 ; <+1608>
respondsToSelector 如果返回 false 跳转到 0x1b88cc014 处,否则继续执行。
<+1504> 通过 performSelector 执行该方法:
0x1b88cbfa4 <+1496>: mov x0, x19
0x1b88cbfa8 <+1500>: mov x2, x23
0x1b88cbfac <+1504>: bl 0x1b88da460 ; objc_msgSend$performSelector:
打印 x19 的值,x19 是执行 performSelector 时的 self,值是一个自定义视图对象:
<PickerColumnView: 0x17dcdcca0; frame = (195 0; 195 257); backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x30107df60>>
打印 x23 的值,x23 是执行 performSelector 时的 cmd,值是 “component”:
(lldb) po (char *)$x23 “component”
查看 PickerColumnView 的头文件,发生声明了属性 component, 不过类型是基础类型 NSUInteger。
@property (nonatomic, assign) NSUInteger component; // 当前纵列
排查到这里,整合已有的信息,崩溃的原因就比较明确了。
结论
+[CKContextContentProviderUIScene _bestVisibleStringForView:usingExecutor:]
方法内的处理逻辑为:
- 使用 respondsToSelector 判断视图是否是否响应 component 方法
- 是则通过调用 performSelector 执行 component 方法。
- 对 component 方法返回值,执行 objc_retain。
PickerColumnView 视图,存在 component 方法,但是返回值是 NSUInteger 类型,对基础类型执行 objc_retain 时,触发崩溃。
将 PickerColumnView 对象的 component 属性 rename 可以解决当前的崩溃。
反思
这个崩溃堆栈看起来唬人,但是着手排查后才发现是只纸老虎,前后排查过程大概耗时两个小时,但是崩溃一开始出现时没有及时的去解决,导致这个实际简单的问题在线上持续存在了几个月,一个用户崩了 9 次反馈给了客服,又有多少用户崩溃后直接卸载转向竞品。世上无难事,只怕有心人,处理崩溃也是如此,关键是勇敢的踏出第一步。
另外,在崩溃时如果能获取足够多的信息,在解决问题时就会有更多的筹码,以这个崩溃为例,如果能在线上崩溃时回溯出栈帧上的寄存器信息并上报,也不会被误判为一个单纯的系统问题,所以笔者最近也开始着手调研实现崩溃栈帧信息的回溯。感兴趣的老板可以点点关注,敬请期待后续的更新。