把 Flutter 插件搬上 OpenHarmony:手把手适配音频录制库
前言
OpenHarmony(后面简称 OHOS)的生态越来越热闹,它的分布式能力和全场景体验确实给开发带来了新的想象空间。对于我们这些熟悉 Flutter 的开发者来说,很自然会想:能不能把 Flutter 丰富的跨平台生态和 OHOS 的原生能力结合起来?这既能扩大应用的覆盖面,也能提升开发效率。
但想法很美好,现实却有道坎:Flutter 海量的第三方库,绝大多数都是为 Android 和 iOS 准备的,想让它们在 OHOS 上顺利跑起来,是个既关键又充满挑战的技术活儿。
今天,我就以一个具体的音频录制插件flutter_record_plugin为例,和大家一起拆解一下 Flutter 插件适配 OHOS 的全过程。通过这个例子,你不仅能学会怎么迁移一个具体的插件,更能掌握一套可以复用的方法和思路,为你后续引入更多 Flutter 生态库铺平道路。
一、 理解适配的核心:原理与技术分析
1.1 Flutter 插件是怎么工作的?
简单来说,Flutter 插件就是一个通信桥梁,核心是Platform Channel。当 Flutter 层的 Dart 代码通过MethodChannel发起调用时,这个消息会被序列化,然后传递到原生平台(Android/iOS),由那边的原生代码执行具体的功能(比如启动录音),最后再把结果传回 Dart 层。
Dart层 (Flutter) <--(Platform Channel)--> 原生平台 (Android/iOS/OHOS)1.2 为 OHOS 适配,关键要做什么?
适配到 OHOS,核心任务就是为这个通信桥梁在 OHOS 端建造一个新的“桥墩”。OHOS 有自己的一套体系,虽然它的 Ability 框架和 UI 框架在理念上和 Android 有些相似,但 API、权限模型、系统服务调用方式都自成一体。所以,我们的工作主要聚焦于两点:
- 接口对齐:在 OHOS 端,原样实现 Flutter 插件约定好的那些
MethodChannel接口方法。 - 能力映射:用 OHOS 原生提供的 API(比如
AudioCapturer、文件操作)去具体实现插件要求的功能(比如录音、存文件)。
1.3 两种适配策略怎么选?
- 完全重写:如果插件功能复杂,或者和 Android API 绑定得很深,最稳妥的办法就是为 OHOS 单独建立一个原生实现目录(比如
ohos/),从头写起。 - 部分复用:如果插件的核心业务逻辑比较独立,可以尝试把这部分逻辑抽成公共代码,然后分别写 Android 和 OHOS 的“外壳”来调用它。不过对于初次适配,通常重写更清晰。
二、 实战开始:适配flutter_record_plugin
2.1 搭好环境,准备开工
首先,确保你的“装备”齐全:
- Flutter SDK: 3.19.0 或更高(需要支持 OHOS 平台)
- DevEco Studio: 4.0 或更高(用于 OHOS 原生开发)
- OHOS SDK: API 12+(对应 HarmonyOS 5.0)
- Node.js: 18.17.0+
用命令创建一个支持 OHOS 的插件模板,这是我们的起点:
# 1. 创建插件模板 flutter create --platforms=ohos --template=plugin flutter_record_plugin_ohos cd flutter_record_plugin_ohos # 2. 看看生成的结构,重点留意 `ohos/` 这个新目录 ls -la2.2 分析插件,设计结构
原来的flutter_record_plugin,它的 Dart API 通常提供了这几个方法:
startRecording(String path)stopRecording()getAmplitude()(这个不一定所有版本都有)dispose()
我们的目标,就是在ohos/目录下,建立一个能响应这些方法调用的原生实现。
2.3 编写 OHOS 原生代码
2.3.1 声明必要的权限 (module.json5)
在 OHOS 上,权限需要在配置文件中明确声明。
{ "module": { // ... 其他配置 "requestPermissions": [ { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.WRITE_AUDIO" }, { "name": "ohos.permission.READ_AUDIO" } // 根据是否需要位置信息,决定是否添加 MEDIA_LOCATION ] } }2.3.2 定义核心音频服务接口 (audio_capture_interface.h)
为了让代码结构更清晰,我们先定义一个 C++ 接口,把核心功能和 Platform Channel 的胶水代码分开。
#ifndef AUDIO_CAPTURE_INTERFACE_H #define AUDIO_CAPTURE_INTERFACE_H #include <string> class AudioCaptureInterface { public: virtual ~AudioCaptureInterface() = default; virtual bool start(const std::string &filePath) = 0; virtual bool stop() = 0; virtual double getCurrentAmplitude() = 0; // 用于获取当前音量振幅 virtual void release() = 0; virtual bool isRecording() const = 0; }; #endif2.3.3 实现 OHOS 音频录制功能 (ohos_audio_capturer.cpp,关键部分节选)
这里就是真正的业务逻辑了,我们使用 OHOS Native API 中的AudioCapturer。
#include "ohos_audio_capturer.h" #include <multimedia/player_framework/audio_capturer.h> #include <fcntl.h> #include <unistd.h> #include "hilog/log.h" // 定义日志标签 #define LOG_TAG "FlutterRecordPlugin" using namespace OHOS::Media; OhosAudioCapturer::OhosAudioCapturer() : isCapturing_(false), audioCapturer_(nullptr), pcmFile_(-1) {} bool OhosAudioCapturer::start(const std::string &filePath) { std::lock_guard<std::mutex> lock(mutex_); if (isCapturing_) { HILOG_ERROR(LOG_APP, "Already recording."); return false; } // 1. 配置并创建 AudioCapturer AudioCapturerOptions options; options.streamInfo.samplingRate = AudioSamplingRate::SAMPLE_RATE_44100; // 44.1kHz 采样率 options.streamInfo.encoding = AudioEncodingType::ENCODING_PCM; options.streamInfo.format = AudioSampleFormat::SAMPLE_S16LE; // 16位深 options.streamInfo.channels = AudioChannel::STEREO; // 立体声 audioCapturer_ = AudioCapturer::Create(options); if (audioCapturer_ == nullptr) { HILOG_ERROR(LOG_APP, "创建 AudioCapturer 失败"); return false; } // 2. 创建文件,准备写入 PCM 数据 pcmFile_ = open(filePath.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR); if (pcmFile_ < 0) { HILOG_ERROR(LOG_APP, "无法创建文件: %{public}s", filePath.c_str()); audioCapturer_->Release(); audioCapturer_ = nullptr; return false; } // 3. 启动录音 if (audioCapturer_->Start() != 0) { HILOG_ERROR(LOG_APP, "启动 AudioCapturer 失败"); close(pcmFile_); audioCapturer_->Release(); audioCapturer_ = nullptr; return false; } isCapturing_ = true; // 开启一个线程循环读取音频数据 captureThread_ = std::thread(&OhosAudioCapturer::captureLoop, this); HILOG_INFO(LOG_APP, "OHOS 音频录制已开始: %{public}s", filePath.c_str()); return true; } void OhosAudioCapturer::captureLoop() { constexpr size_t bufferSize = 4096; uint8_t buffer[bufferSize]; while (isCapturing_) { // 从 AudioCapturer 读取数据 int32_t bytesRead = audioCapturer_->Read(buffer, bufferSize, false); if (bytesRead > 0) { write(pcmFile_, buffer, bytesRead); // 写入文件 // 可以在这里计算当前缓冲区的振幅,供 getCurrentAmplitude 使用 lastAmplitude_ = calculateAmplitude(buffer, bytesRead); } else if (bytesRead < 0) { HILOG_WARN(LOG_APP, "读取音频数据出错: %{public}d", bytesRead); break; } } } // stop(), release() 等方法需要确保线程安全并正确释放资源...2.3.4 编写 Platform Channel 桥接层 (flutter_record_plugin.cpp)
这部分代码是“粘合剂”,负责接收 Dart 层的调用,并转给我们上面写的 OHOS 原生实现。
#include <flutter/plugin-interface.h> #include <memory> #include "ohos_audio_capturer.h" using namespace flutter; class FlutterRecordPlugin : public PluginInterface { public: FlutterRecordPlugin() : audioCapturer_(std::make_unique<OhosAudioCapturer>()) {} void OnMethodCall(const MethodCall& call, const MethodResult& result) override { const auto& method = call.GetMethod(); const auto* arguments = call.GetArguments(); if (method == "startRecording") { if (!arguments || !arguments->IsString()) { result.Error("InvalidArguments", "需要提供文件路径字符串"); return; } bool success = audioCapturer_->start(arguments->StringValue()); result.success(success); } else if (method == "stopRecording") { bool success = audioCapturer_->stop(); result.success(success); } else if (method == "getAmplitude") { double amplitude = audioCapturer_->getCurrentAmplitude(); result.success(amplitude); } else if (method == "isRecording") { bool recording = audioCapturer_->isRecording(); result.success(recording); } else if (method == "dispose") { audioCapturer_->release(); result.success(); } else { result.NotImplemented(); // 不认识的方法 } } private: std::unique_ptr<AudioCaptureInterface> audioCapturer_; }; // 插件的创建和销毁入口函数 extern "C" FLUTTER_PLUGIN_EXPORT PluginInterface* CreatePlugin() { return new FlutterRecordPlugin(); } extern "C" FLUTTER_PLUGIN_EXPORT void DestroyPlugin(PluginInterface* plugin) { delete plugin; }2.4 整合 Dart 层,看看怎么用
Dart 层的 API 我们尽量保持不动,这样原来的 Flutter 业务代码几乎不需要修改。
lib/flutter_record_plugin_ohos.dart:
import 'dart:async'; import 'package:flutter/services.dart'; class FlutterRecordPlugin { static const MethodChannel _channel = MethodChannel('flutter_record_plugin_ohos'); /// 开始录音 static Future<bool> startRecording({required String path}) async { try { final bool result = await _channel.invokeMethod('startRecording', path); return result; } on PlatformException catch (e) { print("启动录音失败: '${e.message}'."); return false; } } /// 停止录音 static Future<bool> stopRecording() async { try { final bool result = await _channel.invokeMethod('stopRecording'); return result; } on PlatformException catch (e) { print("停止录音失败: '${e.message}'."); return false; } } /// 获取当前音量振幅 static Future<double> getAmplitude() async { try { final double amplitude = await _channel.invokeMethod('getAmplitude'); return amplitude; } on PlatformException { return 0.0; // 出错就返回0 } } /// 释放插件占用的资源 static Future<void> dispose() async { try { await _channel.invokeMethod('dispose'); } on PlatformException catch (e) { print("释放资源失败: '${e.message}'."); } } }在 Flutter 应用里,你可以这样调用:
import 'package:flutter_record_plugin_ohos/flutter_record_plugin_ohos.dart'; // 开始录音 bool started = await FlutterRecordPlugin.startRecording(path: '/data/app/recording.pcm'); if (started) { print('已经在 OHOS 上开始录音了!'); } // 比如,每隔100毫秒获取一次振幅来更新UI Timer.periodic(Duration(milliseconds: 100), (timer) async { double amp = await FlutterRecordPlugin.getAmplitude(); _updateVolumeUI(amp); // 更新你的音量条 }); // 停止录音 bool stopped = await FlutterRecordPlugin.stopRecording();三、 让插件更好用:优化和调试技巧
3.1 性能上需要注意的几点
- 管好内存:OHOS Native 层的
AudioCapturer和文件描述符一定要在release()或析构函数里及时释放,这是避免内存泄漏的关键。 - 注意线程安全:录音循环跑在独立线程,像
isCapturing_这种共享状态,读写时必须加锁(比如用std::mutex)。 - 按需调整参数:
AudioCapturerOptions里的采样率、位深不是一成不变的。如果是录语音,可能用单声道、16kHz 就够了,能节省资源;录音乐则需要更高的质量。 - 考虑功耗:长时间后台录音要关注电量消耗和发热,可以适当调整读取数据的策略。
3.2 调试时可能会遇到的坑
- 善用日志:在原生代码里多使用 OHOS 的
HiLog打印关键信息,在 DevEco Studio 的 Log 窗口里根据 TAG (FlutterRecordPlugin) 过滤查看,非常方便定位问题。 - 权限!权限!权限!:这是最容易出问题的地方。务必确认应用在真机或模拟器上已经获取了
MICROPHONE等权限,否则录音会是静音的。 - 检查文件路径:确保你的应用有权限写入目标路径。最保险的做法是使用 OHOS 应用沙箱内的目录(比如通过能力上下文
context获取的路径)。 - 先通通信:写个简单的 Dart 测试程序,把
start,stop,getAmplitude等方法都调用一遍,确保 MethodChannel 通信本身是畅通的,参数传递也没问题。 - 验证录音结果:录下来的 PCM 文件可以用 Audacity 这类音频工具导入播放一下,确认声音内容是否正确,这是功能验证的最后一步。
四、 写在最后
通过上面这一系列步骤,我们成功让flutter_record_plugin在 OHOS 上“安家”了。经过测试,这个适配版插件可以在 OHOS 设备上稳定地进行音频采集,功能和原来的 Android/iOS 版本保持一致。
回顾整个适配过程,有这么几点体会:
- 吃透原理是关键:真正理解了 Platform Channel,适配工作就成功了一半。它就是个通信协议,我们的任务就是在 OHOS 端实现这个协议。
- 保持结构清晰:严格按照 Flutter 插件的标准格式来组织代码,把 OHOS 实现干净地放在
ohos/目录下,后期维护会轻松很多。 - 准确找到“替代品”:适配的本质是功能映射。在 OHOS SDK 里找到对等的原生 API(比如用
AudioCapturer实现录音)是实现功能的核心。 - 稳健比功能多更重要:完善的错误处理、严格的资源生命周期管理、必要的线程同步,这些是保证插件在生产环境稳定运行的基石,一点都不能马虎。
- 这是一座“桥”:我们做的不仅是一个插件的移植,更是在 Flutter 生态和 OpenHarmony 生态之间搭起一座桥。这座桥通了,后面搬运其他优秀的库就会容易得多。
随着 OpenHarmony 自身能力的不断丰富,以及 Flutter 社区对 OHOS 支持的持续完善,我相信两者的结合会越来越紧密。希望这篇实践能给你提供一个清晰的路径,欢迎你一起探索,把更多好用的 Flutter 库带到 OpenHarmony 的世界里来。