ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

团结引擎鸿蒙包Sentry崩溃符号化实战:还原C#行号

团结引擎鸿蒙包Sentry崩溃符号化实战:还原C#行号 1. 为什么要在团结引擎里折腾 Sentry 这套东西如果你正在用团结引擎做鸿蒙平台的游戏或者应用并且已经跑通了打包流程那你大概率会遇到一个很尴尬的局面线上崩溃日志拿到手了但堆栈里全是0x00000000这种地址或者只有一堆libil2cpp.so的偏移量根本看不出是哪一行 C# 代码炸的。这个问题在 IL2CPP 后端下特别明显因为 C# 代码被翻译成了 C 再编译成原生机器码原始的托管堆栈信息在 Release 包里基本被优化掉了。Sentry 本身对 Unity 有官方 SDK但那个 SDK 主要是给标准 Unity 用的团结引擎作为国内深度定制的分支在鸿蒙这个目标平台上有很多自己的改动。我实测下来直接拿官方 Unity SDK 往团结引擎里塞打包阶段就会报一堆平台识别错误即使强行编译过了运行时初始化也会因为鸿蒙的 Native 层差异直接崩掉。所以需要一套针对性的接法核心目标就三个鸿蒙打包能顺利通过、崩溃能上报到 Sentry、上报的堆栈能还原到 C# 的行号级别。这套方案适合谁适合已经用团结引擎出了鸿蒙包、并且被线上崩溃问题折磨过的团队。如果你还在开发阶段本地能复现的 bug 其实用不上这套但一旦进入灰度或者正式上线没有符号化能力的崩溃收集基本等于瞎子摸象。下面我会把整个链路拆开讲包括环境准备、SDK 改造、符号表生成、上传脚本、以及我踩过的几个大坑。2. 鸿蒙打包链路里 Sentry 到底卡在哪几个环节2.1 团结引擎的鸿蒙构建管线与标准 Unity 的差异团结引擎在鸿蒙平台上的构建流程和标准 Unity 导出 Android 或 iOS 有本质区别。鸿蒙用的是 HAP 包格式底层是 ArkTS/ArkUI 加上 Native 的 C 层团结引擎在中间做了一层桥接。当你选择 IL2CPP 作为脚本后端时C# 代码先被转成 C然后通过鸿蒙的 NDK 工具链编译成.so动态库最后打包进 HAP 里。问题就出在这个工具链上。标准 Unity 的 IL2CPP 用的是自己维护的 il2cpp 运行时和一套固定的编译参数而团结引擎为了适配鸿蒙替换了部分底层实现编译出来的符号信息和标准 Unity 不完全兼容。Sentry 的 Unity SDK 在初始化时会去读Application.platform和SystemInfo.operatingSystem来判断运行环境鸿蒙返回的值它不认识直接走了 fallback 分支导致 Native 层的 crash handler 没挂上。2.2 Sentry SDK 在鸿蒙 Native 层的初始化失败原因Sentry 的崩溃捕获分两层C# 层的异常捕获和 Native 层的信号捕获。C# 层的SentrySdk.Init一般能跑通因为那是纯托管代码。真正要命的是 Native 层Sentry 依赖sentry-native这个库来捕获 SIGSEGV、SIGABRT 这些信号而sentry-native在鸿蒙上需要重新编译官方预编译的.so是给 Android 用的链接的 libc 版本和鸿蒙的 musl libc 对不上。我一开始偷懒直接把 Android 版的libsentry.so丢进鸿蒙工程结果打包时 hvigor 报了一堆 undefined symbol全是__android_log_print这类 Android 特有的符号。后来换成鸿蒙 NDK 重新编译sentry-native才把链接错误消掉。但新的问题又来了编译出来的.so虽然能加载但信号处理器挂上之后崩溃时抓到的堆栈地址全是无效的因为鸿蒙的地址空间布局和 Android 不一样ASLR 的偏移计算方式也有差异。2.3 IL2CPP 符号信息在 Release 包里的丢失路径就算 Native 层能抓到地址了你拿到的也只是一串十六进制数。要还原成 C# 行号需要两样东西IL2CPP 生成的符号映射文件通常是LineNumberMappings.json或者symbols文件夹里的内容以及 Native 层的调试符号.so里的 DWARF 信息。团结引擎在打 Release 包时默认会 strip 掉.so里的调试符号来减小包体同时 IL2CPP 的符号映射文件也不会自动保留在输出目录里。我翻了一遍团结引擎的构建日志发现它在BuildPlayer阶段会把il2cpp_output临时目录清掉符号文件就在那个目录里。如果你不提前 hook 构建回调把文件拷出来打完包就再也找不到了。这个坑我踩了整整一个下午最后是通过在IPostprocessBuildWithReport接口里加拷贝逻辑才解决的。3. 把 Sentry 接进团结引擎鸿蒙工程的具体操作3.1 环境准备与依赖版本锁定先列一下我实测通过的版本组合这个很关键版本对不上后面全是玄学问题组件版本说明团结引擎1.3.x LTS鸿蒙支持比较稳定的版本Sentry Unity SDK1.5.x不要用最新的 2.xAPI 变动大鸿蒙 NDK与引擎匹配的版本在团结引擎安装目录里找sentry-native0.6.x需要自己用鸿蒙 NDK 编译Python3.8跑符号上传脚本用Sentry Unity SDK 我建议从 GitHub 拉源码而不是用 UPM 包因为要改里面的平台判断逻辑。拉下来之后把Sentry.Unity和Sentry两个核心目录拷进Assets/Plugins下面注意不要整个仓库丢进去不然 Unity 会去编译测试用例。3.2 改造 Sentry SDK 的平台识别逻辑SDK 里判断平台的地方主要在SentryUnityInit和SentryNativeBridge这两个文件。你需要找到类似Application.platform RuntimePlatform.Android的判断加一个鸿蒙的分支。团结引擎在鸿蒙上运行时Application.platform返回的是RuntimePlatform.Android的变体还是自定义枚举这个取决于引擎版本我遇到的是返回了一个非标准值所以最稳妥的做法是用SystemInfo.operatingSystem里是否包含 Harmony 来做二次判断。具体改法是在初始化 Native bridge 之前加一段#if UNITY_ANDROID !UNITY_EDITOR if (SystemInfo.operatingSystem.Contains(Harmony)) { // 走鸿蒙专用的 native 库加载路径 SentryNativeBridge.LoadLibrary(sentry-native-harmony); } else { SentryNativeBridge.LoadLibrary(sentry-native); } #endif同时要把sentry-native-harmony.so放到Assets/Plugins/Android/libs/arm64-v8a/下面注意鸿蒙目前主要跑在 arm64 上armeabi-v7a 的兼容性我还没测过建议先只出 arm64 包。3.3 用鸿蒙 NDK 编译 sentry-native 的完整命令这一步是整个流程里最耗时的但必须做。先把 sentry-native 源码拉下来然后配置 CMake 工具链。鸿蒙 NDK 的路径一般在团结引擎安装目录的Editor/Data/PlaybackEngines/HarmonyPlayer/NDK下面找到ohos.toolchain.cmake文件。编译命令大概长这样cmake -B build -S . \ -DCMAKE_TOOLCHAIN_FILE$HARMONY_NDK/build/cmake/ohos.toolchain.cmake \ -DOHOS_ARCHarm64-v8a \ -DOHOS_PLATFORMOHOS \ -DSENTRY_BACKENDinproc \ -DSENTRY_BUILD_SHARED_LIBSON \ -DCMAKE_BUILD_TYPERelease \ -DSENTRY_TRANSPORTcurl这里有几个参数要解释一下。SENTRY_BACKENDinproc是让崩溃捕获在进程内完成不用起额外的子进程鸿蒙对子进程的管理比较严格用 inproc 更稳。SENTRY_TRANSPORTcurl是因为鸿蒙自带 curl不用自己再编一个。编译完之后把生成的libsentry.so改名成sentry-native-harmony.so丢进 Plugins 目录。注意编译时如果报找不到libcurl需要在 CMake 参数里手动指定CURL_INCLUDE_DIR和CURL_LIBRARY指向鸿蒙 NDK 里的 curl 头文件和库。3.4 在构建回调里保住 IL2CPP 符号文件前面说过团结引擎打完包会把中间产物清掉所以要在构建完成回调里把符号文件拷出来。写一个继承IPostprocessBuildWithReport的类public class SymbolExporter : IPostprocessBuildWithReport { public int callbackOrder 999; public void OnPostprocessBuild(BuildReport report) { string il2cppDir Path.Combine(report.summary.outputPath, .., Temp, StagingArea, il2cpp_output); string destDir Path.Combine(Application.dataPath, .., SymbolsBackup); if (Directory.Exists(il2cppDir)) { CopyDirectory(il2cppDir, destDir); } } }这个il2cpp_output的路径在不同引擎版本里可能不一样我建议你先打一次包然后去 Temp 目录里翻一下实际位置再把这个路径写死。拷出来的文件里最关键的是LineNumberMappings.json和symbols文件夹前者是 C# 行号映射后者是 Native 符号。4. 符号化还原到 C# 行号的完整链路4.1 崩溃上报的数据流拆解一次完整的崩溃上报数据流是这样的鸿蒙系统触发信号sentry-native-harmony.so捕获信号并生成一个 envelope里面包含 Native 堆栈的地址列表和模块信息。这个 envelope 通过 HTTP 发到 Sentry 服务端。服务端拿到之后先做 Native 符号化把地址转成函数名和行号。然后 Sentry 的 Unity 处理器会去读 IL2CPP 的符号映射把 Native 函数名再映射回 C# 的方法名和行号。关键点在于Sentry 服务端需要知道你的LineNumberMappings.json和.so的调试符号文件。这两个东西必须在上报之前上传到 Sentry 的符号服务器或者作为 release artifact 关联到对应的 release 版本上。4.2 用 sentry-cli 上传符号表的实操步骤sentry-cli是 Sentry 官方的命令行工具上传符号全靠它。先安装npm install -g sentry/cli # 或者直接下二进制 curl -sL https://sentry.io/get-cli/ | bash然后配置认证sentry-cli login # 或者用 token export SENTRY_AUTH_TOKENyour_token_here export SENTRY_ORGyour_org export SENTRY_PROJECTyour_project上传 Native 符号用upload-dif命令sentry-cli upload-dif --org $SENTRY_ORG --project $SENTRY_PROJECT ./SymbolsBackup/symbols上传 IL2CPP 映射文件稍微麻烦一点因为 Sentry 没有直接支持这种格式需要先转成它认识的格式。我写了一个 Python 脚本把LineNumberMappings.json转成 Sentry 的source bundle格式然后用sentry-cli sourcemaps upload上传。这个脚本的核心逻辑是遍历 JSON 里的每个方法提取assembly、class、method、file、line这几个字段拼成 Sentry 能识别的路径格式。4.3 验证符号化是否生效的三种方法上传完之后怎么确认生效了我一般用三种方式交叉验证。第一种是看 Sentry 后台的 issue 详情页如果堆栈里显示的是MyClass.MyMethod() at Assets/Scripts/MyClass.cs:42这种格式说明 C# 行号还原成功了。如果只显示libil2cpp.so加偏移量说明 Native 符号没传对。第二种是用sentry-cli debug-files check命令检查某个.so的调试 ID 是否已经在服务端存在。这个命令会输出每个模块的 UUID 和上传状态很直观。第三种是本地模拟一次崩溃用SentrySdk.CaptureException手动抛一个异常看上报的数据里有没有debug_meta字段。这个字段里包含了符号文件的引用信息如果为空说明上传链路断了。5. 我踩过的几个坑和对应的绕行方案5.1 鸿蒙打包时 hvigor 报符号冲突的解决过程第一次把编译好的sentry-native-harmony.so放进工程后hvigor 打包直接报了一堆duplicate symbol错误全是curl_开头的函数。原因是鸿蒙系统库里已经带了 curl我的.so里又静态链接了一份链接器不知道该用哪个。解决办法是在 CMake 里把 curl 改成动态链接加-DSENTRY_TRANSPORT_SHAREDON然后确保libcurl.so在鸿蒙的system/lib64里能找到。如果还是冲突可以在打包配置里加--exclude-libs参数把重复的符号排除掉。5.2 Release 包崩溃堆栈全是问号的排查链路有一次打完 Release 包崩溃上报上来的堆栈全是???连 Native 函数名都没有。我按这个顺序排查了一遍先检查.so是不是被 strip 了。用readelf -S libil2cpp.so | grep debug看有没有.debug_info段如果没有说明符号被剥了。团结引擎在 Release 模式下默认会 strip需要在PlayerSettings里把Strip Engine Code关掉或者在构建脚本里加-DCMAKE_BUILD_TYPERelWithDebInfo。然后检查LineNumberMappings.json是不是空的。如果文件存在但内容只有几行说明 IL2CPP 在编译时把托管代码优化掉了需要在link.xml里保留相关程序集或者把Managed Stripping Level调到Low。最后检查 Sentry 的 release 版本号是不是和上传符号时用的版本号一致。Sentry 是按 release 来匹配符号的如果打包时用的版本号是1.0.0123上传时用的是1.0.0那就匹配不上。这个坑最隐蔽我查了两个小时才发现。5.3 符号文件体积过大导致上传超时的处理IL2CPP 的符号文件动辄几百 MBsentry-cli上传时经常超时。我的做法是先把符号文件用zstd压缩然后分批上传。sentry-cli支持--batch-size参数可以控制每次上传的文件数量。另外如果你们的 Sentry 是自建的记得把 nginx 的client_max_body_size调大默认的 1MB 肯定不够。还有一个取巧的办法只上传崩溃相关的符号。用sentry-cli upload-dif的--include-sources参数只传有源码引用的那部分符号体积能小一半以上。6. 上线之后怎么持续维护这套符号化链路6.1 把符号上传塞进 CI 流水线的正确姿势手动上传符号迟早会漏最好的办法是塞进 CI。我的做法是在打包脚本的最后加一个 post-build 步骤自动跑sentry-cli上传。关键是要把SENTRY_AUTH_TOKEN存在 CI 的环境变量里不要硬编码在脚本里。流水线的大致顺序是团结引擎命令行打包 - 拷贝符号文件到临时目录 - 用 Python 脚本转换 IL2CPP 映射 -sentry-cli upload-dif上传 Native 符号 -sentry-cli sourcemaps upload上传转换后的映射 - 创建 release 并关联 artifact。这一套跑下来大概五到十分钟取决于符号文件大小。6.2 版本号管理与 release 关联的注意事项Sentry 的 release 版本号必须和打包时的版本号严格一致。我建议在团结引擎的PlayerSettings里把Bundle Version设成一个带构建号的值比如1.0.020260115然后在 CI 里把这个值读出来传给sentry-cli。不要用latest或者dev这种模糊的版本号不然符号匹配会乱套。另外每次发版都要创建一个新的 release不要复用旧的。Sentry 的 issue 是按 release 聚合的如果版本号不变新旧崩溃会混在一起排查起来很痛苦。6.3 线上崩溃聚合后的快速定位经验符号化搞定之后Sentry 后台的 issue 列表就很有用了。我一般按release和environment两个维度过滤先看新版本有没有引入新的崩溃。如果某个 issue 的first seen时间正好是发版时间那基本就是这次改动引入的。点进 issue 详情重点看三样东西C# 行号、崩溃时的设备信息、以及breadcrumbs里的操作路径。C# 行号直接告诉你哪一行代码炸了设备信息能看出是不是特定机型的问题breadcrumbs 能还原用户崩溃前的操作序列。这三样结合起来大部分崩溃都能在十分钟内定位到根因。提示如果 C# 行号显示的是unknown先检查LineNumberMappings.json里有没有这个方法。IL2CPP 对泛型方法和异步状态机的处理比较特殊有些方法确实映射不出来这种情况只能靠 Native 函数名和上下文推断。7. 几个容易被忽略的细节补充7.1 鸿蒙权限配置里别忘了网络和文件读写Sentry 上报崩溃需要网络权限这个在鸿蒙的module.json5里要声明ohos.permission.INTERNET。另外sentry-native在本地会缓存未发送的 envelope需要文件读写权限加上ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA。这两个权限不加的话崩溃能捕获但发不出去或者发出去之后本地缓存清不掉时间长了占空间。7.2 混淆与符号化的冲突处理如果你们用了代码混淆工具IL2CPP 生成的符号名会被改掉LineNumberMappings.json里的方法名和实际运行时的对不上。解决办法是在混淆配置里把需要符号化的程序集排除掉或者在上传符号之前先用混淆映射表把符号名还原。我一般建议核心业务逻辑不要混淆不然崩溃排查的成本太高得不偿失。7.3 多平台同时接入时的符号隔离如果你们的项目同时出鸿蒙包和 Android 包符号上传时要注意隔离。Sentry 的upload-dif是按 debug ID 来匹配的不同平台的.sodebug ID 不一样所以不会串。但 IL2CPP 的LineNumberMappings.json如果两个平台共用一份可能会有路径冲突。我的做法是给每个平台建一个独立的 Sentry project符号分开上传这样最干净。这套方案我在两个项目上跑过从鸿蒙打包到崩溃符号化还原整个链路是通的。最耗时间的部分其实是第一次编译sentry-native和调试符号上传一旦跑通之后后面就是 CI 自动化的事了。如果你在某个环节卡住了优先检查版本匹配和路径配置这两个地方出问题的概率最高。
返回列表