Android协程上下文调用AudioTrack.write致应用崩溃问题求助
AudioTrack播放音频流崩溃问题解决方案
问题重现
使用AudioTrack播放解码后的音频流时遇到三种矛盾场景:
- 主线程直接调用
write:音质正常但UI严重卡顿 - 传入
WRITE_NON_BLOCKING参数:UI恢复流畅但音频频繁中断 - 协程IO线程调用
write:音质流畅且UI无卡顿,但运行数秒或数分钟后触发Native崩溃,错误日志核心信息指向audioTrack?.write:
releaseBuffer: mUnreleased out of range, !(stepCount:952 <= mUnreleased:0 <= mFrameCount:3848), BufferSizeInFrames:3848
崩溃原因分析
该Native层崩溃属于AudioTrack内部缓冲区管理异常,常见触发点:
- AudioTrack实例已调用
release()释放,但后台协程仍持续调用write,触发野指针操作 - 解码后的音频帧参数(采样率、声道数、位深)与AudioTrack初始化配置不匹配,导致缓冲区计数逻辑混乱
- 协程生命周期未与AudioTrack绑定,无法在资源释放时及时终止写入任务
- 忽略
write方法的返回值,未处理写入失败场景,持续写入无效数据破坏缓冲区状态
解决方案
1. 绑定协程与AudioTrack生命周期
使用带Job的CoroutineScope,在AudioTrack释放时主动取消协程,避免无效操作:
// 定义与AudioTrack生命周期绑定的协程Scope private val audioPlaybackScope = CoroutineScope(Dispatchers.IO + Job()) // 启动播放任务 fun startPlaying(decodedFrames: List<ByteArray>) { audioPlaybackScope.launch { try { decodedFrames.forEach { frame -> // 提前检查AudioTrack状态,避免已释放 if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) break val writeResult = audioTrack?.write(frame, 0, frame.size) // 处理写入失败的情况 if (writeResult == null || writeResult < 0) { Log.e("AudioError", "Write failed with code: $writeResult") break } } } catch (e: Exception) { Log.e("AudioError", e.message ?: "Unknown playback exception") } } } // 释放AudioTrack时同步取消协程 fun releaseAudioResources() { audioPlaybackScope.cancel() audioTrack?.stop() audioTrack?.release() audioTrack = null }
2. 校验音频帧与AudioTrack配置匹配
确保解码后的帧参数与AudioTrack初始化参数严格一致,避免缓冲区计数错误:
// 示例AudioTrack初始化参数 val sampleRate = 44100 val channelConfig = AudioFormat.CHANNEL_OUT_STEREO val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 计算单帧字节大小(声道数 × 位深字节数) val singleFrameByteSize = (if (channelConfig == AudioFormat.CHANNEL_OUT_STEREO) 2 else 1) * 2 // 写入前校验帧大小合法性 decodedFrames.forEach { frame -> if (frame.size % singleFrameByteSize != 0) { Log.w("AudioWarn", "Invalid frame size, skip this frame") return@forEach } // 执行写入操作 }
3. 使用回调模式驱动写入(推荐)
避免主动循环写入,改用AudioTrack的周期回调适配缓冲区节奏,减少缓冲区异常:
// 设置播放位置更新监听 audioTrack?.setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener { override fun onPeriodicNotification(track: AudioTrack?) { // 周期性回调补充数据,避免缓冲区耗尽 audioPlaybackScope.launch { writeNextFrame() } } override fun onMarkerReached(track: AudioTrack?) { // 标记位触发,可用于播放完成逻辑 } }) // 设置周期回调的帧间隔(建议为缓冲区大小的1/4) val periodFrames = (audioTrack?.bufferSizeInFrames ?: 0) / 4 audioTrack?.setPositionNotificationPeriod(periodFrames)
4. 正确计算AudioTrack缓冲区大小
初始化时使用系统推荐的最小缓冲区大小,避免硬编码导致的缓冲区适配问题:
val minBufferSize = AudioTrack.getMinBufferSize( sampleRate, channelConfig, audioFormat ) if (minBufferSize == AudioTrack.ERROR_BAD_VALUE) { Log.e("AudioInitError", "Invalid AudioTrack parameters") return } // 实例化AudioTrack时,缓冲区大小建议为最小大小的2倍,提升稳定性 val audioTrack = AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, minBufferSize * 2, AudioTrack.MODE_STREAM )
内容的提问来源于stack exchange,提问作者Stefan Olteanu
相关产品推荐
相关产品推荐

