news 2026/9/3 4:58:40

安卓录音机开发实战:MediaRecorder与权限适配全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
安卓录音机开发实战:MediaRecorder与权限适配全解析

简介:这是一份面向Java开发者的Android录音机完整项目源码,旨在帮助Android初学者与中级开发者系统掌握移动端录音功能开发的核心流程。项目虽小但脉络完整,共41个文件,压缩包仅146KB,以Java源码(3个)、XML配置与布局(15个)、Gradle构建脚本(3个)及PNG界面资源(10个)为主,也包含权限配置和ProGuard混淆规则,可直接导入Android Studio构建运行,适合作为课程设计或毕业设计的起步代码。录音功能基于MediaRecorder类实现,从AndroidManifest权限声明开始,逐一展示了音频源选择、输出格式与编码器设置、采样率和比特率调节、内部或外部文件存储等关键步骤,并配套了录制按钮的UI交互、非主线程操作、错误监听与释放资源等工程实现。开发者可以通过阅读项目代码,具体理解15个高频技术难点,如暂停恢复录音、多片段合并以及Android 6.0以上动态权限适配等,省去边查资料边调试的时间。目前已获得313人学习关注,无论用于个人练习还是技术复盘,这份浓缩的录音项目都能带来直观的参考价值。

1. 需求拆解:录音机不是“点一下开始”那么简单

安卓录音机这个项目,我最初接到的时候以为是个练手小项目,真正做下来才发现里面藏着不少值得琢磨的东西。先别急着写代码,把需求彻底拆一拆,后面能少踩一半的坑。

1.1 核心需求解析

用户侧的“录音机”一般就三个功能:录音、回放、文件管理。但如果你是自己开发一个正式App,藏在背后的是这几层需求:

  • 录制质量可控:支持不同采样率(44.1kHz/48kHz)、不同编码格式(AAC/PCM),能切换音质档位,这是消费级录音App的基础盘。
  • 录音过程可感知:实时显示录音时长,最好有音量波形或分贝指示,让用户知道“它确实在录”。
  • 文件持久化与管理:录音文件要存到公共媒体库(MediaStore)里,让其他文件管理器、音乐播放器也能看到;要能按时间倒序展示,支持重命名和删除。
  • 后台与异常处理:来电打断、锁屏、内存不足、存储空间不足,这些都要做兜底,否则用户录了半小时发现文件损坏,体验直接崩。

1.2 技术选型:MediaRecorder还是AudioRecord

这是第一个岔路口,也是面试里高频出现的问题。我的建议是:项目核心用MediaRecorder,专业场景再上AudioRecord

MediaRecorder是Android封装好的高层录音API,内部把音频采集、编码、封装全串好了,调用逻辑简单,输出的文件是带格式的(m4a/aac/mp3),直接就能播放。

AudioRecord则是PCM裸流,要自己处理编码和封装,能拿到原始音频数据,可以做实时频谱、降噪、变声等功能,但工作量大得多。

我这版录音机以MediaRecorder为主力,原因很现实:开发周期短、系统兼容性好、文件可直接播放。如果你后续想做专业录音、波形可视化和音频分析,再在架构上预留一个AudioRecord的接口替换空间,不冲突。

1.3 你几乎绕不开的权限准备

做录音机,麦克风权限(RECORD_AUDIO)是跑不掉的。但要命的是,不同Android版本对权限的索要时机和方式都有讲究:

  • Android 6.0(API 23)起,麦克风属于危险权限,必须运行时申请,不能只写在Manifest里。
  • Android 11(API 30)起,分区存储强制开启,不能再直接用File路径往外部存储里写,必须走MediaStore。
  • Android 13(API 33)起,新增了通知权限POST_NOTIFICATIONS,如果你的录音App有前台服务通知(后台录音必备),这个也得申请。

权限设计上我建议:进入App时先申请录音权限,把拒绝后的引导逻辑写好,不要在主流程里现申请现崩溃。实际开发中我见过太多因为用户拒绝一次权限,再进页面就空指针崩溃的情况——都死在没做权限被拒后的状态处理。

2. 数据层设计:从文件路径到MediaStore的迁移

Android录音机的数据管理,经历过Android 10之前的“随意写文件”时代,和之后的“分区存储”时代。你要是现在才开始做新项目,直接按MediaStore方案来,别走弯路。

2.1 为什么必须拥抱分区存储

Android 10(API 29)之后的版本,系统强制要求App把媒体文件写入到公共媒体目录时,通过MediaStore接口,而不能再直接拿Environment.getExternalStorageDirectory()拼路径去写。

