作者:字节移动技术——陈奕
去年 9 月份开始,许多用户升级到 iOS 14 之后,线上出现很多 ImageIO 相关堆栈的 Crash 问题,而且公司内几乎所有的 APP 上都有出现,在部分 APP上甚至达到了 Top 3 Crash。
得益于 APM 平台精准数据采集机制和丰富的异常信息现场,我们通过收集到详细的 Crash 日志信息进行分析解决。
从堆栈信息看,是在 ImageIO 解析图片信息的时候 Crash ,并且最后调用的方法都是看起来都是和 INameSpacePrefixMap
相关,推测 Crash 应该是和这个方法 CGImageSourceCopyPropertiesAtIndex
的实现有关。
机型集中在 iOS14 以上的版本,同时是在后台出现
img从 CrashLog 做一个初步分析
img从堆栈信息看,这段代码是图片库在子线程通过 CGImageSourceCopyPropertiesAtIndex
解析 imageSource
中的图片相关信息,然后发生了野指针的 Crash。
CGImageSourceCopyPropertiesAtIndex
的输入只有一个 imageSource
,imageSource
由图片的 data 生成,调用栈并没有多线程操作,可以排除是多线程操作 imageSource
、data 导致的 Crash。
看堆栈是在解析 PNG 图片,通过将下发的图片格式换成 JPG 格式,发现量级并没有降低。推测 Crash 不是某种特定图片格式引起的。
iOS 14.3 的 iPhone 8
ImageIO 系统库:~/Library/Developer/Xcode/iOS DeviceSupport目录下找到对应 iOS 14.3 的 ImageIO
一份 iOS 14.3、iPhone 8 上发生的 CrashLog
Hopper
1、从 CrashLog 上找到 Crash 对应的指令偏移地址 2555072
2、通过 Hopper 打开 ImageIO,跳转到指令偏移地址 2555072
Navigate => Go To File Offset 2555072
3、Crash 对应的指令应该是0000000181b09cc0 ldr x8, [x8, #0x10]
,可以看到应该是访问 [x8, #0x10]
指向的内存出错
4、查看 Crashlog 中对应寄存器的值,错误地址 far: 0x000021a1ee2fa271
,而且 x8 寄存器已经是一个错误的值 0x000021a1ee2fa261
5、向上回溯查看 x8 的来源
0000000181b09cbc ldr x8, [x20]
x8 是存在 x20 指向的内存中(即 x8 = *x20
)
0000000181b09c98 ldr x20, [x21, #0x8]
x20 又存在[x21, #0x8]
指向的内存中
0000000181b09c8c adrp x21, #0x1da0ed000
,0000000181b09c90 add x21, x21, #0xe10
x21 指向的是一个 data 段,推测 x21 应该是一个全局变量,所以,可能是这个全局变量野了,或者是这个全局变量引用的某些内存(x20)野了
6、运行时 debug 查看 x8、x20、x21 对应寄存器的值是什么
x21 从内存地址的名字看,应该是一个全局的 Map
ImageIO`AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap
7、从 Hopper 上看,这个sDefaultNameSpacePrefixMap
只在
AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)
这个函数中调用。可能会在多线程下调用这个函数,而导致这个全局变量的出现 data race 导致了野指针。
__ZZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEbE26sDefaultNameSpacePrefixMap: // AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap
00000001da0ede10 dq 0x0000000000000000 ; DATA XREF=__ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+44, __ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+120, __ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+392
8、经过在运行时反复调试,这个
AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)
会在多个方法中调用(并且调用时都加了锁,不太可能会出现 data race):
AdobeXMPCore_Int::INameSpacePrefixMap_I::CreateDefaultNameSpacePrefixMap()
AdobeXMPCore_Int::INameSpacePrefixMap_I::InsertInDefaultNameSpacePrefixMap(char const*, unsigned long long, char const*, unsigned long long)
AdobeXMPCore_Int::INameSpacePrefixMap_I::DestroyDefaultNameSapcePrefixMap()
9、在后台线程访问访问全局变量 sDefaultNameSpacePrefixMap
时 Crash,推测可能是用户手动杀进程后,全局变量在主线程已经被析构,后台线程还会继续访问这个全局变量,从而出现野指针访问异常。发现 Crash 日志的主线程堆栈也出现 _exit 的调用,可以确定是全局变量析构导致。
在用户手动杀进程后,主线程将这个全局变量析构了,这时候子线程再访问这个全局变量就出现了野指针。
img尝试在子线程不断调用 CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options);
,并且手动杀掉进程触发这个 crash
可以证明上述的推理是正确的。
CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options);
这个方法在解析部分图片的时候最终会访问全局变量
ImageIO`AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap
在用户手动杀进程后,这个sDefaultNameSpacePrefixMap
被析构,如果这时候在子线程再被访问就可能出现野指针的问题
因为sDefaultNameSpacePrefixMap
是在系统库内部的全局变量,没办法对其进行修改,只能避免在子线程调用 CGImageSourceCopyPropertiesAtIndex
方法
方法一:CGImageSourceCopyPropertiesAtIndex
是用来获取图片的宽高、imageOrientation
、动图帧等信息,选择用其他方法来替换,e.g. 宽高用 CGImageRef
来获取
方法二:将 CGImageSourceCopyPropertiesAtIndex
被调用的线程收敛起来,调用atexit函数来注册一个进程结束回调函数,进程结束的时候将终止线程
字节跳动移动平台团队(Client Infrastructure)是大前端基础技术行业领军者,负责整个字节跳动的中国区大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率,支持的产品包括但不限于抖音、今日头条、西瓜视频、火山小视频等,在移动端、Web、Desktop等各终端都有深入研究。
就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣可以联系邮箱 [email protected],邮件主题 简历-姓名-求职意向-电话。