Crash 堆栈竟然无法解析?

784次阅读  |  发布于3年以前

背景

某个版本上线后,发现一些 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 这个是无关的,不然 Cocoapodsissue 早就爆了。

于是用这个库在 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 调试能够正常还原出对应符号,而打包之后就不行。

那实际情况是不是因为这个原因导致的呢?于是把这段代码加到 DemoPodfile,同时改成 Release 模式运行,Xcode跑起来触发 Crash 之后就不会显示对应符号了。

Xcode Crash无符号

实际发生的 Crash 代码如下

- (void)makeCrash {
    dispatch_async(self.schedulerQueue, ^{
        NSMutableArray *a = @[].mutableCopy;
        NSString *nilString = nil;
        [a addObject:nilString];
    });
}

结论

根据上面的分析,这里的修改方式已经很明确了,即对于静态库的 pod 不修改它关于符号裁剪的相关编译参数,只裁剪动态库。

有两种处理方案:

  1. 拿到已知的 podspec 设置了 static_frameworkpod 名称,在上述脚本中,过滤这些 pod,确保这些静态库在生成的时候不会被裁剪符号。
  2. pod install 之前直接拿到所有 static_frameworktruepod ,然后在 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