一、前言语音相关能力在鸿蒙项目里十分常见语音笔记、客服语音消息、语音转文字、音频回放上传等业务都离不开音频模块。很多开发在调试时前台录音播放一切正常一旦退到后台、频繁启停音频就会遇到各种问题后台录音中断、再次打开提示音频占用、上传后资源没释放导致内存持续上涨甚至直接触发应用闪退。这类问题大多不是 API 调用错误而是生命周期和资源管理没有处理到位。本文基于最新鸿蒙 API讲解音频录制、播放、后台保活、文件上传完整流程所有代码可直接复制运行。二、权限前置配置音频能力需要在module.json5配置静态权限同时代码中动态申请敏感权限。// module.json5 requestPermissions: [ { name: ohos.permission.MICROPHONE, reason: $string:mic_permission_reason, usedScene: { abilities: [./EntryAbility], when: inuse } }, { name: ohos.permission.KEEP_BACKGROUND_RUNNING, reason: $string:bg_permission_reason, usedScene: { abilities: [./EntryAbility], when: inuse } } ]说明MICROPHONE麦克风权限属于敏感权限必须动态申请后台持续录音需要申请后台运行权限上架时权限说明要写清楚业务用途。三、音频录制实现支持后台录音使用 AudioCapturer 完成音频采集录制的音频保存到应用沙箱目录。后台录音核心要点启动录音前申请后台任务防止应用切后台被系统挂起。import { audio } from kit.AudioKit; import { fileIo } from kit.CoreFileKit; import { backgroundTaskManager } from kit.BackgroundTaskKit; import { promptAction } from kit.ArkUI; Entry Component struct AudioRecordDemo { private audioCapturer: audio.AudioCapturer | null null; private fileFd: number -1; private bgTaskId: number -1; State isRecording: boolean false; private filePath: string ; // 初始化音频采集器 async initCapturer() { const capturerInfo: audio.AudioCapturerInfo { source: audio.SourceType.SOURCE_TYPE_MIC, streamInfo: { samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, channels: audio.AudioChannel.CHANNEL_1, sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE } }; this.audioCapturer await audio.createAudioCapturer(capturerInfo); } // 开始录音申请后台任务 async startRecord() { if (this.isRecording) return; await this.initCapturer(); const context getContext(this); this.filePath ${context.filesDir}/record_audio.pcm; this.fileFd await fileIo.open(this.filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY); // 申请后台长时任务保障后台录音 this.bgTaskId await backgroundTaskManager.startBackgroundRunning(context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK); await this.audioCapturer?.start(); this.isRecording true; this.readAudioData(); } // 循环读取音频数据流写入文件 async readAudioData() { if (!this.isRecording || !this.audioCapturer) return; const buffer await this.audioCapturer.read(); if (buffer.length 0 this.fileFd 0) { await fileIo.write(this.fileFd, buffer); } setTimeout(() this.readAudioData(), 10); } // 停止录音释放资源 async stopRecord() { if (!this.isRecording || !this.audioCapturer) return; await this.audioCapturer.stop(); this.isRecording false; await fileIo.close(this.fileFd); // 结束后台任务 if (this.bgTaskId ! -1) { const ctx getContext(this); backgroundTaskManager.stopBackgroundRunning(ctx, this.bgTaskId); this.bgTaskId -1; } // 释放音频实例避免占用 this.audioCapturer.release(); this.audioCapturer null; promptAction.showToast({message: 录音已保存}); } build() { Column() { Row() { Button(this.isRecording ? 停止录音 : 开始录音) .onClick(() { if(this.isRecording) { this.stopRecord() } else { this.startRecord() } }) } .margin(20) } .width(100%) } }四、音频播放实现AudioRenderer 用来播放沙箱内录制好的 pcm 音频文件用完必须 release 释放实例。import { audio } from kit.AudioKit; import { fileIo } from kit.CoreFileKit; Component struct AudioPlayDemo { private audioRenderer: audio.AudioRenderer | null null; private fileFd: number -1; async initRenderer() { const renderInfo: audio.AudioRendererInfo { usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, streamInfo: { samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, channels: audio.AudioChannel.CHANNEL_1, sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE } } this.audioRenderer await audio.createAudioRenderer(renderInfo); } async startPlay(filePath: string) { await this.initRenderer(); this.fileFd await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY); await this.audioRenderer?.start(); this.playLoop(); } async playLoop() { if (!this.audioRenderer) return; const buf new ArrayBuffer(4096); const readLen await fileIo.read(this.fileFd, buf); if(readLen 0) { await this.audioRenderer.write(buf); this.playLoop(); } else { await this.stopPlay(); } } async stopPlay() { if(!this.audioRenderer) return; await this.audioRenderer.stop(); await fileIo.close(this.fileFd); this.audioRenderer.release(); this.audioRenderer null; } build() { Button(播放录音) .onClick(async () { const ctx getContext(this); await this.startPlay(${ctx.filesDir}/record_audio.pcm) }) } }五、音频文件上传录音完成后读取沙箱音频文件通过网络模块上传到后端。import { http } from kit.NetworkKit; import { fileIo } from kit.CoreFileKit; async function uploadAudio(filePath: string) { const fileFd await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY); const stat await fileIo.stat(filePath); const buf new ArrayBuffer(stat.size as number); await fileIo.read(fileFd, buf); const httpReq: http.HttpRequestOptions { method: http.RequestMethod.POST, extraData: buf, header: { Content-Type: application/octet-stream } } const res await http.request(https://xxx/upload, httpReq); console.info(上传结果, res.result); await fileIo.close(fileFd); }六、资源释放避坑重点音频硬件属于系统独占资源这也是最容易踩坑的地方。实例释放AudioCapturer、AudioRenderer 使用结束一定要调用 release不能单纯 stop。只 stop 不 release音频硬件会被持续占用下次启动录音直接报错。文件句柄打开的 fd 必须 close。反复录制不关闭句柄会耗尽应用文件描述符触发 IO 异常。后台任务配对startBackgroundRunning 之后必须成对调用 stopBackgroundRunning。如果应用异常退出系统会自动回收但正常业务流程一定要手动结束后台任务否则会影响应用功耗评分。页面销毁页面销毁时如果录音 / 播放还在进行要主动停止并释放所有实例。页面生命周期 onDisappear 里增加资源回收逻辑。并发限制不要同时创建多个 AudioCapturer 实例同一时刻只允许一个录音实例。七、小结音频模块开发难点不在于采集和播放本身而是资源管理和后台运行的配合。录音启动时申请后台任务保证后台持续采集结束时依次停止采集、关闭文件句柄、释放音频实例、关闭后台任务。录音文件上传完成之后可根据业务需求选择保留或删除本地 pcm 文件减少存储空间占用。把资源释放逻辑封装到统一工具类在页面销毁、录音中断等场景统一调用就能规避绝大多数音频占用、内存泄漏类问题。