Android节拍器APP反复点击启停按钮崩溃原因排查
分析与解决方案:Android节拍器快速启停崩溃问题
从你的崩溃栈和代码来看,根本原因是快速点击启停按钮时,AsyncTask实例和AudioTrack资源的生命周期没有正确同步:每次停止你都会创建新的MetronomeAsyncTask,但旧的任务可能还在后台运行,对应的AudioTrack可能已经被尝试停止/释放,却还在被线程调用write()方法,导致抛出IllegalStateException: Unable to retrieve AudioTrack pointer for write()。
具体问题点拆解
- AsyncTask未正确取消与资源清理:你的
stop()方法只是创建了新的AsyncTask,但旧的任务的doInBackground可能还在循环播放,没有被中断,也没有释放AudioTrack资源。 - AudioTrack状态非法:当旧线程还在尝试写入AudioTrack时,该AudioTrack可能已经被停止或释放,此时调用
write()就会触发系统层的指针获取失败异常。 - 并发状态混乱:快速点击导致多个AsyncTask实例同时存在,多个线程操作音频资源,引发状态冲突。
分步解决方案
1. 给MetronomeBrain添加安全的停止与资源释放机制
在你的MetronomeBrain类中,添加一个volatile标志位控制播放循环,同时实现资源释放方法:
public class MetronomeBrain { private AudioGenerator audioGenerator; private volatile boolean isPlaying = false; // 用volatile保证线程可见性 private Handler handler; public MetronomeBrain(Handler handler) { this.handler = handler; audioGenerator = new AudioGenerator(); audioGenerator.createPlayer(); // 初始化AudioTrack } public void startPlaying() { isPlaying = true; } public void playBeat() { if (!isPlaying) return; // 播放前先检查状态 // 发送节拍更新消息到UI线程 Message msg = handler.obtainMessage(); msg.obj = /* 当前节拍字符串 */; handler.sendMessage(msg); // 仅在播放状态有效时写入音频 if (audioGenerator != null && isPlaying) { audioGenerator.writeSound(); } } public void stopPlaying() { isPlaying = false; } public void releaseResources() { stopPlaying(); if (audioGenerator != null) { audioGenerator.releaseAudioTrack(); audioGenerator = null; } } }
2. 修复AsyncTask的取消逻辑
修改MetronomeAsyncTask,让它正确响应取消请求,并在任务结束时释放资源:
private class MetronomeAsyncTask extends AsyncTask<Void, Void, String> { private MetronomeBrain metronome; MetronomeAsyncTask() { mHandler = getHandler(); metronome = new MetronomeBrain(mHandler); metronome.startPlaying(); } public void stop() { if (metronome != null) { metronome.stopPlaying(); } cancel(true); // 触发AsyncTask取消 } @Override protected String doInBackground(Void... params) { metronome.setBeat(4); long bpmInterval = 60000 / /* 你的BPM值 */; // 计算节拍间隔 while (!isCancelled()) { // 循环中检查是否被取消 metronome.playBeat(); try { Thread.sleep(bpmInterval); } catch (InterruptedException e) { // 被中断后恢复中断标志,退出循环 Thread.currentThread().interrupt(); break; } } // 无论正常结束还是取消,都释放资源 if (metronome != null) { metronome.releaseResources(); } return "Task completed"; } }
3. 优化启停按钮的状态管理
不要每次停止就立即创建新的AsyncTask,而是确保旧任务完全结束后再创建新实例,同时同步按钮点击逻辑避免并发:
public synchronized void playStopButtonPressed(View v) { boolean wasPlayingWhenPressed = playStopButton.isSelected(); playStopButton.setSelected(!wasPlayingWhenPressed); if (wasPlayingWhenPressed) { stopPressed(); } else { // 只有当旧任务已经完成/取消时,才创建新任务 if (aSync == null || aSync.getStatus() == AsyncTask.Status.FINISHED) { aSync = new MetronomeAsyncTask(); } playPressed(); } } private void stopPressed() { if (aSync != null && aSync.getStatus() != AsyncTask.Status.FINISHED) { aSync.stop(); } } private void playPressed() { if (aSync.getStatus() == AsyncTask.Status.PENDING) { aSync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }
4. 完善AudioGenerator的资源释放
在AudioGenerator中添加releaseAudioTrack方法,确保AudioTrack被正确停止和释放:
public void releaseAudioTrack() { if (audioTrack != null) { if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) { audioTrack.stop(); } audioTrack.release(); audioTrack = null; } }
关键注意点
- volatile关键字:用来保证
isPlaying标志位在多线程中的可见性,避免线程读取到过期状态。 - AsyncTask的
isCancelled()检查:在doInBackground的循环中定期检查,确保任务能及时响应取消请求。 - 资源释放时机:无论任务正常结束还是被取消,都必须释放AudioTrack资源,避免内存泄漏和状态冲突。
内容的提问来源于stack exchange,提问作者Nerdy Bunz
相关产品推荐
相关产品推荐

