You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
    }
}

我的目标是追踪音频预加载的进度,现提出两个技术问题:

  1. 此处使用AtomicInteger是否属于过度设计?不使用的话是否安全?
  2. 如何观察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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 06:15:40