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

Kotlin协程并发场景下,本地图片存储的线程安全问题咨询

问题解答

核心疑问解答

  1. 是否需要处理并发读写问题?
    是的,你当前的代码确实存在并发读写风险。因为async块默认会继承父协程的调度器,而Repository中的下载操作大概率会切换到IO线程池执行(Firebase Storage的挂起函数通常在IO线程运行)。多个async任务可能在IO线程池的不同线程中同时调用savePicture,而MutableList本身并非线程安全容器,并发添加元素可能导致列表结构损坏、数据丢失或异常。

  2. 使用ReentrantReadWriteLock是否合理?
    是可行方案,但并非最适配Kotlin协程的选择。ReentrantReadWriteLock是线程级锁,会阻塞整个线程;而协程的核心优势是轻量调度,用线程锁会浪费协程的资源优势,更推荐使用协程友好的Mutex来处理并发。


代码优化方案

1. 替换线程锁为协程Mutex(本地存储层)

将ReentrantReadWriteLock替换为Kotlin协程的Mutex,它会挂起协程而非阻塞线程,更符合协程的设计理念:

class LocalPlantPictureStoreImpl : LocalPlantPictureStore {
    private val mutex = Mutex()
    private val plantPictureList: MutableList<Pair<String, ByteArray>> = mutableListOf()

    override suspend fun savePicture(plantPicture: Pair<String, ByteArray>) {
        mutex.withLock {
            plantPictureList.add(plantPicture)
        }
    }

    override suspend fun getPicture(plantId: String): Pair<String, ByteArray>? {
        return mutex.withLock {
            plantPictureList.firstOrNull { it.first == plantId }
        }
    }
}

注意:需要将接口方法改为suspend类型,因为Mutex.withLock是挂起函数。

2. 优化Repository逻辑(避免重复下载)

在Repository中先检查本地存储,存在则直接返回,无需重复下载:

suspend fun getProfilePicture(plantId: String): Pair<String, ByteArray> {
    // 优先从本地缓存获取
    localPicture.getPicture(plantId)?.let {
        return it
    }
    // 本地无缓存时才发起下载
    val profilePicture = remotePicture.plantProfilePicture(
        plantPicturePath = profilePicturePath(currentUser, plantId),
        bufferSize = currentDownloadSize
    )
    val result = Pair(plantId, profilePicture)
    localPicture.savePicture(result)
    return result
}

3. 修正ViewModel逻辑(实现单任务完成即展示)

你当前的代码会等待所有下载完成后才更新RecyclerView,不符合“每个异步任务完成后展示图片”的需求。修改为每个任务完成后立即更新列表:

// 建议将LiveData替换为StateFlow,更适配协程且线程安全
private val _profilePictureList = MutableStateFlow<List<Pair<String, ByteArray>>>(emptyList())
val profilePictureList: StateFlow<List<Pair<String, ByteArray>>> = _profilePictureList

init {
    viewModelScope.launch {
        plantList.forEach { plant ->
            launch {
                val picture = plantScreenRepository.getProfilePicture(plant.plantId)
                // 原子性更新列表,避免并发修改问题
                _profilePictureList.update { currentList ->
                    currentList + picture
                }
            }
        }
    }
}

使用StateFlow的update方法可以保证列表更新的原子性,无需额外处理并发。


额外说明

  • 内存缓存仅在ViewModel生命周期内有效,若进程被杀会丢失数据。如果需要持久化缓存,建议将图片保存到本地文件系统,而非内存列表。
  • 屏幕旋转时,ViewModel会被保留,重新创建的Fragment/Activity可以直接从ViewModel的profilePictureList获取已下载的图片,无需重新发起请求。

内容的提问来源于stack exchange,提问作者No_Name

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 02:02:13