好处是很明显的:

  1. 录音文件能统一进系统媒体库,用户在其他音乐播放器、文件管理器里也能找到。
  2. App无需申请存储权限,减少权限弹窗,用户心理负担小。
  3. 卸载App时,自己写入的媒体文件可保留也可以删除,系统帮我们管理归属关系。

对应在代码上,保存录音文件的逻辑就变成:

val values = ContentValues().apply { put(MediaStore.Audio.Media.DISPLAY_NAME, displayName) // 文件名,如record_20250101_153000.m4a put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4") put(MediaStore.Audio.Media.RELATIVE_PATH, "Music/MyRecorder") // 相对路径,Android 10+ put(MediaStore.Audio.Media.IS_PLAYBACK, 1) put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis() / 1000) } val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)

这里的核心就是先通过ContentResolver插入一条空记录拿到URI,然后打开这个URI的输出流写入录音数据。注意RELATIVE_PATH这个字段,Android 10以上必须要设置,否则文件会存到默认位置,导致列表查不到。

2.2 录音列表:查询与排序的正确姿势

录音文件列表的展示,我推荐直接“查系统媒体库”,而不是维护自己的数据库。原因很直接:如果你维护了一个DB记录文件路径,一旦用户在系统相册/文件管理器里删除了某些录音,你的DB和实际文件就对不上了,还要额外做同步逻辑。

查询也很简单,一次性把当前App写入的录音捞出来:

val projection = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.SIZE, MediaStore.Audio.Media.DATE_ADDED ) val selection = "${MediaStore.Audio.Media.RELATIVE_PATH} LIKE ?" val selectionArgs = arrayOf("%MyRecorder%") contentResolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, "${MediaStore.Audio.Media.DATE_ADDED} DESC" )

这里有个小技巧:通过RELATIVE_PATH做筛选,只展示本App写的文件,不会把系统的其他录音也带进来。如果你想让App更通用(比如显示所有录音),也可以不要这个筛选条件,但通常个人录音机产品只展示自己的文件夹就够了。

3. MediaRecorder实操:录音全流程的实现细节

上面那些是地基,这章才是真正的施工。我按“初始化→开始→停止→释放”的完整链路,把每一步的关键代码和注意事项都写出来。

3.1 MediaRecorder的基本配置与状态机

MediaRecorder是典型的状态机模型,操作顺序错一步就给你抛异常。核心状态是:Idle → Initialized → DataSourceConfigured → Prepared → Recording → Released

常见的一个坑是:录音完成后直接再次调用start(),会崩——因为状态不对。必须先reset()回到Idle,再重新配置才能再次录制。

我封装了一个简单的录音管理器,关键配置如下:

fun startRecording(context: Context, fileName: String) { if (recorder != null) return recorder = MediaRecorder(context) recorder.apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) // 采样率:CD音质 setAudioEncodingBitRate(128000) // 比特率:128kbps setOutputFile(getOutputFile(context, fileName).toString()) } runCatching { recorder?.prepare() recorder?.start() }.onFailure { releaseRecorder() // 这里要处理prepare失败的情况,比如麦克风被其他App占用 } }

注意:prepare()start()必须放在try-catch里。prepare()失败多半是Sdcard未就绪或路径非法,start()失败大概率是麦克风被占用或权限没给。这两个异常在真机上非常常见。

3.2 输出文件的保存:MediaStore与File的两手准备

刚才说过Android 10以上走MediaStore,但为了兼容老版本(如果你minSdk设得比较低),还是要兼容WRITE_EXTERNAL_STORAGE权限和File直写。我的做法是用一个分支:

