Flutter Catcher 在鸿蒙端的错误捕获与上报适配指南

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 的工作流程

简单来说,它的工作可以分为三步:

  1. 错误捕获 :通过自定义的 FlutterError.onErrorrunZonedGuarded 来抓取 Dart 层和 Flutter 框架抛出的错误。
  2. 错误处理 :错误被送到事先配置好的 CatcherOptions 里,这里定义了多种处理方式(比如弹窗提示、静默上报等)。
  3. 平台交互:当需要用到原生能力时(比如写文件、发网络请求),就会通过平台通道去调用原生代码。

1.3 鸿蒙适配要注意什么

鸿蒙用的开发语言是 ArkTS/JS,它的 API 和 Android 的 Java/Kotlin 差别挺大。所以适配时主要得解决下面几个问题:

  1. 在鸿蒙侧实现一个和 Android/iOS 功能相同的通道处理器。
  2. 处理好鸿蒙和 Flutter 在文件路径、网络权限、后台机制等方面的差异。
  3. 尽量保持插件接口一致,让 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 让性能更好一点

  1. 异步操作 :鸿蒙侧的所有文件读写和网络请求都必须用异步 API,别阻塞 UI 线程(这一点和 Flutter 要求一样)。上面的示例代码已经用了 async/await
  2. 数据序列化:通过通道传数据时,尽量让结构简单、可轻松转成 JSON。别传太大的二进制数据,可以考虑分片,或者只传文件路径。
  3. 错误采样:正式环境中,可以加一个采样逻辑,避免错误太多导致频繁调用原生侧和网络请求,白白消耗资源。
  4. 及时释放资源 :在鸿蒙侧,像 HttpClient 这类资源用完后记得销毁(比如示例里的 httpRequest.destroy()),防止内存泄漏。

3.2 调试时可能会遇到的问题

  1. 善用日志 :在鸿蒙侧做好日志记录(比如前面代码里的 Logger),通过 hdc shell hilog 命令就能查看,这是排查通道通信问题最直接的方法。
  2. 通道名要对齐 :检查 Dart 侧 MethodChannel 的通道名和鸿蒙侧 @FlutterPlugin(CHANNEL_NAME) 里的名字是否一字不差
  3. 参数结构保持一致 :Dart 侧 invokeMethod 传的 arguments 必须和鸿蒙侧 onMethodCall 里期待的 args 结构对上。调试时可以先用 JSON.stringify 把参数打印出来看看。
  4. 别忘了权限 :在鸿蒙应用的 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 侧集成,再到性能优化,一步步走了过来。

总结几个关键点:

  1. 原理不变:适配不改动插件原有的工作流程,只是换一个平台实现。
  2. 代码尽量兼容 :通过统一的 MethodChannel 接口,Dart 代码可以保持多平台一致。
  3. 处理好平台差异:重点关注鸿蒙在文件系统、网络 API 和权限模型上的不同,并在代码中做相应调整。
  4. 调试是关键 :多利用鸿蒙的 hilog 系统来排查问题,能节省大量时间。

通过这次适配,不仅能让 flutter_catcher 在鸿蒙上跑起来,也为其他 Flutter 插件迁移到鸿蒙提供了一个可参考的思路。随着 HarmonyOS NEXT 不断推进,掌握 Flutter 与鸿蒙原生能力的融合技术,对开发者来说会越来越有价值。未来,我们甚至可以考虑把错误报告和鸿蒙的分布式能力、统一日志服务结合起来,构建更强大的跨平台监控体系。

相关推荐
TT_Close11 小时前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT12 小时前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
你听得到1114 小时前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
TrisighT2 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
stringwu2 天前
Flutter 开发必备:MVI 架构的高效实现指南
前端·flutter
程序员老刘3 天前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端
用户965597361905 天前
Provider vs Bloc vs GetX vs Riverpod:Flutter 状态管理方案怎么选?
flutter
恋猫de小郭5 天前
Flutter Patchwork,不用 Fork 改依赖包源码的第三方工具
android·前端·flutter
程序员老刘5 天前
跑分第一的编程大模型,我为啥不用?
flutter·ai编程·vibecoding
恋猫de小郭6 天前
苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力
android·前端·flutter