Flutter跨端录音控制模块实战:统一Android、iOS与HarmonyOS
做跨端录音模块这件事我在好几个项目里反复折腾过。以前无非是 Android 和 iOS 各写一套原生Dart 层封装接口虽然也能用但维护成本实在感人。这次接到“声迹 Recorder”这个需求时正好赶上 HarmonyOS 6.0 的适配窗口我干脆直接用 Flutter 做了一套跨端录音控制模块把 Android、HarmonyOS 和 iOS 三端统一成一个入口。这篇文章就把这个项目从选型、设计到落地踩坑的过程完整拆一遍给正在做类似功能的同学一份能直接抄作业的参考。这个模块解决的核心问题很简单让业务 app 能够像控制一个本地播放器一样控制录音包括开始、暂停、恢复、停止、获取音量波形以及处理后台录音和音频中断。适合 Flutter 开发者、要适配鸿蒙的跨端团队以及做音视频基础能力的同学参考。我会从为什么选 Flutter、录音模块怎么抽象、原生侧怎么接、到常见问题排查一步步讲明白。1. 项目背景与方案选型为什么拿 Flutter 做跨端录音控制1.1 录音控制模块的定位与考验“声迹 Recorder”不是一个独立录音 app而是嵌入在某个内容创作 App 里的基础录音能力。它需要被语音笔记、短视频配音、直播预告等多个业务模块复用所以从一开始目标就不是“能录音就行”而是要做一个可控、可扩展、可观测的录音控制模块。在确定方案之前我梳理了模块要面对的硬条件跨端Android 和 iOS 必须支持新增 HarmonyOS 端控制复杂度需要暂停、恢复、停止监听录音进度与音量生命周期敏感录音界面切后台、来电话、插拔耳机都要正确响应文件输出支持 m4a、wav 等常用格式采样率可配置复用性上层业务模块不关心底层实现只通过统一接口交互。这其实非常考验抽象能力。如果直接用原生代码把录音控制器写在某个页面里后续多端同步会相当痛苦。所以我决定把它做成一个独立的跨端模块Flutter 负责业务逻辑与状态管理原生端只做最小化的录音引擎适配。1.2 Flutter 在鸿蒙跨端里的角色HarmonyOS 6.0 对 Flutter 的支持已经相对成熟官方社区和开源方案都提供了鸿蒙的 Flutter 引擎适配Dart 层的代码基本不用改只需要处理原生平台通道的鸿蒙实现。这正是 Flutter 作为跨端层的价值UI、状态机、业务逻辑全在 Dart 侧统一平台差异被隔离在 plugin 层。我现在的做法是整个录音控制模块作为 Flutter plugin 组织对外暴露一个统一的RecorderController内部根据平台自动分派到 Android、iOS 或 HarmonyOS 的原生实现。上层业务完全不需要知道当前跑在什么系统上。相比用 native 开发三套Flutter 方案在以下几点有明显优势业务控制逻辑如状态切换、错误处理、数据缓冲只需要写一遍且可以用 Dart 单测直接验证热重载对录音控制界面的调试很友好鸿蒙 6.0 的接入成本在可控范围内社区有现成模板可以套。当然也有代价比如音频延迟比纯原生高一点、原生 audio session 的深度控制需要绕道 platform channel。所以我明确划了一条线需要精确到毫秒的实时音频处理绝对不走 Flutter但录音这种秒级无感操作Flutter 足够。1.3 HarmonyOS 6.0 带来的变化HarmonyOS 6.0 在 API 层面强化了音频会话和后台任务管理这对录音模块是个好消息。做录音的人都知道Android 上后台录音的保活和权限管理特别琐碎而鸿蒙 6.0 提供了更清晰的后台任务类型申请接口录音属于audioRecording分类申请方式比较直接。新版也加强了安全隐私管理录音权限需要动态申请并严格声明使用场景。在开发中我遇到过因为没在 module.json5 里声明ohos.permission.MICROPHONE导致运行时一直闪退的情况。这个后面在问题排查环节会详细说。另一个变化是 Flutter 在鸿蒙上的插件注册方式。5.0 时代还需要手动改工程的 CMake 和注册类6.0 开始有一套更自动化的 Flutter 模块集成方案生成能和 ArkTS 流畅通信的 bridge整体体验像极了 Flutter 官方在 Android 上的集成。2. 录音控制模块的核心设计与关键细节2.1 录音能力怎么抽象Plugin 还是 MethodChannel这是项目启动时我第一个决策点。做录音控制第一反应是找现成的 Flutter 插件比如record、flutter_sound但这些插件对鸿蒙的支持要么没有要么只覆盖基础录音无法满足我们精细控制的需求。最后决定自己写一个轻量 plugin只包裹“录音引擎 控制命令 回调事件”。在实现方式上我选择了MethodChannel EventChannel 组合而不是把所有能力都塞进一个 Channel。理由如下MethodChannel 负责一次性调用比如start、pause、resume、stop、getAmplitudeEventChannel 负责持续事件流比如音量回调、录音进度、状态变化两者职责分开天然契合录音这种“命令-响应 持续通知”的模型。Dart 侧对外暴露的样子是这样的abstract class RecorderApi { Futurevoid start(RecordConfig config); Futurevoid pause(); Futurevoid resume(); Futurevoid stop(); StreamRecordEvent get events; }所有原生实现都遵循这个接口这保证了上层业务某种语义上的稳定。2.2 录音权限与生命周期处理录音权限是跨端模块最容易翻车的地方。Android 需要动态请求RECORD_AUDIOiOS 需要在Info.plist配置NSMicrophoneUsageDescriptionHarmonyOS 需要同时配置权限声明和动态申请。我在模块内部封装了一层PermissionHandler统一处理三端权限在 Dart 侧发起ensurePermission()底层通过 MethodChannel 调用原生权限请求对于 Android 和鸿蒙都要处理用户拒绝后跳系统设置的操作iOS 还有一个AVAudioSession的requestRecordPermission回调。特别要注意的是HarmonyOS 6.0 的权限请求是异步回调不能像早期版本那样同步 await。我在这里踩过一次坑导致 start 方法被回调结果竞态覆盖。最后统一用Promise风格封装保证权限结果返回后再进入录音状态机。生命周期方面录音控制模块必须感知 app 前后台切换和页面销毁。我在 Flutter 层注册AppLifecycleListener一旦发现paused状态且有录音在进行就自动触发暂停回到前台时再自动恢复。这样做虽然简单粗暴但能够避免很多手机厂商激进的后台清理策略导致的音轨损坏。2.3 音频焦点与会话管理录音不仅是“开麦克风、写文件”还会和音频播放、电话、通知产生冲突。以 Android 为例录音前必须申请音频焦点否则来电时录音会被系统直接掐断。HarmonyOS 6.0 同样有自己的音频会话管理接口。在处理这些情况时我建立了一套事件映射规则电话打进录音自动暂停pause等电话挂断后根据策略恢复播放器抢占焦点降低采样或者停止录音并及时通知 UI 层系统录音权限被关闭触发错误码permissionDenied终止状态机。鸿蒙端我使用audio.AudioRenderer只是用于播放录音则用AVRecorder。关键是AVRecorder在启动时需要设置audioCaptureSourceType为MIC同时配置好captureRate和音频通道。这个配置直接影响后期文件体积和声音质量不能拍脑袋乱填。2.4 录音状态机与控制命令录音控制最核心的其实是状态机。业务侧不管按多少次暂停、停止底层都必须保证状态合法转换。我把录音状态定义成四种idle空闲可以开始recording录音中可以暂停、停止paused已暂停可以恢复、停止stopped已停止准备释放资源。Dart 侧一个枚举搞定enum RecorderState { idle, recording, paused, stopped }每次命令进入都会先校验当前状态如果不合法就直接返回错误码。比如在idle状态下收到pause()我会抛一个InvalidStateException防止原生端状态错乱。这个状态机还承接了录制时间的累计。因为暂停时系统 clock 还在走我们不能直接用当前时间减开始时间。我的做法是累计elapsedDuration now - segmentStartTime暂停时记录下来恢复时重置segmentStartTime。这个细节如果不注意UI 上的录音时长会显示得比实际多。3. 实操过程从零搭建跨端录音控制链路3.1 环境准备Flutter SDK 与 HarmonyOS 工程接入先说环境。开发机上我用的 Flutter 版本是 3.22Dart 3.4不过当时配置的时候系统提示过一句The current configured Flutter SDK is not known to be fully supported. Please...这句话的意思是当前 Flutter SDK 版本和工程依赖的 Gradle/AGP 组合有兼容性警告。大部分情况不影响运行但如果你用的是最新版 Flutter 配旧版 Android Gradle Plugin确实会编不过。我最终固定了一套组合Flutter 3.22.5 AGP 8.2.2 Gradle 8.4。HarmonyOS 6.0 的工程结构和 Android 完全不同不过现在有hvigor构建工具和 DevEco Studio 来管理。我们需要在现有 Flutter 工程里加入鸿蒙的entry模块并让 Flutter 引擎通过hms_plugin桥接到鸿蒙侧。在接入鸿蒙侧前一定要确认build-profile.json5和oh-package.json5都配置好了 Flutter SDK 路径。如果没配置后面所有 MethodChannel 调用都会卡死在not implemented。3.2 编写 Dart 侧录音控制接口我在 Dart 侧建立了一个recorder_method_channel.dart使用常量名来降低沟通成本static const _channel MethodChannel(com.shenji.recorder/methods); static const _eventChannel EventChannel(com.shenji.recorder/events); Futurevoid start(RecordConfig config) async { await _channel.invokeMethod(start, config.toMap()); } StreamRecordEvent events() _eventChannel.receiveBroadcastStream().map((e) RecordEvent.fromMap(e));RecordConfig我会传入这些关键参数采样率默认 44100通道数默认 1单声道反而适合语音编码格式m4a(AAC) 或 wav最大时长用于防止无限录音撑爆内存音量回调间隔500ms。这些参数要写成可配置因为不同业务对格式要求差别很大。比如语音笔记可能要 wav 方便剪辑短视频配音则要 m4a 控制体积。事件流里我定义了音量、时长、状态变化三个事件类型。音量归一化成 0~1 浮点这样 UI 侧不需要关心平台差异。3.3 HarmonyOS 侧录音服务实现HarmonyOS 侧我用 ArkTS 写了RecorderService.ets核心依赖是ohos.multimedia.media里的AVRecorder。初始化大概长这样let avRecorder await media.createAVRecorder(); let recordingConfig: media.AVRecorderConfig { audioSourceType: media.AudioSourceType.SOURCE_TYPE_MIC, profile: { audioCodec: media.CodecMimeType.AUDIO_AAC, sampleRate: 44100, channels: 1 }, url: fd:// fileDescriptor, fileFormat: media.ContainerFormatType.CFT_MPEG_4A };这里的fd是应用通过fileIo创建的文件描述符。很多人会直接传路径字符串结果AVRecorder报错找不到文件。我建议一律使用文件描述符避免路径映射问题。鸿蒙的AVRecorder是状态机模型必须等回调进入prepared状态后才能调start()。所以要严格按照create - prepare - start - pause - resume - stop - release的顺序处理。另外鸿蒙端还需要在on(audioCapturerChange)或on(stateChange)里监听状态把变化同步到 Dart 侧。我实现了一个emitState(state)方法通过EventHandler把事件送入 EventChannel。3.4 打通控制链路开始、暂停、恢复、停止这四个操作是录音模块的基本功但每个平台都有各自的坑。开始录音Dart 先请求权限权限通过后再调原生start。原生端拿到采样率和文件路径后创建AVRecorder或 AndroidMediaRecorder然后调用prepare和start。这里要注意必须等原生端返回“已经真正开始”后Dart 侧再更新状态为 recording不能提前。否则 UI 显示在录音实际音频流还没建立容易丢失开头几百毫秒。暂停对 MediaRecorder 来说很尴尬因为 Android 的MediaRecorder.pause()只在 API 24 以上有效而且有些国产 ROM 实现有 bug暂停后无法恢复。我最终在 Android 上改用自定义AudioRecordAACEncoder方案才彻底解决暂停恢复的兼容问题。鸿蒙的AVRecorder原生支持 pause/resume所以简单一点。恢复恢复前要重新配置一下音频源因为某些手机在暂停时会切断音频管线。鸿蒙 6.0 上只需调resume()但需要考虑暂停了多久要不要在事件流里补一个durationUpdated事件。停止停止时最需要注意资源释放。原生端必须等回调真的进入stopped状态后再释放 recorder 实例否则下一次 start 会报 IllegalStateException。我在 Dart 侧还会等一个stopComplete事件然后才把状态改成stopped并把文件路径回传。这四条链路走通后整个模块的骨架就稳定了。4. 常见问题与排查实录4.1 Flutter SDK 兼容性警告怎么破不少人在拿到项目后都遇到过开头说的那句 “The current configured Flutter SDK is not known to be fully supported”。这句是 Flutter 官方在检测到 SDK 版本不在白名单时给出的warning级别提示。我这边出现过两个分支场景一是用 Flutter 3.29 配旧版 Kotlin 或 Gradle导致编译期报Main Gradle plugin相关错误二是 Flutter 3.24 配新版 AGP 8.5 时出现 Gradle 配置缓存不兼容。我的解决思路很简单不要盲目追新先用 Flutter 官方建议的版本组合。固定好 Flutter 版本后再检查android/settings.gradle里的agp版本。其实这句 warning 在大多数情况下不影响真机运行但如果你同时用了较新的 Flutter 引擎特性比如 Impeller兼容问题就会浮出水面。4.2 权限弹窗和后台录制的坑HarmonyOS 6.0 的动态权限申请让我印象深刻不仅要requestPermissionsForResult还必须在 UIAbility 的onCreate中声明需要展示的弹窗文案。如果你没写权限弹窗直接不出现导致录音失败。后台录制这块鸿蒙规定如果 app 退到后台还要继续录音必须申请长时任务continuouslyTask并且类型为audioRecording。申请成功后系统会常驻通知栏提示“正在录音”。如果你的 app 不允许通知栏常驻后台录音可能被系统中断。Android 侧类似前台录音时如果 targetSdk 34要在服务里加foregroundServiceType microphone否则后台直接杀进程。这些规则很细碎但真到了线上每一项都是用户差评的来源。4.3 录音文件采样率与格式选择我发现不少团队在录音格式上很随意。默认 44100Hz 双声道签名音频看似高端实际上对语音场景没有任何好处。在我的实践里语音笔记使用单声道 16bit 22050Hz m4a文件大小和音质平衡点最优如果要人声质检或者后续做 ASR建议直接用 16bit PCM 16000Hz再转 wav。因为 ASR 模型一般都对 16k 采样率做过优化。HarmonyOS 的AVRecorder对采样率的支持也有上限某些中低端设备并不支持 96000Hz需要动态查询AudioManager.getSupportedFormats或者做个 fallback如果创建 recorder 时报参数错误就自动降到 44100Hz 重试。4.4 状态管理用 Cubit 还是 Bloc“声迹 Recorder”的 UI 层我用了 Flutter 的 Cubit 做状态管理。为什么不用 Bloc因为录音控制的状态其实很线性事件源单纯不需要 Bloc 那样复杂的异步变换。Cubit 提供的简单emit已经足够。不过要注意Cubit 里管理录音状态时需要把录音模块原生事件的 Stream 订阅起来。用 Cubit 的优点是原生事件流转化成 UI 状态后页面销毁时close()会自动取消订阅不用手动处理内存泄漏。如果你要用 Bloc也可以但是要小心BlocListener和录音回调的频率。音量回调 500ms 一次如果 UI 侧每个周期都 rebuild不必要的 widget 会显著卡顿。我会在 UI 层做一层throttle确保界面上音量条刷新不超过 20fps。4.5 性能与包体积优化录音控制模块虽然是基础能力但也得注意性能。我在这一版的调优里做了几件事音量回调默认送到 UI 层但上层如果不需要音量波形可以在 start 时传enableLevelMeterfalse原生端直接不计算 amplitude减少 CPU 占用录音文件的 IO 操作必须在子线程执行不能在主线程写数据否则音频数据会丢帧。鸿蒙的AVRecorder封装了内部线程但 Android 自定义AudioRecord时一定要thread.start()包体积方面尽量不要引入体积大的蓝牙音频处理库。只要用系统 API鸿蒙和 Android 的录音代码加起来体积很小最终产物只增加不到 1MB。另外 Impeller 渲染引擎事也值得提一句。Flutter 3.22 之后 iOS 默认启用 ImpellerAndroid 如果想要更流畅的动画可以手动开。但录音控制界面上如果播放音量波形图Impeller 反而可能因为 shader 编译产生轻微迟滞。我实测下来关闭 Impeller 在低端机上更稳。5. 一些实在的调试工具与 Cookies 技巧做录音模块最痛苦的是黑盒调试。你根本不知道麦克风有没有在收声文件写了一半还是权限被系统悄悄回收。所以我给自己搭了一套调试辅助方案给读者借鉴。原生侧加日志埋点每次状态切换都打印带时间戳的日志比如[Recorder] pause() at 1723452340.1Dart 侧用dart:developer的Timeline记录事件间隔线上如果怀疑录音模块异常上报RecorderStateSnapshot包含当前状态、已用时长、文件路径和最后一次错误码。在实际编码中我还养成了一个习惯先写 Dart 侧接口定义再写原生实现。因为 Dart 接口就是一份契约原生实现只要对照编译期检查就能保证方法名和参数不串。如果先做了原生再回头接 Dart往往因为大小写或者参数名不一致反复折腾浪费时间。另外鸿蒙端调试要特别注意 API 版本。我在部分 API 上顺利用 5.0 的方法但 6.0 上已经标记废弃会打 deprecation 警告。所以开发时一定要用 DevEco Studio 的 SDK 说明对照一遍别条件反射照抄老代码。写到这里“声迹 Recorder”的核心思路和实现细节基本都在这里了。我个人对 Flutter 跨端的最大体会是跨端工程最怕的不是写代码而是抽象边界没划清。把原生能力牢牢关在 channel 的黑盒里业务侧只和状态机打交道这样哪怕未来还要接一个新的系统你也不需要重写任何业务逻辑。我下一步打算把这个模块里的录音链路继续扩展到音频剪辑预览让它的价值再进一步。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →