这是DFX系列的第三篇:崩溃问题专题。让我们直接进入主题。Flutter 鸿蒙应用的崩溃问题主要来自三个不同的层面:引擎底层、Dart代码层和ArkTS 层。三个层面的崩溃问题所涉及的关键词不同、严重程度不同、修法也是不同的。

一、先分清是哪种崩溃问题

三种崩溃类型:

崩溃类型发生层日志关键词常见原因
Native 崩溃引擎底层(C++)Caught signal SIG*空指针、GPU 驱动崩溃、内存被破坏
Dart 异常Dart 代码Unhandled exception类型错误、数组越界、空指针
ETS 异常ArkTS 代码Failed to handleMethodChannel 回调中出错

Tip:

  1. Dart 异常是最常见的,通常是代码里的问题

  2. Native 崩溃问题就比较严重了,可能是引擎问题 或 GPU 驱动问题

  3. 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 或 assertSIGABRT引擎内部的断言检查失败
画面花屏、绿屏后闪退SIGSEGVGPU 内存被破坏
退后台再回前台后闪退SIGSEGVGPU 上下文丢失后仍渲染
闪退堆栈里有 GPU / RasterizerSIGSEGV / SIGABRTGPU 驱动崩溃
闪退堆栈里有 Texture / external_textureSIGSEGV纹理被释放后还在访问

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 failedDCheck 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栈溢出,递归没有终止条件加递归终止条件
LateInitializationErrorlate 变量没初始化就用了确保使用前已赋值

第 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:

参数类型限制说明
frameworkNameString固定 “FLUTTER”
errorMsgString≤4096 字符异常错误信息
stackTraceString≤8192 字符异常堆栈
pidInt64进程 ID
timeStampInt64事件时间,UTC 毫秒

FLUTTER_ETS_EXCEPTION:

参数类型说明
EVENT_NAMEString固定 “FLUTTER_ETS_EXCEPTION”
CONTEXTString异常发生环节,见第四节 CONTEXT 表
ERROR_MSGString异常错误信息
STACK_TRACEString异常堆栈

下一篇展开卡顿问题专项:持续期待下~

小伙伴们记得点赞+关注

CPF-Flutter社区官网:

https://atomgit.com/CPF-Flutter

“AI再牛,技术不能丢”

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