private fun getOutputFile(context: Context, fileName: String): Uri { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val values = ContentValues().apply { put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4") put(MediaStore.Audio.Media.RELATIVE_PATH, "Music/MyRecorder") } context.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)!! } else { // API 29以下,直接写公共目录并发出扫描广播 val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) val file = File(dir, "MyRecorder/$fileName") file.parentFile?.mkdirs() Uri.fromFile(file) } }

这里有个细节我要强调:录音时的setOutputFile()传入的是URI,不是String路径。MediaRecorder的重载方法setOutputFile(FileDescriptor fd)setOutputFile(String path)都有,但如果你传入Uri.toString()一定是错的,得用ParcelFileDescriptor或直接传File路径。

3.3 实时时长与音量反馈的实现

录音过程中显示时长和分贝波形,用户感知会很专业。时长好办,开一个定时器,每秒钟去拿MediaRecorder的maxAmplitude做分贝计算和时长累加就行:

val timer = Timer() timer.schedule(object : TimerTask() { override fun run() { val amplitude = recorder?.maxAmplitude ?: 0 val db = if (amplitude > 0) { 20 * Math.log10(amplitude / 32768.0) } else { -80.0 // 静音阈值 } runOnUiThread { binding.tvDuration.text = formatDuration(++durationSeconds) updateWaveView(db) } } }, 0, 1000)

maxAmplitude的值范围是0~32767,是16位PCM最大值。用对数公式转成dB,动态范围大概在-80dB~0dB。波形View可以简单地用一串柱状图实现,每秒钟更新一根柱子,也能做出像样的视觉效果。

4. 播放与文件复用:别让录音变成一次性的

录音功能做完了,紧接着是播放。这块看着不难,但你照着官方示例写AudioManager那一套,很容易忽略一个舒适度问题:录音机里听录音,扬声器和听筒的切换

4.1 播放器的选择与音量控制

做录音回放,我只推荐MediaPlayer或ExoPlayer,不推荐SoundPool——SoundPool更偏向短音效,不适合长音频播放。

MediaPlayer播放录音文件的示例:

mediaPlayer = MediaPlayer().apply { setDataSource(context, uri) setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) prepare() start() }

这里有一个很实用的细节:录制和播放之前的音频焦点处理。用户在播放录音时,如果突然来电话或导航提示,播放应该暂停而不是继续吵。我给播放器设置了AudioManager.OnAudioFocusChangeListener,在丢失焦点时暂停,在恢复焦点时继续:

val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) .setOnAudioFocusChangeListener(focusListener) .build() audioManager.requestAudioFocus(focusRequest)

这个细节虽然小,但做出来之后,App的“专业感”会明显不一样。用户录完音后去刷抖音,抖音声音不会被录音机卡掉,反过来录音机播放时来电话也不会卡住电话铃声。

4.2 录音文件的删除与重命名

列表页的删除和重命名看起来常规,但Android 10以后都要用到MediaStore的API:

// 删除 contentResolver.delete(uri, null, null) // 重命名 val values = ContentValues().apply { put(MediaStore.Audio.Media.DISPLAY_NAME, newName) } contentResolver.update(uri, values, null, null)

只需要操作ContentResolver,不需要关心底层文件的物理路径,这是分区存储带来的便利。另外要注意的是,如果你用File直接删除(针对Android 9及以下的旧文件),还需要给MediaStore发一个Intent.ACTION_MEDIA_SCANNER_SCAN_FILE广播,否则系统图库里还会残留文件信息,显示一个“假文件”。

5. 真机调试与适配:不同系统版本的录音坑

我在适配过程中踩了不少系统的坑,这里挑几个典型的记录一下吧。

5.1 权限相关:Android 6/11/13三个分水岭

Android权限模型这几年改了三次,我得分别适配:

  • Android 6(API 23):没有动态权限,Manifest.permission.RECORD_AUDIO只在安装时授予。如果你的minSdk低于23,老机器上不会弹权限框,但录音会静音。必须显式判断SDK版本,低于23直接进录音,高于23运行时请求。
  • Android 11(API 30):分区存储强制生效,不能用File写公共目录。如果你的代码里有Environment.getExternalStorageDirectory()的老写法,Android 11上一定会写入失败。
  • Android 13(API 33):新增POST_NOTIFICATIONS权限。如果你的录音服务是前台服务(录音过程中屏幕关闭后继续录),Android 13上必须显示通知,否则系统直接Kill掉你的Service。

5.2 真机问题实录

我拿手边几台不同厂商的机器测试,整理成下表:

问题现象原因解决方案
部分小米/红米机型录音断断续续系统录音权限弹窗后,需要等到用户点击允许才继续不要在主线程同步等待权限,用registerForActivityResult异步处理
华为EMUI后台录音被系统清理省电策略干掉后台Service前台服务+通知,或在App内引导用户关闭电池优化
录音前几秒有滋滋的电流声部分机型麦克风启动需要时间,拿到的音频字节是无效的开始录音后延迟500ms~800ms再显示“正在录音”,或者舍弃前0.5秒的静音数据
部分Android 13设备上MediaRecorder输出文件大小为0存到MediaStore后没有正确关闭输出流在停止录音后确保fileDescriptor.close()被调用

第三个电流声问题,我一开始也以为是算法问题,后来发现是硬件层面的“预热”。我加了一个小延时,用户体验上基本无感,但文件质量明显提升。

5.3 后台录音的保活策略与合理边界

后台录音是录音类App的刚需场景,但也是最容易被手机系统“管教”的。我在做法上分三层:

  • 前台服务:录音时启动前台服务并显示通知,声明foregroundServiceType="microphone"(Android 14要求声明)。
  • 前台服务类型声明:在AndroidManifest里给Service添加android:foregroundServiceType="microphone",并在运行时动态申请FOREGROUND_SERVICE_MICROPHONE权限。
  • 合理降级:如果用户明确关闭了通知权限或系统禁止前台服务启动,就回退为普通Service+弱提醒,录音中断时给用户一个通知。

这里要强调一个安全边界:录音App的保活,是为了“用户主动开始录音后,App退到后台也能继续把这段录音录完”。绝对不要拿这个能力去做任何后台偷偷录音的事情——既违反用户隐私预期,在应用市场审核上也过不了。

6. 常见问题排查:从表象到根因的定位思路

最后这部分,我整理一套问题排查逻辑。真机上遇到问题,先不要瞎改代码,沿着这个思路来定位。

6.1 录音失败类问题

最典型的报错就是start failedprepare failed。定位步骤:

  1. 检查运行权限:设置里看App有没有麦克风权限。有的厂商ROM在第一次拒绝权限后,会进“仅在本次允许”状态,杀进程后自动收回权限,特别坑。
  2. 检查是否被其他App占用麦克风:微信语音、系统语音助手、甚至某些地图App的语音搜索都会占住麦克风。MediaRecorder的start失败代码是-19(EACCES),这时候提示用户关闭其他录音应用即可。
  3. 检查存储空间:录了半小时后磁盘满了,MediaRecorder的start或write会异常。最好在开始录音前做一次可用空间判断,少于100MB时提醒用户。

6.2 录音文件不存在或无法播放

录音文件生成了,但列表里看不到,或者播放失败。我的排查路径:

  • 先看MediaStore查询条件里的RELATIVE_PATH和实际写入路径是否一致。常见错误是写入用“Music/MyRecorder”,查询却用“MyRecorder”,结果查不到。
  • 确认录音停止时是否正确关闭了输出流。停录流程是:stop()reset()release(),顺序不能错。如果直接调用release()而不先stop(),MediaRecorder会自己把文件finalize,但有时候会生成一个0字节的坏文件。
  • 检查录音编码是否被系统播放器支持。AAC/AAC-LC基本所有设备都支持,但如果你换了AMR-NB(语音通话格式),部分音乐播放器识别不了,但系统自带的录音机是能放的。

6.3 容易被忽略的性能与体验细节

最后分享几个体验层面的小优化,做完之后整个App的质感会不一样:

  • 录音计时不准:用SystemClock.elapsedRealtime()做计时基准,不要用Date,因为用户中途改系统时间会导致计时错乱。
  • Dialog与通知的文案:录音开始时给用户一个明确的当前状态。我加了双保险:前台服务通知+页面内红点闪烁动画,让用户无论在哪个界面都知道“正在录音”。
  • 大文件处理:长时间录音(2小时以上),MediaRecorder会生成几个GB的文件。建议在录音超过一定时长(比如1小时)时自动分段保存,方便管理也减少文件损坏风险。

这个录音机做下来,我最大的体会是:看似简单的系统级功能,一旦涉及到设备和系统的碎片化适配,里面全是细节。录音的代码量不大,但如果不能把状态机、权限适配、MediaStore原理弄明白,排起bug来会非常痛苦。希望这篇记录能帮你把这块的路趟得平一点,少踩几个隐形的坑。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 4:56:33

基于STM32的模糊PID水温控制系统设计与Proteus仿真

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 4:56:02

南卡OE GT2开放式耳机评测:百元价位如何平衡音质、佩戴与续航

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 4:55:56

Flutter首次提交iOS包避坑指南:版本号、打包路径与上传问题

如果 Flutter 项目只跑过 Android,第一次接到“把 iOS 包提上去”的任务时,你很可能在最后一个环节卡住一整天。原因不是 Dart 代码有问题,而是 Flutter 帮你复用逻辑和 UI,却没有帮你抹平 Xcode 工程配置、证书签名、ipa 导出和上…

作者头像 李华
网站建设 2026/9/3 4:55:18

Arduino智能小车三模切换:红外传感器与状态机实战教程

在实际嵌入式课程设计和电子竞赛项目中,智能小车是一个经典的综合实践载体。它融合了微控制器编程、传感器数据采集、电机驱动控制以及简单的决策算法,是检验学生硬件连接、软件调试和系统集成能力的绝佳平台。本文将以“红外三模智能切换小车”为具体目…

作者头像 李华
网站建设 2026/9/3 4:54:06

多智能体辩论系统:用对抗性验证提升AI答案可靠性的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 4:54:00

AI技术博客写作指南:从OCR到ComfyUI的实践

抱歉,我无法根据这个标题生成 CSDN 技术博客正文。原因很直接:标题内容是横滨冠军赛国乒男队相关报道,属于体育赛事话题,与我的角色(AI 模型/本地部署/开发工具的 CSDN 技术写作)不匹配,也不在我…

作者头像 李华