某个版本上线后,发现一些 Crash 的堆栈无法解析,bugly 堆栈如下图所示。
bugly堆栈1
bugly 还原前后堆栈没啥区别,最初以为 dSYM
上传有问题,导致解析不出来,后面再次确认有上传符号表,同时本地用 atos
也是一样的堆栈。
后面看 bugly
接口发现有个进程内还原的标记位默认为 YES
,如图所示。
bugly标记位
所以在新版本中关掉了这个功能,但是新版本灰度后,这个堆栈之前被还原成符号的部分变成了地址,依旧无法还原。
bugly堆栈2
一时没有什么头绪,和 bugly
同事一起看 dSYM
里面发现有些符号的地址跨度非常大,比较有特征的是有某个 C 方法只有 3 行,但是它的地址跨度有几十 k,有点蹊跷,但还是没什么头绪。
新版本在内测环节捕获到几例类似的堆栈上报,于是联系对应同事拿到系统的 Crash
日志。
一般
Crash
后系统会捕获对应堆栈日志,并将其保存到设备里。获取方式:设置 → 隐私 → 分析和改进->分析数据,列表按字母及日期排序,根据规律即可找到对应文件。如果没有找到,大概率是存满了,可以采用这种方式[1]来清理旧数据,以便新的Crash
日志都能够保存。
拿到日志后,通过 atos
依然无法解析出对应堆栈,但是看到了线程的名称,是下载器起的一个串行队列(但是 bugly
获取到的线程名为空)。
atos后堆栈
从这个线程名入手,发现下载器在最近版本做成了 pod
组件,同时为了不影响启动速度,在 podspec
里面加了 s.static_framework=true
来保证这个库是以静态库的方式来接入。
一开始怀疑是因为加了这一句导致的,于是直接在这个库里面加了个必 Crash
的方法,然后打包,触发这个 Crash
,发现出来的堆栈确实也还原不出来。但是如果连着 Xcode
调试发现堆栈能够正常解析出来。
这里为了快速定位问题,就先注释 podspec
里的那一句,然后再打包触发必现 Crash
发现堆栈能够被正常还原。于是在新版本先将一些组件库 podspec
里的 s.static_framework=true
都注释掉。
因为历史原因,项目里的所有
pod
都是以动态库的方式接入,即use_frameworks!
。
新版上线后,发现之前无法解析的堆栈也能够正常还原了。
虽然问题解决了,但是具体原因还没有找到,所以还要继续挖掘。理论上堆栈无法解析跟s.static_framework=true
这个是无关的,不然 Cocoapods
的 issue
早就爆了。
于是用这个库在 Demo
里做验证,发现即使 podspec
添加上面那一句,打出来的包触发 Crash
也是能够正常解析。这个时候在想是不是工程配置的问题,对比一遍下来也没什么差别。于是对比 Podfile
,发现项目内有一处这样的代码
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
if config.name != 'Debug'
config.build_settings['DEPLOYMENT_POSTPROCESSING'] = 'YES'
config.build_settings['COPY_PHASE_STRIP'] = 'YES'
config.build_settings['STRIP_STYLE'] = 'non-global'
config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
end
end
end
end
这段代码是之前在做包体积优化的时候,对 pod
做了一些编译参数的调整,因为之前的库都是动态库(use_frameworks!
),所以裁剪了符号。但是这里对 pod
并没有做本身是静态还是动态库的判断,那是否是这里把静态库的调试符号也给裁掉了呢?于是注释掉这一段,同时在那个库的 podspec
加上 s.static_framework=true
打包后,触发 Crash
这个时候堆栈就能正常还原了。同时通过上面这段脚本,发现只有在非 Debug
才会做符号裁剪,所以当时连着 Xcode
调试能够正常还原出对应符号,而打包之后就不行。
那实际情况是不是因为这个原因导致的呢?于是把这段代码加到 Demo
的 Podfile
,同时改成 Release
模式运行,Xcode
跑起来触发 Crash 之后就不会显示对应符号了。
Xcode Crash无符号
实际发生的 Crash
代码如下
- (void)makeCrash {
dispatch_async(self.schedulerQueue, ^{
NSMutableArray *a = @[].mutableCopy;
NSString *nilString = nil;
[a addObject:nilString];
});
}
根据上面的分析,这里的修改方式已经很明确了,即对于静态库的 pod
不修改它关于符号裁剪的相关编译参数,只裁剪动态库。
有两种处理方案:
podspec
设置了 static_framework
的 pod
名称,在上述脚本中,过滤这些 pod
,确保这些静态库在生成的时候不会被裁剪符号。pod install
之前直接拿到所有 static_framework
为 true
的 pod
,然后在 post install
时过滤掉这些库。这里直接选用方案 2,因为这种方式一劳永逸,避免后续新增了在 podspec
中指定了 static_framework
的库出现符号无法还原的问题。
具体的方式也很简单, Podfile
中添加类似如下代码即可
# 先获取所有 podspec 中声明是静态库的 pod
all_static_pods = Array.new
pre_install do |installer|
installer.pod_targets.each do |pod|
if pod.static_framework?
all_static_pods.push("#{pod.name}")
end
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
# 非静态库pod才做符号裁剪来减小包体积
unless all_static_pods.include?(target.name)
if config.name != 'Debug'
config.build_settings['DEPLOYMENT_POSTPROCESSING'] = 'YES'
config.build_settings['COPY_PHASE_STRIP'] = 'YES'
config.build_settings['STRIP_STYLE'] = 'non-global'
config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
end
end
end
end
end
关于为什么要给动态库添加这些编译参数来做包体积精简,可以参考《关于 Mac/iOS 平台的符号》[2]
总结:包体积大小优化调整了各 pod
库的编译参数,忽略了 podspec
里指定静态库的 pod
,导致这些静态库的符号被裁掉,没有集成到二进制的 dSYM
里,进而导致堆栈无法符号化。
[1]这种方式: https://stackoverflow.com/questions/13869038/how-can-i-delete-ios-crash-reporter-logs-off-the-device
[2]《关于 Mac/iOS 平台的符号》: https://github.com/Tencent/matrix/wiki/About-macOS-&-iOS-symbol
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8