Compose中SoundPool加载音频:AtomicInteger必要性与状态观察
我是Compose及Android开发的新手,在构建一个Compose玩具项目时遇到了问题。我需要从assets中加载约100个短音频,通过SoundPool进行播放。SoundPool.load()是异步加载的(否则就不需要回调了)。
ViewModel中的代码如下:
private val TAG = javaClass.simpleName private val soundPool: SoundPool private val soundsInProgress = AtomicInteger(0) var sounds: List<Sound> by mutableStateOf(listOf()) private set init { val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() soundPool = SoundPool.Builder() .setMaxStreams(3) .setAudioAttributes(audioAttributes) .build() loadSoundsFromAssets() } private fun loadSoundsFromAssets() { val assetManager = getApplication<MyApp>().applicationContext.assets val sounds = assetManager.list("sounds/")!!.map(this::parseSound) soundsInProgress.set(sounds.size) soundPool.setOnLoadCompleteListener { _, id, status -> if (status == 0) { val left = soundsInProgress.decrementAndGet() Log.i(TAG, "Loaded 1 sound, $left in progress") if (left == 0) { sounds = sounds } } else { Log.e(TAG, "Could not load sound $id, status is $status") } } sounds.forEach { val soundId = soundPool.load(assetManager.openFd("sounds/" + it.fileName), 0) it.soundId = soundId } }
我的目标是追踪音频预加载的进度,现提出两个技术问题:
- 此处使用AtomicInteger是否属于过度设计?不使用的话是否安全?
- 如何观察
soundsInProgress的变化?MutableState无法正常工作。
1. AtomicInteger是否过度设计?
不是过度设计,完全有必要使用。
原因是SoundPool.setOnLoadCompleteListener的回调在后台线程执行,批量加载100个音频时会有多个回调线程同时修改计数。如果用普通Int变量,执行--count这类操作会出现竞态条件:多个线程同时读取、修改同一个值,导致计数结果不准确(比如两次自减操作最后只生效一次),最终会导致加载完成的判断(left == 0)出错。
AtomicInteger的decrementAndGet()是原子操作,能保证多线程环境下的计数修改线程安全,不会出现竞态问题,所以这里的使用是合理的。
2. 如何观察soundsInProgress的变化?
MutableState无法正常工作的核心原因是:SoundPool的回调在后台线程,而Compose的MutableState要求必须在主线程更新其值,直接在后台线程修改会抛出异常或状态不更新。
这里有两种可行方案:
方案一:主线程更新MutableState
把soundsInProgress改成Compose的MutableState,在回调中切换到主线程更新状态:
// ViewModel中定义状态 private val _soundsInProgress = mutableStateOf(0) val soundsInProgress: State<Int> = _soundsInProgress // 对外暴露不可变State private fun loadSoundsFromAssets() { // ...其他代码不变 _soundsInProgress.value = sounds.size // 初始化计数 soundPool.setOnLoadCompleteListener { _, id, status -> if (status == 0) { // 切换到主线程更新状态 viewModelScope.launch(Dispatchers.Main) { _soundsInProgress.value -= 1 val left = _soundsInProgress.value Log.i(TAG, "Loaded 1 sound, $left in progress") if (left == 0) { sounds = sounds } } } else { Log.e(TAG, "Could not load sound $id, status is $status") } } // ...其他代码不变 }
在Compose中直接观察:
// 组合函数中 val progress by viewModel.soundsInProgress Text("加载进度:${viewModel.sounds.size - progress}/${viewModel.sounds.size}")
方案二:使用StateFlow(更适合数据流场景)
用StateFlow管理计数,它支持线程安全的更新,无需额外切换线程,在Compose中通过collectAsState()观察:
// ViewModel中定义StateFlow private val _soundsInProgress = MutableStateFlow(0) val soundsInProgress: StateFlow<Int> = _soundsInProgress // 对外暴露不可变StateFlow private fun loadSoundsFromAssets() { // ...其他代码不变 _soundsInProgress.value = sounds.size // 初始化计数 soundPool.setOnLoadCompleteListener { _, id, status -> if (status == 0) { // StateFlow支持后台线程安全更新 _soundsInProgress.update { it - 1 } val left = _soundsInProgress.value Log.i(TAG, "Loaded 1 sound, $left in progress") if (left == 0) { // 更新sounds需要主线程,因为MutableState要求主线程 viewModelScope.launch(Dispatchers.Main) { sounds = sounds } } } else { Log.e(TAG, "Could not load sound $id, status is $status") } } // ...其他代码不变 }
在Compose中观察:
// 组合函数中 val progress by viewModel.soundsInProgress.collectAsState() Text("加载进度:${viewModel.sounds.size - progress}/${viewModel.sounds.size}")
内容的提问来源于stack exchange,提问作者Sebastian Lore

