Flutter Catcher 在鸿蒙端的错误捕获与上报适配指南
引言
鸿蒙(OpenHarmony)生态的发展越来越快,它的全场景分布式能力吸引了不少开发团队。如果你已经在用 Flutter 开发,现在想将应用迁移或扩展到鸿蒙平台,那么确保应用稳定运行是关键一环。而建立可靠的错误监控,则是稳中求进的基础。
在 Flutter 生态中,flutter_catcher 是一个功能比较全面的错误捕获与上报插件,不过它原本主要面向 Android 和 iOS。如果我们想在鸿蒙上也用它,就需要做一些适配。这篇文章就来详细讲讲如何把 flutter_catcher 搬到 OpenHarmony 上,里面会包括适配原理、完整代码示例,以及一些性能调优的经验,希望能帮你少踩坑。
一、适配背后的技术原理
1.1 Flutter 如何与鸿蒙通信
Flutter 和原生平台(包括鸿蒙)之间是靠平台通道 来通信的。flutter_catcher 的核心任务之一,就是把 Dart 层的错误信息传到原生侧,方便我们存到本地或者上报到服务器。这个传递过程一般通过 MethodChannel 来实现。
- Dart 侧 :调用
MethodChannel.invokeMethod,把错误信息传过去。 - 原生侧 (Android/iOS):需要实现
MethodCallHandler来处理这个调用。 - 鸿蒙侧 :我们也得提供一个功能对等的
MethodCallHandler------ 这其实就是本次适配最核心的一步。
1.2 flutter_catcher 的工作流程
简单来说,它的工作可以分为三步:
- 错误捕获 :通过自定义的
FlutterError.onError和runZonedGuarded来抓取 Dart 层和 Flutter 框架抛出的错误。 - 错误处理 :错误被送到事先配置好的
CatcherOptions里,这里定义了多种处理方式(比如弹窗提示、静默上报等)。 - 平台交互:当需要用到原生能力时(比如写文件、发网络请求),就会通过平台通道去调用原生代码。
1.3 鸿蒙适配要注意什么
鸿蒙用的开发语言是 ArkTS/JS,它的 API 和 Android 的 Java/Kotlin 差别挺大。所以适配时主要得解决下面几个问题:
- 在鸿蒙侧实现一个和 Android/iOS 功能相同的通道处理器。
- 处理好鸿蒙和 Flutter 在文件路径、网络权限、后台机制等方面的差异。
- 尽量保持插件接口一致,让 Dart 侧的代码不用改,或者只做最小改动。
二、一步步实现适配
2.1 准备开发环境
首先,确保你的 Flutter 环境已经支持 OpenHarmony。
bash
# 1. 获取支持 OHOS 的 Flutter SDK
git clone https://gitee.com/openharmony-sig/flutter_flutter.git
cd flutter_flutter
git checkout master
# 2. 配置环境变量(加到 ~/.bashrc 或 ~/.zshrc 里)
export FLUTTER_ROOT=/your_path/flutter_flutter
export PATH="$FLUTTER_ROOT/bin:$PATH"
# 3. 启用 OpenHarmony 支持
flutter config --enable-ohos
flutter precache --ohos
# 4. 检查一下环境是否正常
flutter doctor
# 输出应该能看到 'OHOS toolchain' 相关的可用条目。
2.2 编写鸿蒙侧的插件代码
假设你的插件叫 flutter_catcher_ohos,那我们需要在鸿蒙侧新建一个对应的模块。
第一步:在 Flutter 插件项目里创建鸿蒙模块
进入你的 flutter_catcher 插件目录(或者新建一个插件),然后执行:
bash
# 在插件根目录下,创建鸿蒙工程结构
flutter create --template=plugin --platforms=ohos .
# 这会生成一个 `ohos` 目录,里面就是鸿蒙侧的项目骨架。
第二步:实现鸿蒙侧的通道处理器(ArkTS)
打开 ohos/src/main/ets/com/example/flutter_catcher_ohos/CatcherPlugin.ets,编写如下代码:
typescript
// CatcherPlugin.ets
import plugin from '@ohos.plugin';
import { FlutterPlugin, MethodCall, MethodResult } from '@ohos/flutter';
import fs from '@ohos.file.fs';
import http from '@ohos.net.http';
import Logger from './Logger'; // 自己封装的日志工具
// 这些通道名和方法名需要和 Dart 侧约定好,保持一致
const CHANNEL_NAME = 'xclud.io/catcher';
const METHOD_WRITE_ERROR_LOG = 'writeErrorLog';
const METHOD_SEND_ERROR_REPORT = 'sendErrorReport';
@FlutterPlugin(CHANNEL_NAME)
export default class CatcherPlugin {
private context: plugin.Context;
constructor(context: plugin.Context) {
this.context = context;
}
// 所有从 Flutter 发过来的调用,都会先到这里
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
Logger.info(`收到Flutter调用: ${call.method}`);
switch (call.method) {
case METHOD_WRITE_ERROR_LOG:
this.writeErrorLog(call.arguments, result);
break;
case METHOD_SEND_ERROR_REPORT:
this.sendErrorReport(call.arguments, result);
break;
default:
result.notImplemented();
}
} catch (error) {
Logger.error(`处理调用失败: ${JSON.stringify(error)}`);
result.error('UNKNOWN_ERROR', error.message, null);
}
}
// 将错误日志写入鸿蒙的应用沙箱目录
private async writeErrorLog(args: any, result: MethodResult): Promise<void> {
try {
const { fileName, content } = args; // Dart 侧传过来的参数
const dirPath = this.context.filesDir + '/error_logs';
// 如果目录不存在,就先创建
if (!fs.accessSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const filePath = `${dirPath}/${fileName}`;
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, content);
fs.closeSync(file.fd);
Logger.info(`错误日志已写入: ${filePath}`);
result.success({ path: filePath });
} catch (error) {
Logger.error(`写入错误日志失败: ${error.message}`);
result.error('WRITE_FAILED', error.message, null);
}
}
// 将错误报告发送到远程服务器
private async sendErrorReport(args: any, result: MethodResult): Promise<void> {
try {
const { reportData, serverUrl } = args;
let httpRequest = http.createHttp();
let options = {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify(reportData)
};
let response = await httpRequest.request(serverUrl, options);
Logger.info(`上报完成,状态码: ${response.responseCode}`);
if (response.responseCode === 200) {
result.success({ success: true });
} else {
result.error('HTTP_ERROR', `Server returned ${response.responseCode}`, null);
}
httpRequest.destroy();
} catch (error) {
Logger.error(`网络上报失败: ${error.message}`);
result.error('NETWORK_FAILED', error.message, null);
}
}
}
2.3 调整 Dart 侧代码,让它兼容鸿蒙
通常你不需要动 flutter_catcher 的核心逻辑,但要确保它调用平台通道时使用的名称和鸿蒙侧一致。比较好的做法是加一个适配层,或者直接修改插件的源码。
下面是一个修改 lib/flutter_catcher.dart 的示例:
dart
// 原本的代码可能直接用了 MethodChannel('...')
// 我们要确保鸿蒙和 Android/iOS 使用同一个通道名,或者在鸿蒙上用专门的通道名。
import 'dart:io';
class _CatcherNativeHandler {
static const MethodChannel _channel = MethodChannel('xclud.io/catcher'); // 统一通道名
static Future<String> writeErrorLog(String fileName, String content) async {
try {
// 鸿蒙和 Android/iOS 都走这个接口
final String result = await _channel.invokeMethod('writeErrorLog', {
'fileName': fileName,
'content': content,
});
return result;
} on PlatformException catch (e) {
print('Failed to write log: ${e.message}');
rethrow;
}
}
static Future<bool> sendReport(Map<String, dynamic> report, String url) async {
try {
await _channel.invokeMethod('sendErrorReport', {
'reportData': report,
'serverUrl': url,
});
return true;
} on PlatformException catch (e) {
print('Failed to send report: ${e.message}');
return false;
}
}
}
2.4 在 Flutter 应用里集成并测试
第一步:配置 pubspec.yaml
yaml
dependencies:
flutter_catcher: ^0.6.11
# 如果你的适配是作为一个独立插件发布的:
flutter_catcher_ohos:
path: ./path/to/your/adapted_plugin
第二步:在主文件中初始化 Catcher
dart
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_catcher/flutter_catcher.dart';
import 'package:flutter_catcher_ohos/flutter_catcher_ohos.dart'; // 鸿蒙适配插件
void main() {
// 配置 Catcher
var catcher = Catcher(
rootWidget: MyApp(),
debugConfig: CatcherConfig(
// 静默模式:出错直接上报
SilentReportMode(),
[
ConsoleHandler(),
HttpHandler(
HttpRequestBuilder(),
printLogs: true,
),
// 可以为鸿蒙加一个专用的文件处理器
if (Platform.isOHOS) OHOSFileHandler(),
],
),
releaseConfig: CatcherConfig(
DialogReportMode(),
[HttpHandler(HttpRequestBuilder())],
),
);
// 如果是鸿蒙平台,可以做一些额外初始化
if (Platform.isOHOS) {
FlutterCatcherOHOS.initialize();
}
// 启动应用
catcher.bootstrap();
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Flutter Catcher OHOS Demo')),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () => throw Exception('这是一个测试异常!'),
child: Text('触发异常'),
),
ElevatedButton(
onPressed: () async {
// 手动测试通道是否通畅
try {
await _CatcherNativeHandler.writeErrorLog(
'manual_test.log',
'手动测试日志内容',
);
} catch (e) {
print(e);
}
},
child: Text('测试本地写入'),
),
],
),
),
),
);
}
}
三、性能优化与调试技巧
3.1 让性能更好一点
- 异步操作 :鸿蒙侧的所有文件读写和网络请求都必须用异步 API,别阻塞 UI 线程(这一点和 Flutter 要求一样)。上面的示例代码已经用了
async/await。 - 数据序列化:通过通道传数据时,尽量让结构简单、可轻松转成 JSON。别传太大的二进制数据,可以考虑分片,或者只传文件路径。
- 错误采样:正式环境中,可以加一个采样逻辑,避免错误太多导致频繁调用原生侧和网络请求,白白消耗资源。
- 及时释放资源 :在鸿蒙侧,像
HttpClient这类资源用完后记得销毁(比如示例里的httpRequest.destroy()),防止内存泄漏。
3.2 调试时可能会遇到的问题
- 善用日志 :在鸿蒙侧做好日志记录(比如前面代码里的
Logger),通过hdc shell hilog命令就能查看,这是排查通道通信问题最直接的方法。 - 通道名要对齐 :检查 Dart 侧
MethodChannel的通道名和鸿蒙侧@FlutterPlugin(CHANNEL_NAME)里的名字是否一字不差。 - 参数结构保持一致 :Dart 侧
invokeMethod传的arguments必须和鸿蒙侧onMethodCall里期待的args结构对上。调试时可以先用JSON.stringify把参数打印出来看看。 - 别忘了权限 :在鸿蒙应用的
module.json5里,记得声明需要的权限,比如网络访问和文件读写。
json
// ohos/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
}
]
}
}
四、写在最后
这篇文章详细介绍了把 flutter_catcher 适配到 OpenHarmony 的整个过程。适配的核心思路其实不难:理解 Flutter 的平台通道机制,然后在鸿蒙侧用 ArkTS 实现一个功能对等的处理器。我们从环境搭建、鸿蒙侧实现、Dart 侧集成,再到性能优化,一步步走了过来。
总结几个关键点:
- 原理不变:适配不改动插件原有的工作流程,只是换一个平台实现。
- 代码尽量兼容 :通过统一的
MethodChannel接口,Dart 代码可以保持多平台一致。 - 处理好平台差异:重点关注鸿蒙在文件系统、网络 API 和权限模型上的不同,并在代码中做相应调整。
- 调试是关键 :多利用鸿蒙的
hilog系统来排查问题,能节省大量时间。
通过这次适配,不仅能让 flutter_catcher 在鸿蒙上跑起来,也为其他 Flutter 插件迁移到鸿蒙提供了一个可参考的思路。随着 HarmonyOS NEXT 不断推进,掌握 Flutter 与鸿蒙原生能力的融合技术,对开发者来说会越来越有价值。未来,我们甚至可以考虑把错误报告和鸿蒙的分布式能力、统一日志服务结合起来,构建更强大的跨平台监控体系。