先说结论团结引擎导出鸿蒙包然后接 Sentry 做崩溃采集这条路是能走通的而且最终能把 Native 崩溃堆栈还原到 C# 的方法和行号。我们项目组大概花了两周半时间把这条链路完全跑通中间踩了十几个坑。这篇文章把整个流程、原理和能直接抄的配置都整理出来给后面要搞鸿蒙崩溃监控的同行省点时间。这篇文章适合谁看准备在团结引擎项目里接 Sentry、或者已经把 Unity 项目切到鸿蒙平台但不知道怎么上报崩溃、或者正在被堆栈全是 C 函数名和十六进制地址折磨的人。我会把三件事讲透怎么从团结引擎导出能安装的鸿蒙包、Sentry 在鸿蒙上到底怎么接、以及如何让崩溃堆栈准确映射回 C# 行号而不是让你对着汇编地址发呆。1. 为什么要在团结引擎的鸿蒙包里接 Sentry先交代一下背景。我们项目原来是标准的 Unity 2022 LTS 工程因为要上鸿蒙发布到应用市场整体迁移到了团结引擎。团结引擎就是 Unity 的中国版基于 Unity 2022 LTS 做的分叉最大的价值就是原生支持 HarmonyOS NEXT 的导出不用像以前那样先导出 Android 再转手改鸿蒙。换引擎这件事本身不复杂真正让我失眠的是崩溃排查。HarmonyOS NEXT 上跑的是 IL2CPP 编译出来的 Native 代码线上用户一旦闪退我们拿到的信息大概率是这样Device Log 里几十行信号地址看起来像天书。没有堆栈、没有方法名、没有行号想定位问题基本靠猜。这时候 Sentry 的价值就体现出来了——它不只是一个日志上报工具更重要的是它有一套完整的符号化系统你只需要把构建产物里的符号文件传上去它就能在后台把崩溃地址还原成人能读懂的 C# 方法名、文件路径和行号。1.1 鸿蒙崩溃监控的现状我在调研阶段翻遍了国内各种社区发现鸿蒙上的崩溃监控方案大概分成三类。第一类是用华为自家的 AppGallery Connect 崩溃服务它和鸿蒙生态集成得最自然但问题是我们团队已经用 Sentry 管理了 iOS 和 Android 的全部崩溃再引入第二个平台意味着团队要维护两套后台、两套告警规则、两套账号体系协作成本直接翻倍。第二类是自己写一个崩溃日志采集器捕获 signal 然后把堆栈写到本地文件听起来简单但堆栈还原、符号文件管理、告警通知全部要自己撸工作量远超预期。第三类就是这篇文章写的方案继续用 Sentry只不过接入方式要自己改造。1.2 整体方案拆成两半来看搞清思路的关键是把接 Sentry这件事拆成两个独立的部分。第一部分是托管层C# 抛出的异常、Debug.LogError打出的错误日志、Application.logMessageReceived能收到的消息这些属于托管层数据。第二部分是 Native 层SIGSEGV、SIGABRT、C throw 导致的崩溃这些发生在 IL2CPP 虚拟机或者我们自己写的 C 插件里需要底层的信号捕获能力。Sentry 官方 Unity SDK 支持 Android、iOS、macOS、Windows但 HarmonyOS NEXT 并不在官方支持列表里。所以官方 SDK 在鸿蒙上不能直接用需要拆开用托管层的 SDK 本身大部分是纯 C# 实现这部分可以在鸿蒙环境跑Native 层的采集需要通过别的方式补上。这里先记住结论就行后面第三章会讲具体怎么做。2. 团结引擎导出鸿蒙工程从 Unity 到 DevEco 的完整链路先把打包这条链路搞清楚后面做符号化才有基础。我用的是团结引擎 1.1.0 版本鸿蒙 SDK 用 API 12 那一代不同版本界面略有差异但整体思路一致。2.1 导出前的准备在 Unity Hub 里安装团结引擎的时候记得勾选 HarmonyOS 模块不然 Build Settings 里根本看不到这个平台。工程切过来之后在 Build Profiles 窗口新建一个 HarmonyOS 配置这里有几个参数值得注意。Architecture 我建议只勾 arm64鸿蒙真机基本都是 ARM 架构留着 x86_64 还会让安装包变大。Scripting Backend 选择 IL2CPPMono 后端在鸿蒙上兼容性不行而且后面要做 C# 行号符号化的话IL2CPP 是唯一选择。Target SDK Version 看产品需求但尽量不要低于 API 11否则一些新的系统 API 不支持。导出前还有一个非常容易忽略的地方包名。鸿蒙应用的包名格式和 Android 类似但应用市场有自己的规则建议提前在 DevEco Studio 里创建一个空工程核对一下 Bundle Name 的合法性别等导出来再后悔。另外Unity 里的公司名、产品名也要提前设置好它们会直接影响导出工程的签名和显示名称。2.2 导出后的工程结构点击导出后团结引擎会生成一个标准的 HarmonyOS 工程目录里面有一堆.json5文件、ohpm配置、entry模块等等。这个工程要用 DevEco Studio 打开而不是用 Visual Studio 或 Android Studio。我刚开始不熟悉 DevEco打开工程之后第一件事就是点 Build结果编译失败——因为签名没配置。鸿蒙应用签名和 Android 不太一样它需要在 AppGallery Connect 上申请证书和 Profile然后把这些文件配置到工程的build-profile.json5里。如果你是团队里第一个做鸿蒙打包的人这部分是最花时间的要去华为开发者平台创建项目、添加应用、申请证书然后把certificate.p12、profile.p7b这些文件下载下来填到工程的签名配置中。实际上在开发调试阶段DevEco 可以自动生成调试证书也就是 Automatic 签名模式能省掉不少前期配置的功夫但上架和应用市场审核的时候必须切到手动签名。2.3 真机安装与调试编译通过之后用 DevEco 直接 Run 到真机上。鸿蒙开发调试的链路和 Android 类似手机开启开发者模式后通过 USB 连接电脑DevEco 会识别设备并安装应用。这里有一个比较坑的地方如果你在导出时设置了Debug构建类型包可以直接装但如果是Release构建就必须签上正确的证书才能安装。我们当时直接用了 Release 包调试结果安装时一直报签名错误后来发现开发调试阶段老老实实切回 Debug 就好了。整个打包链路跑通之后接下来才能在工程基础上做 Sentry 接入的改造。记住一个关键点每次从团结引擎重新导出鸿蒙工程之前对导出工程做的所有修改都可能被覆盖所以后面所有 Sentry 相关的定制化内容最好通过自定义构建脚本自动 patch 到导出工程别手动改否则迟早会改疯。3. Sentry 在 C# 层的接入与平台适配前面说过官方 Sentry Unity SDK 不直接支持鸿蒙。这一章讲我们最终采用的接入方案以及每一步的取舍原因。3.1 官方 SDK 在鸿蒙上为什么不行把 Sentry Unity SDK 装进团结引擎工程后你会发现SentrySdk.Init在编辑器里跑得好好的但打包到鸿蒙真机上就出问题——要么初始化时静默失败要么直接抛异常。原因在于官方 SDK 的初始化流程里有平台桥接逻辑在 Android 上它通过AndroidJavaObject调 Java 层在 iOS 上通过DllImport调 C 层而鸿蒙平台两个入口都没有SDK 找不到合适的原生后端就直接放弃了。但 SDK 的核心部分——事件的组装、队列发送、离线缓存——其实都是纯 C# 写的和平台无关。所以我们最初试了一下只在 C# 层调用把原生后端强行关掉结果托管层的崩溃和日志上报是可以正常工作的。具体做法是在初始化时只给一个 DSN不启用在官方 SDK 里默认开启的 Native 捕获选项。3.2 推荐做法官方托管 API 自研信号捕获我的最终方案是三层结构。第一层用官方 SDK 的托管 API用来上报 C# 异常和Debug.Log日志第二层写一个很小的 C 信号处理器编译成.so后放进鸿蒙工程专门捕获 Native 崩溃并生成带地址信息的崩溃信息第三层用 Sentry CLI 上传符号文件让 Sentry 后台能做地址到 C# 行号的还原。第一层的初始化代码大概长这样using Sentry; public static class SentryBridge { public static void Init(string dsn) { SentrySdk.Init(options { options.Dsn dsn; options.SendDefaultPii true; options.Environment production; options.TracesSampleRate 0.2f; options.AutoSessionTracking false; }); } public static void Capture(System.Exception e) { SentrySdk.CaptureException(e); } }同时挂一个全局日志监听把 Unity 里的LogError和LogException都转成 Sentry 事件这样开发者在代码里随手打的错误日志也能进 SentryApplication.logMessageReceivedThreaded (condition, stackTrace, type) { if (type LogType.Error || type LogType.Exception) { SentrySdk.CaptureMessage(condition, SentryLevel.Error); } };3.3 Native 崩溃捕获的实现思路C 信号处理器这个部分很多团队会直接用 Sentry 官方提供的sentry-native库来交叉编译但我这里选择自己写一个最小实现主要是为了保持依赖可控。原理很简单注册SIGSEGV、SIGABRT、SIGBUS这几个信号的处理器崩溃发生时读取当前线程寄存器里的 PC 地址结合dladdr解析出模块名和偏移量再把堆栈信息写到一个临时文件或者直接通过__android_log_print输出。这个信息随后会在下一次启动时由 C# 层读取并上报到 Sentry。这个自研信号处理器的质量离生产级还差得远如果你没有足够的 C 信号处理经验建议直接用社区编译好的 sentry-native 鸿蒙包或者找你们客户端团队帮你做。关键点是无论用哪种方案最终你手上的崩溃信息必须包含模块名 符号偏移因为符号化依赖的就是这两样东西。4. 崩溃符号化到 C# 行号的原理与配置到了这篇文章最核心的部分为什么地址能还原成 C# 行号以及具体怎么配。4.1 崩溃地址到 C# 行号的完整链路当用户手机上跑的是 IL2CPP 打包的应用真正的执行体是libil2cpp.so你在 C# 里写的方法已经变成了 C 函数再被编译成机器码。崩溃时系统记录下来的现场是某个共享库的某个地址这个地址本身是数字没有任何意义。符号化有三级还原过程。第一级是地址到函数名通过 DWARF 调试信息把崩溃地址换算成函数符号例如得到Game::UI::GuideManager::ShowGuide()。第二级是函数到 C# 文件IL2CPP 在编译时会生成一个行号映射表它记录了每个 C 函数对应哪个 C# 源文件、哪一行。第三级是指令偏移到行号结合 PC 地址离函数入口的偏移量能从映射表里折算出更精确的行号。Sentry 后台做的就是在收到崩溃事件后拿你上传的符号文件去做这三级还原。4.2 需要上传哪些文件我做这个项目时尝试了很多种上传文件组合最后稳定生效的是以下两样。第一样是libil2cpp.so的调试符号文件它包含 DWARF 信息能让堆栈还原到 C 函数名。第二样是 IL2CPP 的行号映射一般由 Unity 构建时生成路径通常在导出工程的build目录下。需要重点检查构建后产物里的LineNumberMap相关内容不同 Unity 版本生成文件名不一样但一般都是以LineNumberMap前缀命名。上传时我用的是sentry-cli的debug-files upload命令把文件当作一个通用 ELF 符号文件上传sentry-cli debug-files upload \ --org your-org \ --project your-project \ --include-sources \ ./Temp/StagingArea/libs/arm64-v8a/libil2cpp.so行号映射文件可以用debug-files upload一并传Sentry 后台识别到对应的 IL2CPP 调试信息后会自动把堆栈还原到 C# 行号。这里提醒一下命令里的 org 和 project 不能填错否则上传的文件匹配不到你的项目符号化照样不生效。4.3 在构建流程里自动上传手动上传只能用于验证上线版本必须自动化。我们是在团结引擎打包之后加了一个 Python 脚本自动定位产物目录下的libil2cpp.so和LineNumberMap然后调用sentry-cli上传。脚本里还有一个重要逻辑版本号要跟你在 Sentry 里设的 Release 版本保持一致否则后台会把事件和符号文件错配。脚本大致思路import os import subprocess def upload_symbols(build_dir, org, project, release): # 精确匹配 libil2cpp.so 和 line-number-map 文件 so_path os.path.join(build_dir, libs, arm64-v8a, libil2cpp.so) if os.path.exists(so_path): subprocess.run([ sentry-cli, debug-files, upload, --org, org, --project, project, --include-sources, so_path ], checkTrue) # 遍历查找 LineNumberMap 相关文件 for root, dirs, files in os.walk(build_dir): for f in files: if LineNumberMap in f: subprocess.run([ sentry-cli, debug-files, upload, --org, org, --project, project, os.path.join(root, f) ], checkTrue)有个容易忽略的细节在 CI 机器上跑sentry-cli前先设置环境变量SENTRY_AUTH_TOKEN并且不要在脚本里硬编码 token写死在构建日志里会被安全扫描盯上。4.4 验证符号化是否生效配置完之后我建议先人为制造两种崩溃来验证。第一种是托管异常在代码里写一个空引用异常捕获后上报第二种是 Native 崩溃在导出工程里加一个 C 函数直接调用一个空指针导致 SIGSEGV。两种崩溃产生后到 Sentry 后台的事件详情页里看堆栈信息。如果一切正常Native 崩溃的堆栈第一帧应该是GameNative::TestCrash()这样的 C 方法名再往下能跟出一串Assets/Game/UI/Guide.cs:42这样的 C# 文件行号。如果只看到地址和 C 函数名、没有 C# 行号大概率是行号映射文件没上传成功或者上传时项目号填错。5. 踩坑记录打包与 Sentry 集成中的高频问题最后整理一下我们当时踩过并且花了最多时间排查的问题按踩坑顺序排列。5.1 初始化时机不能早于文件系统就绪Sentry 在鸿蒙上初始化时如果写了一个持久化缓存目录而该目录在应用启动早期还不可用上传队列就会反复失败且没有任何报错。建议不要放在Application-initializer里初始化放到第一个场景加载之后再初始化或者显式指定一个可用的缓存路径。5.2 行号补偿问题我最开始验证符号化时发现所有崩溃的 C# 行号都比实际位置偏了一行到两行。这不是 Sentry 的 bug而是 IL2CPP 编译时行号映射表生成规则导致的某些构建模式下行号记录的是方法声明行而不是实际执行行。解决方式不是硬调偏移而是去查你的 Unity 构建参数里有没有开启脚本调试信息相关的优化开关。很多公开的崩溃上报工具也有同样的现象不用过分在意只要行号落在同一个方法内定位问题的效率已经很高了。5.3 DevEco 工程覆盖问题团结引擎每重新导出一次DevEco 工程就会被覆盖。如果你手动改过module.json5、.so或者 C 代码下次导出就不见了。第一次发生这种事情时我们排查了一个多小时以为是自己写错了代码。后来直接把所有定制内容都固化到构建脚本里每次导出后自动重新 patch 一次。5.4 不要混淆 Android 符号文件和鸿蒙符号文件有些团队图省事直接把 Android 构建产物里的libil2cpp.so用sentry-cli传到同一个项目。这在某些时候是能工作的因为两者都是 ELF 结构但 C# 方法对应的行号映射很可能不一致——Android 和鸿蒙两个包如果是不同构建时间生成的行号就对不上。符号文件必须和用户实际运行的版本完全同步上传。5.5 模拟器上符号化成功不代表真机成功模拟器是 x86 架构真机是 arm64两个架构下的崩溃地址和符号差异很大。我在模拟器上验证通过后信心满满地给真机装包结果真机上所有崩溃都还原不了。后来发现是构建脚本里取.so的路径写死了 arm64 目录在模拟器上跑直接跳过符号文件压根没传上去。验证符号化功能时统一用 arm64 真机不要图方便用 x86 模拟器。写在最后的小建议如果你正在搞同样的方案我的建议是先把托管异常上报和Native 崩溃采集拆开验收。第一步先确认 C# 异常能出现在 Sentry 后台第二步再确认 Native 信号处理器能写出崩溃信息最后才上符号化。每一步都验证完再合到一起排查效率会高很多。符号化配置那块最容易出问题的是文件匹配确保上传符号文件时--project和代码里初始化的 DSN 是同一个项目版本号也一致基本能把成功率拉满。另外提醒一句鸿蒙生态更新很快团结引擎版本、DevEco 版本、API Level 这三者之间的兼容性经常变。如果你用的版本组合和这篇文章不完全一致先花半天时间把空工程跑通再把 Sentry 集成进去别直接用主工程试错能省下大量时间。