Android节拍器APP真机时间间隔不稳定问题求助
节拍器APP真机时间间隔不稳定问题解决
问题描述
我正在开发一款节拍器APP,需求是精准循环计数4拍,第一拍带重音。在Android Studio模拟器上运行正常,但在Galaxy J3-2018(Android v9)真机上,时间间隔完全不稳定,随机性很强。
尝试过Threads、Coroutines、Handlers,都没效果;也试过Timer对象,参考相关代码重构仍失败。
当前代码(模拟器运行正常):
timer = 5000 timeSignatureTop = 4 accentPlayed = false measureCount = 1 Thread { while (!stopButtonPressed) { Log.d("Thread running", "running") if (!accentPlayed) { mediaPlayerAccent!!.start() measureCount++ accentPlayed = true } else if (measureCount < timeSignatureTop) { mediaPlayerBeat!!.start() measureCount++ } else if (measureCount == timeSignatureTop) { mediaPlayerBeat!!.start() measureCount = 1 accentPlayed = false } Thread.sleep(timer.toLong()) } Log.d("Thread stopped", "stopped") return@Thread }.start()
核心问题分析
用Thread.sleep()控制节拍间隔的做法,容易被Android系统调度干扰——尤其是低性能设备,后台任务、进程优先级变化都会打乱sleep的精准度,导致时间间隔不稳定。另外,MediaPlayer的start()本身存在微小耗时,累积后会让节拍误差越来越大。
修复方案
方案1:使用ScheduledExecutorService保证固定速率执行
ScheduledExecutorService的scheduleAtFixedRate方法会自动调整执行时间,抵消任务本身的耗时,维持稳定的时间间隔,适合节拍器这种需要精准定时的场景。
优化后的代码示例:
import java.util.concurrent.Executors import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit // 全局变量 private val scheduler = Executors.newSingleThreadScheduledExecutor() private var scheduledFuture: ScheduledFuture<*>? = null var timer = 5000L // 直接用Long类型,避免类型转换耗时 val timeSignatureTop = 4 var accentPlayed = false var measureCount = 1 // 启动节拍器 fun startMetronome() { // 先终止之前的任务,避免重复启动 scheduledFuture?.cancel(false) // 固定速率执行:首次立即执行,之后每隔timer毫秒执行一次 scheduledFuture = scheduler.scheduleAtFixedRate({ when { !accentPlayed -> { mediaPlayerAccent?.start() measureCount++ accentPlayed = true } measureCount < timeSignatureTop -> { mediaPlayerBeat?.start() measureCount++ } measureCount == timeSignatureTop -> { mediaPlayerBeat?.start() measureCount = 1 accentPlayed = false } } }, 0, timer, TimeUnit.MILLISECONDS) } // 停止节拍器 fun stopMetronome() { scheduledFuture?.cancel(false) scheduledFuture = null } // Activity销毁时关闭调度器,避免内存泄漏 override fun onDestroy() { super.onDestroy() scheduler.shutdown() }
方案2:用SoundPool替代MediaPlayer(进一步降低延迟)
MediaPlayer适合播放长音频,节拍器的短节拍音用SoundPool更高效,播放延迟更低,能提升整体精准度。
示例代码:
import android.media.SoundPool private val soundPool = SoundPool.Builder().setMaxStreams(2).build() private var accentSoundId: Int = 0 private var beatSoundId: Int = 0 // 在Activity初始化时加载音频资源 fun loadBeatSounds() { accentSoundId = soundPool.load(this, R.raw.accent_beat, 1) beatSoundId = soundPool.load(this, R.raw.normal_beat, 1) } // 播放重音 fun playAccent() { soundPool.play(accentSoundId, 1f, 1f, 1, 0, 1f) } // 播放普通节拍 fun playNormalBeat() { soundPool.play(beatSoundId, 1f, 1f, 1, 0, 1f) } // 销毁时释放资源 override fun onDestroy() { super.onDestroy() soundPool.release() }
额外优化点
- 提前初始化音频资源:在APP启动或进入节拍器页面时,完成MediaPlayer或SoundPool的初始化与准备工作,避免播放时的临时初始化耗时。
- UI更新需切主线程:如果要在界面上显示当前节拍数,需用
runOnUiThread或协程切换到主线程执行更新操作,避免跨线程异常。
内容的提问来源于stack exchange,提问作者gab
相关产品推荐
相关产品推荐

