线上反馈群突然热闹起来:有人说应用秒退,有人录屏说列表滑动像PPT,还有人截了一张电池温度截图说手机烫得快握不住。如果你接手的是一个 Flutter 鸿蒙应用,听到这三个症状时,最忌讳的就是立刻打开源码开始猜。猜一个改一个,改完发版,下个版本又冒出来,循环往复。我自己在鸿蒙适配和稳定性治理这半年里,感受最深的一点是:崩溃、卡顿、发热虽然都是用户口中的“体验差”,但它们背后的故障链路完全不同,用到的工具、日志、分析方法也完全不一样。必须先分诊,再定位,最后才谈修复。这套分诊、定位、沉淀的方法,就是我们要说的 DFX。
这篇文章是 DFX 系列的第一篇,主题就一句话:Flutter 鸿蒙应用崩了、卡了、发烫了,到底从哪里开始查。我把三件事分别怎么入手、怎么查干净、查到哪里算结束,按我实际工作的顺序完整讲一遍。适合正在做鸿蒙适配的 Flutter 开发者,也适合线上问题处理经验还不多、遇到反馈不知道第一步做什么的朋友。
1. 先把“崩、卡、烫”拆成三类故障模型再动手
1.1 三个症状对应三条完全不同的故障链路
“崩、卡、烫”听起来都是应用出了问题,但故障链路差异很大。崩溃是应用进程被终止,属于确定性事件,通常一定有日志、有堆栈、有退出码。卡顿是应用还活着,但动画掉帧、输入延迟,属于性能劣化,日志可能干干净净,要靠在运行中抓取性能数据才能复现。发烫则是功耗问题,用户感知的是设备温度,但应用层面往往看不到任何错误,需要把 CPU 占用、线程调度、后台任务、渲染频率这些信息交叉起来看。
我用一个日常类比解释:崩溃像家里的空气开关跳闸,一定有明确的触发条件,找到那个电器就能解决;卡顿像水管出水变慢,可能是一处堵塞,也可能是多个水龙头同时开;发烫像电费异常升高,一个月账单下来看不出来是哪个电器耗的电,必须逐项排查。三者的发生机制不同,排查路径自然也不同。
1.2 从DFX视角看:先收集证据,再谈改代码
DFX 这个词在鸿蒙语境里指的是诊断与故障定位能力,包括日志系统、崩溃记录、性能监控、事件打点等一整套机制。它的核心思想不是“出问题了再查”,而是提前把各类故障现场的证据留下来,让问题发生时有迹可循。我们在排查线上问题时,最痛苦的不是问题有多难,而是现场已经被破坏了。
举个例子。用户报告应用闪退,如果你没有接入任何日志上报,只能让用户重新操作一遍,还不一定能复现。但如果上线前就接入了崩溃日志收集、让每次崩溃的堆栈和系统日志自动归档,用户再反馈时你直接拉取记录就能定位。这就是 DFX 的价值。所以,排查的第一步不是打开代码找 bug,而是确认我们手里有哪些证据,还缺哪些证据,然后想办法把证据补齐。
1.3 优先级判断:崩大于卡,卡大于烫
三个症状同时出现或者先后出现时,处理优先级要明确。崩溃的影响半径最大,用户直接丢掉当前操作,必须最先处理。卡顿影响体验,但用户还能勉强用,属于第二优先级。发烫通常不会单独出现,往往是卡顿或者崩溃的前兆,比如某个任务一直在后台空转,先是卡顿,然后发热,最后可能被系统杀死。所以烫很多时候是果,不是因。
我一般会在接到反馈后先按“崩、卡、烫”给问题分类,分类的同时把现场信息收集时间排出来:崩溃要第一时间拿日志,卡顿要拿到性能数据,发烫要拿到功耗和 CPU 数据。证据拿得越早,后面越省事。下面几章我就按这个优先级分别展开。
2. 用hilog把现场日志拉起来:第一把钥匙
2.1 连接鸿蒙设备与基础日志查看命令
排查 Flutter 鸿蒙应用的问题,第一把钥匙必然是日志。鸿蒙设备日志的基础命令是 hilog,通过 hdc 工具链与设备通信。先确认设备被识别,执行hdc list targets,能看到设备序列号就说明连接正常。接着就能开抓日志了,最简单的做法是执行hdc shell hilog,它会实时把系统及各应用的日志往终端上刷。
这里有个新手容易犯的错:什么都不过滤就一把刷,几分钟下来终端里全是乱七八糟的系统消息,真正有用的内容早被淹没。正确做法是先想清楚要看哪个进程,然后用进程号或者应用包名做过滤。Flutter 应用在鸿蒙上运行时有 Dart 侧的日志输出,也有 Native 侧的日志输出,两类日志会统一汇聚到 hilog 里,所以我们既要知道怎么过滤,也要知道过滤后怎么识别哪些是 Flutter 引擎打的,哪些是鸿蒙系统打的。
2.2 按进程和关键字过滤Flutter日志
拿到设备的进程列表可以用hdc shell ps -ef | grep 包名找到目标 PID。找到 PID 之后,用hdc shell hilog -p PID就能只看这个进程的日志。如果你的应用是多进程架构,比如推送进程、上报进程、渲染进程分开,那就要把相关的几个 PID 都盯住。
只按 PID 过滤还不够,Flutter 日志有自己的标签体系。引擎层大部分日志会带 flutter 相关 tag,Dart 侧debugPrint的输出也会流向 hilog。我习惯再加一层关键字过滤,比如用hdc shell hilog -p PID -e "flutter|dart|crash|Exception"把关键字匹配的日志筛出来。这样抓到的日志内容量和噪声比会舒服很多。
实际抓崩溃日志时,我经常用另一个组合拳:先hdc shell hilog -c清空日志缓冲区,再让测试人员重新触发一次问题,最后用过滤命令把这段时间的日志拉出来。清空这一步很关键,它能保证你拿到的日志就是问题发生前后的完整记录,而不是夹杂着几小时前的历史残留。
2.3 崩溃日志与faultlog:东西方两套归档
除了实时 hilog,鸿蒙系统还维护了一套崩溃归档。应用发生 native crash 时,系统会在设备端记录 faultlog,通常存放在/data/log/faultlog/faultlogger/目录下。这类文件有崩溃进程名、崩溃时间、信号类型、寄存器、backtrace 等信息。通过hdc shell ls和hdc shell cat可以查看,但多数设备需要开发者权限,调试机上配合镜像解锁后基本都能读到。
Dart 侧的异常不会被记到 faultlog 里,它们是 Flutter 引擎层面处理的逻辑异常。所以你会遇到一种情况:hilog 里明确出现了未捕获异常,faultlog 目录却什么都没有。这不代表问题不存在,只是说明崩溃发生在 Dart 虚拟机内,没有上升到 native 层。判断一条崩溃到底该去 faultlog 里查,还是从 hilog 里翻,是 Flutter 鸿蒙排查特有的一个分叉点,后面第 3 章我会详细讲。
2.4 日志量太大时怎么去噪
线上测试阶段最常遇到的是日志量爆炸。Flutter 的debugPrint在 debug 包会往控制台打印大量内容,Release 包如果没注意保留print,同样会产生大量 I/O 开销。这时日志抓取会变得非常痛苦,因为有效信息被大量周期性的噪声淹没。
我的处理手段有三层:第一层,用时间窗口过滤,只看崩溃前后 30 秒内的日志;第二层,用 tag 过滤掉明显无关的系统服务日志;第三层,如果还不能定位,就把日志落盘后离线处理,用文本工具把关键字上下文的行拉出来看。长期方案是在代码里把日志分级,普通流程日志和错误日志分 tag,这样线上抓包时只抓 error 级别,效率会成倍提升。
3. “崩了”的排查:Dart异常与Native信号两条线并进
3.1 Dart侧的未捕获异常:先找Zone和FlutterError
Flutter 应用的崩溃,相当比例发生在 Dart 层。Dart 是单线程事件循环模型,一个未捕获的异常如果没人接管,会沿着 Zone 向上抛,最终导致 isolate 终止。表现在用户侧就是应用闪退或者页面白屏。
排查 Dart 层崩溃,第一件事是看日志里有没有Unhandled Exception字样,后面会跟着一段用#0、#1编号的 Dart 堆栈。这段堆栈足够定位到具体是哪个文件哪一行抛出的异常。但要注意,异步任务里的异常往往不会带上完整的调用链,你在堆栈里看到的可能只是一个.then回调或者一个Future内部的调用点,这时就要靠日志中前后的业务信息来判断是哪个入口进来的。
我建议在工程初始化阶段就接管全局异常。FlutterError.onError可以捕获框架层异常,PlatformDispatcher.instance.onError可以捕获平台消息异常,runZonedGuarded可以兜住业务代码里的未捕获异步异常。这三层接上之后,崩溃现场的堆栈才会被完整保留下来。没有这些兜底,很多线上崩溃你只能看到一个光秃秃的进程退出码,排查成本直接翻倍。
3.2 Native侧的signal与backtrace:关键信息怎么读
当崩溃发生在 Native 侧,比如 Flutter 引擎、三方插件、NAPI 桥接层,日志里会出现signal 11 (SIGSEGV)、signal 6 (SIGABRT)之类的关键词。SIGSEGV 多是指针访问非法内存,SIGABRT 多为主动中止,常见于 C++ 层检测到致命错误后调用 abort。
拿到 native backtrace 后,先不要被一大串地址吓住。Release 包里这些地址通常是对应到 so 文件的偏移量,需要用符号表或者映射服务翻译成函数名,这一步叫符号化。Flutter 引擎的崩溃,符号化后你能看到是哪个引擎模块出的问题;三方插件引起的崩溃,符号化后你会看到自己工程里引用的那个 .so 名字,顺着名字去查对应插件的版本和兼容性说明。
在鸿蒙侧有一个 Flutter 开发容易忽略的坑:部分 Flutter 插件在 Android 和 iOS 上很成熟,但鸿蒙适配是通过 OpenHarmony 社区的兼容层完成的,插件内部可能直接使用了不稳定的系统接口,或者动态库加载路径不完整,这类问题经常以 SIGSEGV 的形式出现。排查时不要默认“插件在别的平台没问题”,要看鸿蒙平台的实际日志。
3.3 高频崩溃类型与典型根因对照
下面这张表是我在 Flutter 鸿蒙项目里整理的高频崩溃类型对照,按出现频率排序。它不能覆盖所有情况,但能帮你把 80% 的崩溃归到正确的排查方向。
| 崩溃表现 | 日志/堆栈特征 | 优先排查方向 |
|---|---|---|
| Dart 未捕获空对象异常 | NoSuchMethodError、Null check operator used on a null value | 数据源返回空值,接口字段缺失 |
| 异步任务崩溃 | Unhandled Exception in async callback | 未做 catchError,Future 链断裂 |
| Native SIGSEGV | signal 11 (SIGSEGV) | 插件系统调用异常、引擎版本不匹配 |
| Native SIGABRT | signal 6 (SIGABRT) | C++ 层 fatal、so 动态库加载失败 |
| 内存快速增长后崩溃 | 伴随Out of Memory、lowmemorykiller | 图片未释放、流式数据未关闭 |
| 页面切换偶发闪退 | 无固定堆栈,多在路由跳转时出现 | 页面对象被提前释放,引擎复用冲突 |
这里面内存相关崩溃需要特别说明。Flutter 应用在鸿蒙上运行时,Dart 内存不受 Java 堆限制,但 native 层内存仍然会被系统进程检查。当应用频繁加载大图、生成纹理、解码视频帧时,native 内存会快速上涨,系统内存压力增大后可能触发 lowmemorykiller 把进程杀掉。这种崩溃在 faultlog 里不一定有 signal,反而能在 hilog 里看到系统内存告警的记录。
3.4 给线上崩溃加一道“兜底闸门”
崩溃治理不能只靠出了问题再查,还要在应用里加兜底。最基础的是崩溃后的状态恢复,比如在main()入口记录一个启动标记,应用正常进入主页后清除;下次冷启动时如果发现上一次启动标记没有被清除,说明上次发生了崩溃,此时自动清理可能导致崩溃的临时状态,再进入安全模式,比如关闭动画、降低图像质量。
第二个兜底是崩溃信息本地缓存。崩溃发生时网络可能不可用,或者上报链路自身有问题,所以崩溃日志要先写到本地文件,等下次启动网络恢复时再统一上报。上报内容要包含设备型号、鸿蒙版本号、Flutter SDK 版本、应用构建号、崩溃堆栈、崩溃时间这六要素,缺了任何一项,后续回溯都会费劲。
我在实际项目里见过太多次“崩溃只在用户手机上出现、只在某个版本出现、只在某个页面出现”,而开发者手头只有一条用户手打的描述。兜底闸门不是为了消灭崩溃,是为了保证崩溃真的发生时,我们能拿到足够的证据。
4. “卡了”的排查:帧预算与线程协作
4.1 用Toolkit确认两件事:帧率和瓶颈线程
卡顿排查与崩溃完全不同。崩溃是找“为什么停了”,卡顿是找“为什么慢了”。慢不是靠看日志看出来的,要靠性能数据算出来。
Flutter 的性能模型里有一个硬指标:每一帧的预算约 16.6 毫秒。超出这个预算就会掉帧,用户感知为卡。排查卡顿,第一件事是确认掉帧到底发生在哪个线程。Flutter 渲染主要涉及三条线程:UI 线程执行 Dart 代码和布局,Raster 线程执行图层合成和绘制,Platform 线程负责平台通道通信。瓶颈在哪条线程,卡顿的直接原因就在哪条线程。
我会先用 Flutter DevTools 的 Performance 页面打开 frame timeline,查看掉帧时每一帧的耗时分布。UI 线程耗时过高,说明 Dart 代码里有重的计算、布局或者构建;Raster 线程耗时过高,说明绘制指令、图层合成、纹理上传有问题;两条线程都不高但依旧卡,就要怀疑鸿蒙侧主线程被其他逻辑占用。
4.2 从Frame Timeline读“卡”的现场
Frame Timeline 是 Flutter DevTools 里最直观的卡顿现场。打开后你会看到一列帧柱状图,绿色表示达标,红色表示超时。点击某一帧可以看到它拆分成 build、layout、paint、raster 等阶段各自的耗时。
我一般先看掉帧是否成片出现。偶发单帧掉帧,通常是某个瞬时任务引起,比如一次网络回调触发了大面积 setState。成片掉帧,则是持续性的性能问题,比如列表构建没有复用、动画回调里做了重计算。还有一种特殊模式:周期性掉帧,每隔 N 帧掉一帧。这种往往是定时器或者固定的后台任务抢占资源,需要配合 CPU 分析才能确认。
读取 timeline 时要特别注意一个误区:不要只看耗时最高的那一帧,要看掉帧的上下文。连续三帧超过预算和每隔二十帧掉一帧,修复策略完全不同。
4.3 UI线程过载:最常见也是最好修的
UI 线程过载的根因通常是三类:不必要的重建、过重的构建逻辑、同步 I/O。不必要的重建最常见,比如页面顶层使用了没有优化的 ValueListenableBuilder,或者 setState 范围过大,导致整个页面子树重建。排查办法是在 DevTools 里开启 Widget rebuild 计数,看看哪些 Widget 的重建频率异常。
过重的构建逻辑我遇到过不少,典型场景是在 build 方法里做 JSON 解析、做大量字符串拼接、甚至做数据库查询。build 方法应该保持轻量,它的职责是“用已有数据快速生成 widget 树”,而不是“加工数据”。所有数据准备和计算都应该在 setState 之前完成,build 里只做读取。
同步 I/O 在 Flutter 里尤其要注意。Dart 的 File 读取如果不用异步接口,会把当前 isolate 阻塞住。虽然 Flutter 新版本里File.readAsBytesSync这类接口会跳转到后台线程执行,但业务代码里直接处理大数据时仍然可能拖慢 UI。UI 线程过载的修复其实不复杂,复杂的是找出“哪里在做多余的事”。
4.4 Raster线程和合成路径:容易被忽略的第二现场
UI 线程正常但界面仍然卡,这一半的锅在 Raster 线程。Raster 线程负责把 GPU 指令转换成画面,它的耗时常见于图片解码、纹理上传、路径绘制、遮罩裁剪、模糊效果。
我印象很深的一次排查:页面里有一个高度复杂的 SVG 图标,每次进出页面都重新解码和栅格化,Raster 线程单帧耗时超过 40 毫秒。修复方式简单到难以置信,把这张 SVG 转成 PNG 图标或者用ui.Picture做缓存,帧耗时直接回到 8 毫秒以内。另一个高频问题是图片没有做缓存策略适配,列表快速滑动时每张新图都会触发一次解压和纹理上传,Raster 线程被塞得满满当当。
鸿蒙合成路径上还有一个特殊点:Flutter 渲染后的画面要通过鸿蒙的合成框架显示到屏幕上,如果页面中存在大量透明的浮层、圆角裁剪、高斯模糊,会加重合成负担。排查时可以尝试把所有视觉特效临时关掉,如果卡顿消失,说明问题在渲染特效链路上,再做逐项精确定位。
4.5 鸿蒙侧主线程被占用:Flutter进程不卡的卡
有一种卡顿现象很隐蔽:Flutter 内部两帧之间的时间间隔正常,DevTools 里一片绿,但用户就是觉得页面反应迟钝。这种情况大概率不是 Flutter 渲染进程的问题,而是鸿蒙侧主线程被其他任务占满,Flutter 的平台消息没有得到及时处理。
Flutter 跑在鸿蒙上,底层通过平台通道与鸿蒙侧通信,比如读取系统状态、调用振动、打开相机、发起网络请求。每当 Dart 侧发起平台调用,消息要经过鸿蒙主线程的 Looper 队列。如果鸿蒙侧有耗时操作阻塞了主线程,平台消息就会排队,表现是 Dart 侧的 Future 迟迟不回调,页面像被“卡住”了一样。
排查时把 hilog 打开,看有没有明显的 UI 线程警告,或者用 DevEco Studio 的 Profiler 看鸿蒙主线程的 CPU 占用。早期版本里,部分鸿蒙 Flutter 适配层的处理逻辑还比较粗糙,主线程上做了不少重活,这类问题会随着版本的迭代逐步改善,但如果我们自己在鸿蒙侧写的原生插件里有耗时调用,这个坑会一直存在。
5. “发烫了”的排查:功耗热点找源头
5.1 发热问题的第一屏信息:CPU、温度、功耗曲线
发热问题最怕上来就猜“是不是某个动画导致的”。应该先看第一屏信息:CPU 占用率、电池温度、功耗曲线。DevEco Studio 的 Profiler 提供功耗和 CPU 模块,可以记录应用运行时的 CPU 各核占用率和温度曲线。打开后先观察一个关键现象:应用进入后台之后,CPU 占用是否显著下降。
正常 Flutter 应用在后台应该处于低功耗状态,CPU 占用几乎为零。如果你发现应用切到后台后 CPU 仍然居高不下,说明有任务在后台运行。顺着 CPU 热点记录,你可以看到是哪个线程在占 CPU、它在执行什么调用栈。发烫的第一屏信息就是“谁在什么时候吃了多少 CPU”,这一步做完,问题范围基本能缩小到具体模块。
5.2 隐性耗电大户:Timer、后台任务与长连接
Flutter 开发里太容易写出隐性耗电逻辑了。最典型的是Timer.periodic创建了周期性任务却没有在页面销毁时取消。页面虽然关了,定时器还在转,每秒钟唤醒一次,CPU 无法进入低功耗状态,设备自然发热。
第二个隐蔽来源是轮询请求。不少应用为了实时性,会起一个循环不停地刷新接口,有的甚至不做前后台判断,切到后台还在轮询。鸿蒙设备对应用后台运行有限制,但 Flutter 计时器驱动的 Dart 侧任务有时能绕过系统管控,表现得异常旺盛。
第三个是长连接保活。WebSocket、MQTT 这类长连接如果在弱网环境下频繁重连,每次重连都会触发 DNS 解析、TLS 握手、断线重连,耗电比正常通信高出几十倍。排查时如果发现某个版本的发热问题集中在弱网场景,优先怀疑长连接的重连策略。这类问题的共同点是不容易产生崩溃,但会持续消耗 CPU,发热曲线是一条稳定上升的弧线。
5.3 Widget重建和图片解码:看起来正常的热
还有一类发热问题,CPU 占用不算特别高,但温度就是下不来。这通常是显卡单元在持续工作,表现在 Flutter 里是高频的界面刷新。比如一个不断改变透明度的动画在循环播放、一个一直在旋转的 loading 图标、一个每帧都重新布局的动态列表。这类任务让 GPU 在后台保持高频率渲染,功耗自然高。
图片解码也经常被忽略。Flutter 解码一张大图时,如果反复触发ui.Image的创建和释放,纹理上传和回收会成为性能黑洞。特别是某个页面反复进出时,相同的图片资源被反复解码上传,GPU 和 CPU 都在做无用功。处理方式很直接:Image cache 策略要设置合理,能直接用Image.network的 cacheWidth 参数缩小解码尺寸的就别解码原图。
5.4 把“发烫”和“卡顿”放在一起查
发烫问题里,相当高比例实际上是卡顿问题的延续。举一个真实场景:某个列表构建逻辑很重,UI 线程每帧耗时 30 毫秒,用户滑动时帧率只有 30 帧左右。用户感受是既卡又烫,因为 UI 线程在满负荷运转,CPU 大核被持续唤起,功耗曲线平滑上升。
所以排查发烫时,如果找不到明确的 CPU 热点,建议回到第 4 章的卡顿排查流程走一遍。很多时候,把帧耗时降下来,发热问题会迎刃而解。反过来也一样,一个明显由后台 Timer 引起的发热问题,如果不清理 Timer,即使优化了构建逻辑,温度也降不下来。卡和烫是一枚硬币的两面,共同指向资源被无效占用。
6. 从“救火”到“预防”:沉淀一套Flutter鸿蒙DFX清单
6.1 应用内接入全局错误上报
线上问题靠用户反馈永远是被动的,主动上报才是 DFX 的常态。Flutter 应用在鸿蒙上要做的第一件事,是在main()入口注册好全局错误处理。我建议把所有异常信息统一格式化为结构化 JSON,包含时间戳、异常类型、堆栈、路由信息、设备状态,然后写入本地文件并异步上报。
上报的时机要讲究。首崩时可能网络环境差,本地缓存后启动重试是最稳妥的。上报通道可以用自己后端的接口,也可以接入现有的监控平台。关键是数据格式要稳定,别频繁改动字段,否则后面做聚合分析的时候会非常痛苦。我见过太多团队因为日志格式改来改去,导致两个版本之间的崩溃数据完全无法对比。
6.2 结构化日志与崩溃文件归档
除了崩溃堆栈,业务日志也要结构化。我在项目里维护了一个简单的日志工具,按模块打 tag,并区分 debug、info、warn、error 几个级别。debug 和 info 级别日志只在 debug 包输出,Release 包只保留 warn 和 error。这样既保证了排障材料充足,又不会因为日志 I/O 拖累性能。
崩溃文件归档建议按日期和设备维度组织。每次崩溃生成一个独立文件,文件命名包含应用版本号和时间戳。线上出问题时,按版本号过滤就能快速圈定影响范围。这个归档目录要定期清理,比如只保留最近 30 天,否则存储占用会失控。
6.3 我在项目里实际用的DFX排查清单
我整理了工作中最常用的一套排查顺序,把它做成清单贴在团队文档里。每次处理“崩、卡、烫”问题时按这个顺序执行,能避免在错误方向上浪费时间。
| 排查阶段 | 关键动作 | 目标产出 |
|---|---|---|
| 信息收集 | 确认版本号、设备型号、系统版本、复现路径 | 明确问题边界 |
| 崩溃类问题 | 拉取 hilog、faultlog,确认是 Dart 层还是 Native 层 | 拿到崩溃堆栈 |
| 卡顿类问题 | 打开Frame Timeline、CPU Profiler,确认瓶颈线程 | 定位到具体阶段 |
| 发热类问题 | 查看 CPU 热点、温度曲线、后台任务 | 找出占用源 |
| 修复验证 | 修复后对比修复前后的帧耗时、温度、崩溃率 | 确认问题真被解决 |
| 归档沉淀 | 更新排查清单,记录本次问题特征与修复方式 | 提高下次排查效率 |
这张清单的核心价值不是“每一步都做”,而是“每一步都有产出物”。排查过程中产出物越清晰,问题就越接近定位。
6.4 灰度阶段的日志收割技巧
新版本提测和灰度阶段,是排查崩卡烫问题的黄金窗口。这个阶段用户量小,问题集中,复现率高。我之前吃过亏:真机测试环境没有同步开启日志输出和崩溃归档,导致灰度版本用户反馈问题后,手里没有可用日志,只能从线上几千个用户里捞。后来我学聪明了,灰度发布前强制打开日志输出开关,同时把崩溃上报服务配置到位。
灰度收割还有一个技巧:给灰度版本单独打一个 build 号,这样线上归档的崩溃文件和日志都能按 build 号快速筛出来。如果灰度发现问题,第一时间让测试人员用相同环境复现一次,并用hdc shell hilog -c清空日志后重新操作,保证抓到的现场是完全新鲜的。
还有一个容易被忽视的点:灰度版本的崩溃率和卡顿数据,要每天都看一遍,而不是等产品经理反馈。很多稳定性问题在灰度第一天就有苗头,越早发现越容易定位。等到全量发布后再发现,影响面已经不可控了。
说实话,我在鸿蒙适配初期也是从“瞎猜”开始走过来的,走了不少弯路。有一次线上崩溃查了一整天才发现是某个第三方 JSON 解析库在鸿蒙上的兼容问题,而问题其实在上报的崩溃日志里已经写得很清楚了,只是我没耐心去读那一段 native 堆栈。后来我强迫自己遵循一个原则:没有拿到现场证据之前,绝不碰代码。这个原则帮我省下了非常多的时间。
如果你现在正被线上用户反馈的“崩、卡、烫”折磨,不妨照着上面这套流程走一遍。先从 hilog 和崩溃归档拿现场,再用 Profiler 确认瓶颈,最后把所有证据归档沉淀。排查的终点从来不应该是“这次改好了”,而应该是“下一次同类问题能更快定位”。我后续也会在 DFX 系列里继续展开每一个环节的具体工具操作和案例复盘,欢迎一起交流实战中踩到的坑。