把 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 -la
2.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;
};
#endif
2.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 的世界里来。
1166

被折叠的 条评论
为什么被折叠?



