背景

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 复现崩溃:

iOS 17  ContextKitExtraction 崩溃处理

查看 _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:] 方法内的处理逻辑为:

  1. 使用 respondsToSelector 判断视图是否是否响应 component 方法
  2. 是则通过调用 performSelector 执行 component 方法。
  3. 对 component 方法返回值,执行 objc_retain。

PickerColumnView 视图,存在 component 方法,但是返回值是 NSUInteger 类型,对基础类型执行 objc_retain 时,触发崩溃。

将 PickerColumnView 对象的 component 属性 rename 可以解决当前的崩溃。

反思

这个崩溃堆栈看起来唬人,但是着手排查后才发现是只纸老虎,前后排查过程大概耗时两个小时,但是崩溃一开始出现时没有及时的去解决,导致这个实际简单的问题在线上持续存在了几个月,一个用户崩了 9 次反馈给了客服,又有多少用户崩溃后直接卸载转向竞品。世上无难事,只怕有心人,处理崩溃也是如此,关键是勇敢的踏出第一步。

另外,在崩溃时如果能获取足够多的信息,在解决问题时就会有更多的筹码,以这个崩溃为例,如果能在线上崩溃时回溯出栈帧上的寄存器信息并上报,也不会被误判为一个单纯的系统问题,所以笔者最近也开始着手调研实现崩溃栈帧信息的回溯。感兴趣的老板可以点点关注,敬请期待后续的更新。