Android缓存目录音频无法播放:MediaPlayer与ExoPlayer报错问题
问题分析与解决方案
核心问题
录音文件可手动播放、成功上传,但本地播放失败,根源在于三个关键问题:
- Android 7.0+ 对
file://URI的权限限制 - MediaRecorder未正确释放导致文件被占用
- ExoPlayer实例管理不当引发资源泄漏
解决方案
1. 用FileProvider生成合法Content URI
Android 7.0及以上禁止直接使用file:// URI访问应用私有目录文件,需通过FileProvider生成content://格式的URI:
步骤1:注册FileProvider到AndroidManifest.xml
<application> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application>
步骤2:创建路径配置文件res/xml/file_paths.xml
指定缓存目录的访问路径:
<?xml version="1.0" encoding="utf-8"?> <paths> <cache-path name="audio_cache" path="/" /> </paths>
步骤3:替换Uri.fromFile为Content URI
修改processRecording()中的URI生成逻辑:
private fun processRecording() { val audioFile = File(audioFilePath!!) Log.d("Path>>", audioFilePath!!) if (audioFile.exists()) { val audioBytes = audioFile.readBytes() // 生成Content URI并授予读取权限 val audioUri = FileProvider.getUriForFile( applicationContext, "${packageName}.fileprovider", audioFile ) grantUriPermission( packageName, audioUri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) playRecording(audioUri) // 上传逻辑不变 FireRef.storageRef.child("ChatAudios/").child(audioFile.name).putBytes(audioBytes) .addOnSuccessListener { val url = it.storage.downloadUrl url.addOnCompleteListener { link -> if (link.isSuccessful) { Log.d("URL>>", link.result.toString()) } } } } }
2. 确保MediaRecorder完全释放
录音结束后必须调用stop()和release(),否则文件会被进程占用,无法读取:
private fun stopRecording() { recorder?.apply { try { stop() } catch (e: IllegalStateException) { // 处理录音未启动的异常 } finally { release() recorder = null } } }
注意:必须在调用processRecording()前执行stopRecording(),保证文件写入完成并释放。
3. 优化ExoPlayer实例管理
每次播放新建实例会引发资源泄漏,建议复用单例或完善释放逻辑:
方案1:复用单例实例
private var exoPlayer: ExoPlayer? = null private fun initExoPlayer() { if (exoPlayer == null) { exoPlayer = ExoPlayer.Builder(applicationContext).build() } } private fun playRecording(uri: Uri) { initExoPlayer() exoPlayer?.apply { stop() clearMediaItems() setMediaItem(MediaItem.fromUri(uri)) prepare() play() addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED) { stop() } } override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) stop() } }) } } // 在Activity/Fragment销毁时释放 override fun onDestroy() { super.onDestroy() exoPlayer?.release() exoPlayer = null }
方案2:多实例场景下完善释放逻辑
若需每次新建实例,需在错误或中途停止时也释放资源:
private fun playRecording(uri: Uri) { val exoPlayer = ExoPlayer.Builder(applicationContext).build() val mediaItem = MediaItem.fromUri(uri) exoPlayer.apply { setMediaItem(mediaItem) prepare() play() addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED || playbackState == Player.STATE_IDLE) { release() } } override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) release() // 错误提示逻辑 } }) } }
4. 修复MediaPlayer的播放逻辑
同步prepare()易阻塞线程,改用异步方法并完善错误处理:
private fun playRecording(uri: Uri) { MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // 录音为语音,适配该类型 .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setDataSource(applicationContext, uri) setOnPreparedListener { start() } setOnErrorListener { _, _, _ -> release() true } prepareAsync() } }
验证步骤
- 录音结束后优先调用
stopRecording() - 确认使用FileProvider生成的
content://URI - 检查MediaRecorder是否已完全释放
- 播放时监听错误信息,排查剩余问题
内容的提问来源于stack exchange,提问作者coDeR
相关产品推荐
相关产品推荐

