【DFX系列】Flutter 鸿蒙崩溃问题定位指南
这是DFX系列的第三篇:崩溃问题专题。让我们直接进入主题。Flutter 鸿蒙应用的崩溃问题主要来自三个不同的层面:引擎底层、Dart代码层和ArkTS 层。三个层面的崩溃问题所涉及的关键词不同、严重程度不同、修法也是不同的。
一、先分清是哪种崩溃问题
三种崩溃类型:
| 崩溃类型 | 发生层 | 日志关键词 | 常见原因 |
|---|---|---|---|
| Native 崩溃 | 引擎底层(C++) | Caught signal SIG* | 空指针、GPU 驱动崩溃、内存被破坏 |
| Dart 异常 | Dart 代码 | Unhandled exception | 类型错误、数组越界、空指针 |
| ETS 异常 | ArkTS 代码 | Failed to handle | MethodChannel 回调中出错 |
Tip:
Dart 异常是最常见的,通常是代码里的问题
Native 崩溃问题就比较严重了,可能是引擎问题 或 GPU 驱动问题
ETS 异常通常会导致功能失效
判断方法,直接在日志里依次搜索关键字:
# 第 1 步:搜到了 → 引擎底层崩溃,看第二节
hdc shell hilog | grep "Caught signal"
# 第 2 步:搜到了 → Dart 代码有 bug,看第三节
hdc shell hilog | grep "Unhandled exception"
# 第 3 步:搜到了 → ArkTS 插件有 bug,看第四节;都没搜到,回去抓全量日志重新排查
hdc shell hilog | grep "FLUTTER_ETS_EXCEPTION"
还不会抓日志的话,先看上一篇的四步流程
二、Native 崩溃:引擎层
2.1 这种崩溃问题是什么
Native 崩溃是 Flutter 引擎的 C++ 代码发生了严重错误,可以理解为「引擎自己崩了」。它比 Dart 异常严重得多(引擎爆了,车就瘫了):引擎一旦崩溃,整个应用直接闪退,没有机会做任何清理。引擎在启动时会注册信号处理器,作用类似烟雾报警器,崩溃发生时,报警器把崩溃信息记录下来输出到日志。
2.2 崩溃信号类型
下面的异常类型清单,根据信号关键字查看对应初步原因:
| 信号 | 名称 | 通俗解释 | 常见原因 |
|---|---|---|---|
| SIGSEGV | 段错误 | 访问了不该访问的内存 | 空指针、对象已释放但还在用 |
| SIGABRT | 异常终止 | 引擎自己调用 abort() 退出 | 断言失败、内存被破坏 |
| SIGFPE | 浮点异常 | 数学运算出错 | 除以零 |
| SIGBUS | 总线错误 | 内存对齐有问题 | DMA buffer 被提前释放 |
| SIGTERM | 软件终止 | 系统让进程退出 | 系统资源限制 |
| SIGPIPE | 管道断裂 | 往已关闭的管道写数据 | IPC 通信通道断了 |
2.3 从现象判断信号类型
| 看到的现象 | 可能的信号 | 说明 |
|---|---|---|
| 无征兆突然闪退,没有任何 Dart 报错 | SIGSEGV | 最常见,引擎访问了非法内存 |
| 闪退前日志有 CHECK failed 或 assert | SIGABRT | 引擎内部的断言检查失败 |
| 画面花屏、绿屏后闪退 | SIGSEGV | GPU 内存被破坏 |
| 退后台再回前台后闪退 | SIGSEGV | GPU 上下文丢失后仍渲染 |
| 闪退堆栈里有 GPU / Rasterizer | SIGSEGV / SIGABRT | GPU 驱动崩溃 |
| 闪退堆栈里有 Texture / external_texture | SIGSEGV | 纹理被释放后还在访问 |
2.4 日志长什么样
在日志中搜索 Caught signal,典型输出:
E Flutter: Caught signal SIGSEGV during program execution.
E Flutter: Frame 0: 0x7f8b2c3d4e50 FlutterRenderer::DrawFrame
E Flutter: Frame 1: 0x7f8b2c3d4f60 Shell::OnDrawFrame
E Flutter: Frame 2: 0x7f8b2c3d5070 fml::MessageLoopImpl::Run
读法:第一行 Caught signal SIGSEGV 说明是段错误,空指针类问题;后面的 Frame 行是崩溃时的调用栈,Frame 0 是崩溃发生的最直接位置,Frame 1、Frame 2 是调用链的上层。函数名对应引擎源码,能据此判断是哪块功能出了问题。
2.5 排查问题,可以分为四步
第 1 步,拿到完整堆栈。-A 30 表示搜到关键词后再往后看 30 行:
hdc shell hilog | grep -A 30 "Caught signal"
第 2 步,按信号类型定排查方向:
| 信号 | 排查方向 | 具体要找什么 |
|---|---|---|
| SIGSEGV | 空指针 / 悬垂指针 | 搜 GpuReclaim(GPU 回收)、UnRegisterExternalTexture(纹理注销) |
| SIGABRT | 断言失败 / 堆破坏 | 搜 CHECK failed、DCheck failed |
| SIGFPE | 除零运算 | 检查尺寸计算、帧率计算中是否有 0 值 |
| SIGBUS | 内存映射失效 | 检查 DMA buffer 是否被提前释放 |
第 3 步,按堆栈函数名定位问题模块。函数名直接对应引擎源码的文件名;显示 Unknown 的,用 addr2line 把地址翻译成源码行号,重点看 Frame 0:
addr2line -e libflutter.so -f -C 0x7f8b2c3d4e50
第 4 步,看崩溃之前的日志找原因,就像查案要看案发前的监控录像一样。崩溃前 5-10 秒的日志通常包含触发原因,-B 50 表示往前看 50 行:
hdc shell hilog | grep -B 50 "Caught signal"
2.6 四个场景
场景 1:GPU 驱动崩溃。堆栈里有 GPUSurface、Rasterizer、SkSurface 这些词时对号入座:
| 原因 | 怎么排查 | 怎么修 |
|---|---|---|
| GPU 回收后没恢复就渲染 | 搜 GpuReclaim,看是否有 Surface REBUILT | 确保 OnGrContextCreated 完成后再渲染(见 系列内存篇 第 3 节) |
| Surface 重建失败 | 搜 SetDisplayWindow failed | 检查 native_window 是否还有效 |
| Vulkan 驱动 bug | 检查是否用 Vulkan 后端 | 尝试切换到 OpenGL ES 后端 |
| 着色器太复杂 | 检查是否有复杂的自定义 Shader | 简化 Fragment Shader |
场景 2:纹理释放后还在访问。堆栈里有 Texture、external_texture、OHOSExternalTexture。类比:快递箱已经扔了,还有人去箱子里拿东西。常见三种:注销纹理后原生侧还在写入(搜 UnRegisterExternalTexture,修复是先停生产者再注销纹理,顺序不能反);外部 NativeImage 被提前释放(搜 SetExternalNativeImage,确保外部资源生命周期比纹理长);GPU 上下文销毁后还引用 GPU 资源(搜 OnGrContextDestroyed,确认 OnTextureUnregistered 释放了资源)。正确的注销顺序:
// 第 1 步:先停止原生侧的生产,比如暂停视频播放
stopProducer();
// 第 2 步:再注销纹理
textureRegistry.unregisterTexture(textureId);
场景 3:引擎重复释放。堆栈里有 ~Shell、Destroy,信号是 SIGABRT。类比:退房时钥匙还了,又还了一次。两种触发:多次调用 engine.destroy()(搜 FLUTTER_ENGINE_DESTROY 出现次数,加 null 检查确保只销毁一次);分栏模式误触发 onDestroy(检查 FlutterPage.onDestroy 调用时机)。正确的写法:
onAbilityDestroy() {
this.flutterEngine?.destroy();
this.flutterEngine = null; // 防止重复销毁
}
场景 4:Skia 渲染异常。堆栈里有 SkCanvas、SkSurface、GrDirectContext。三种对应:CustomPainter 访问了已释放资源(检查是否持有外部引用,确保不持有会被释放的外部资源);GPU 上下文丢失后继续渲染(搜 GpuReclaim,在 OnGrContextDestroyed 后暂停渲染);Picture 跨 isolate 传递(用 toImage 转换后再传递)。
三、Dart 异常:代码层
3.1 怎么理解
最常见的崩溃原因:Dart 代码里有未被 try-catch 捕获的异常,比如调用了 null.toString(),或者访问 list[100] 但列表只有 10 个元素。异常未捕获时,isolate 退出,应用闪退,引擎自动捕获并记录到日志。这也是最容易修的崩溃类型,日志里直接标注了出错的文件和行号。
3.2 日志长什么样
HiLog 输出:
E Flutter: Unhandled exception:
E Flutter: type 'Null' is not a subtype of type 'String'
E Flutter: #0 main (package:myapp/main.dart:12:5)
E Flutter: #1 _runMain (package:myapp/main.dart:8:3)
如何理解:
第一行说明是未捕获的 Dart 异常
第二行是错误原因:试图把 null 当成 String 用
第三行开始是堆栈,格式为「#帧号 函数名 (文件:行号:列号)」,#0 是最直接的出错位置,package:myapp/main.dart:12:5 指 main.dart 第 12 行第 5 列。直接打开日志标注的文件和行号,通常一眼就能看出问题。
3.3 排查四步走
第 1 步,拿完整堆栈:
hdc shell hilog | grep -A 30 "Unhandled exception"
第 2 步,
打开日志标注的代码文件,定位到行。
从 #0 开始看,找 package:应用名/ 开头的行,那是自己的代码。
dart: 和 package:flutter/ 开头的是框架代码,通常不是问题源。
第 3 步,按异常类型判断原因:
| 异常类型 | 通俗解释 | 怎么修 |
|---|---|---|
| NoSuchMethodError | 在 null 上调用了方法 | 加空判断:obj?.toString() |
| TypeError | 类型不匹配 | 安全转换:value?.toString() ?? ‘’ |
| RangeError | 索引越界 | 加边界检查 |
| StateError | 对象状态不对 | 检查对象状态 |
| FormatException | 格式不对 | 检查输入格式 |
| Stack Overflow | 栈溢出,递归没有终止条件 | 加递归终止条件 |
| LateInitializationError | late 变量没初始化就用了 | 确保使用前已赋值 |
第 4 步,用 DevTools 直观调试:flutter run --ohos 启动,浏览器打开 DevTools,开启「Stop on uncaught exceptions」,复现问题,调试器会在出错的地方暂停,所有变量的值都能看到。
3.4 五个高频问题与修复
问题 1:空指针,最常见。user 可能为 null 时直接链式调用:
// user 是 null 时,这里就崩了String name = user.name.toUpperCase();
用 ?. 安全调用加默认值:
String name = user?.name?.toUpperCase() ?? 'unknown';
问题 2:异步异常没捕获。Future 没有 await 时,try-catch 拦不住里面的异常:
// 没有 await,这个异常不会被外层 try-catch 捕获
Future.delayed(Duration(seconds: 1), () {
throw Exception('异步出错');
});
三种修法,按优先级排:局部用 await + try-catch,或 catchError;全局兜底用 runZonedGuarded,所有未捕获的异常都会进回调:
// 推荐:全局兜底,在 main 中用 runZonedGuarded
runZonedGuarded(() {
runApp(MyApp());
}, (error, stackTrace) {
print('全局捕获: $error\n$stackTrace');
});
问题 3:类型转换出错。JSON 字段为 null 或类型不符时,as 强转会崩:
// name 是 null 崩;price 是 100(int)也崩
String name = json['name'] as String;
double price = json['price'] as double;
// 安全的字符串转换、int 和 double 都能处理的数字转换
String name = json['name']?.toString() ?? '';
double price = (json['price'] as num)?.toDouble() ?? 0.0;
问题 4:数组越界。两种修法,边界检查或 elementAtOrNull(Dart 3.0+,越界返回 null 而不是崩溃):
// 方法 1:边界检查
if (index >= 0 && index < items.length) {
var item = items[index];
}
// 方法 2:elementAtOrNull(Dart 3.0+)
var item = items.elementAtOrNull(index);
问题 5:MethodChannel 回调里出错。handler 里抛异常会闪退,包一层 try-catch 返回默认值
_methodChannel.setMethodCallHandler((call) async {
try {
if (call.method == 'getData') {
return await _loadData();
}
} catch (e, s) {
print('MethodChannel 出错: $e\n$s');
return null;
// 返回默认值,不要让异常跑出去
}
});
3.5 边界问题
三个限制提前知道:错误信息最长 4096 字符、堆栈最长 8192 字符,超出会被截断,发现信息不完整,去 HiLog 里搜完整版;系统 API 低于 18 时不生成 HiAppEvent 事件,但 HiLog 仍然有。
四、ETS 异常:插件层
4.1 是什么
ETS 是 ArkTS 的简称,鸿蒙的编程语言。ArkTS 插件代码在处理 MethodChannel 消息时出错,就会产生 ETS 异常。
三个特点:通常不会导致闪退(被 try-catch 捕获了);但会导致某个功能用不了,典型表现是 MethodChannel 调用无响应;用第三方插件时,这个问题可能是插件代码的 bug。
4.2 日志长什么样
搜 Failed to handle 或 FLUTTER_ETS_EXCEPTION,典型输出:
E Flutter: MethodChannel# Failed to handle method call
E Flutter: TypeError: Cannot read property 'length' of undefined
E Flutter: at MethodChannel.onMessage (MethodChannel.ets:227:15)
怎么理解:
第一行说明 MethodChannel 消息处理失败;
第二行是原因:读取了 undefined 的 length 属性;
第三行是出错位置,MethodChannel.ets 第 227 行。
4.3 排查分为三步
第 1 步,搜 ETS 异常日志:
hdc shell hilog | grep -E "FLUTTER_ETS_EXCEPTION|Failed to handle|Uncaught exception in"
第 2 步,按事件里的 CONTEXT 字段确定异常发生在哪个环节:
| CONTEXT 值 | 排查方向 |
|---|---|
| MethodChannel.onMessage | 插件接收消息时出错,检查插件的 onMethodCall 方法 |
| MethodChannel.reply | 处理返回结果时出错,检查 Dart 侧的回调代码 |
| DartMessenger.invokeHandler | 二进制消息处理出错,检查 BinaryMessageHandler |
| DartMessenger.handlePlatformMessageResponse | 消息回复处理出错,检查 BinaryReply 回调 |
第 3 步,打开出错的 ETS 文件定位行号。STACK_TRACE 格式是「at 函数名 (文件:行号:列号)」,重点关注 undefined / null 访问。
4.4 两个高频问题与修复
问题 1:MethodChannel 参数为 undefined。argument() 返回 undefined 时直接取属性:
// name 是 undefined 时,undefined.length 崩
onMethodCall(call: MethodCall, result: MethodResult): void {
const name: string = call.argument("name");
const length = name.length;
}
问题 2:消息编解码不匹配。Dart 侧和 ETS 侧用了不同的 Codec,参数解析失败。修法是确保两侧一致:默认都用 StandardMethodCodec;自定义 Codec 时两侧相同。类型对照:Dart int 对 ETS number,String 对 string,bool 对 boolean,List 对 Array,Map 对 object。
4.5 特点
ETS 异常被框架捕获后不会闪退:MethodChannel.onMessage 捕获后自动返回 error 给 Dart 侧,DartMessenger.invokeHandler 捕获后发送空回复。代价是功能受影响,消息处理失败了。
五、崩溃排查指南
遇到崩溃,,按顺序走:
第一步,确认崩溃类型: 抓全量日志 hdc shell hilog > flutter_log.txt;搜 Caught signal,有就是 Native 崩溃;搜 Unhandled exception,有就是 Dart 异常;搜 FLUTTER_ETS_EXCEPTION / Failed to handle,有就是 ETS 异常。
Native 崩溃场景: 记录信号类型;记录堆栈中的函数名和地址,Unknown 的用 addr2line 解析;搜崩溃前 50 行日志找导火索;搜 GpuReclaim 确认是否和 GPU 有关;搜 UnRegisterExternalTexture 确认是否和纹理有关。
Dart 异常场景: 读完整堆栈,找 package:应用名/ 的行;打开对应文件和行号;确认异常类型;检查是不是异步异常(Future 没 await、没 catchError);用 DevTools 的「Stop on uncaught exceptions」调试。
ETS 异常场景: 按 CONTEXT 确定环节,打开 ETS 文件和行号,检查参数是否为 undefined / null,检查参数类型是否匹配;通用:记录崩溃前的操作场景(点了什么按钮、进了什么页面),确认是否能稳定复现,能复现的 bug 最好修。
六、HiAppEvent 事件参数
两类崩溃事件的字段,写脚本解析日志时用得上。
FLUTTER_DART_EXCEPTION:
| 参数 | 类型 | 限制 | 说明 |
|---|---|---|---|
| frameworkName | String | — | 固定 “FLUTTER” |
| errorMsg | String | ≤4096 字符 | 异常错误信息 |
| stackTrace | String | ≤8192 字符 | 异常堆栈 |
| pid | Int64 | — | 进程 ID |
| timeStamp | Int64 | — | 事件时间,UTC 毫秒 |
FLUTTER_ETS_EXCEPTION:
| 参数 | 类型 | 说明 |
|---|---|---|
| EVENT_NAME | String | 固定 “FLUTTER_ETS_EXCEPTION” |
| CONTEXT | String | 异常发生环节,见第四节 CONTEXT 表 |
| ERROR_MSG | String | 异常错误信息 |
| STACK_TRACE | String | 异常堆栈 |
下一篇展开卡顿问题专项:持续期待下~
小伙伴们记得点赞+关注
CPF-Flutter社区官网:
https://atomgit.com/CPF-Flutter
“AI再牛,技术不能丢”
更多推荐


所有评论(0)