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

Android RecyclerView Adapter中避免图片API重复请求的方法

解决RecyclerView滚动重复调用图片API的问题

核心问题

RecyclerView的ViewHolder会被复用,每次滚动到可见项都会触发bind方法,当前代码每次bind都会发起API请求,没有任何缓存判断,导致重复请求。

解决方案

方案一:内存缓存(快速实现)

在Adapter内部维护一个内存缓存,存储图片ID与对应字节数组的映射,每次请求前先检查缓存:

class MyAdapter @Inject constructor(@ApplicationContext private val context: Context) :
    RecyclerView.Adapter<MyAdapter.ViewHolder>() {

    private var items = emptyList<Result>()
    private val userToken by lazy { GoodPrefs.getInstance().getString(USER_TOKEN, "") }
    // 新增内存缓存:图片ID -> 图片字节数组
    private val imageCache = mutableMapOf<String, ByteArray>()

    // ... 其他原有代码不变 ...

    inner class ViewHolder(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
        // 新增:管理协程Job,避免重复请求
        private var imageJob: Job? = null

        @SuppressLint("SetTextI18n")
        fun bind(item: Result) {
            binding.apply {
                //Title
                titleTxt.text = item.title?.replace("\"","\"")
                //File
                if (item.file != null) {
                    when (item.file.type) {
                        IMAGE -> {
                            thumbImg.isVisible = true
                            val imageId = item.file.id!!
                            // 先检查缓存
                            imageCache[imageId]?.let { cachedBytes ->
                                thumbLoading.isVisible = false
                                Glide.with(context)
                                    .load(cachedBytes)
                                    .override(900, 400)
                                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                                    .into(thumbImg)
                                return // 直接返回,不发起请求
                            }

                            // 缓存没有,发起请求
                            thumbLoading.isVisible = true
                            // 先取消之前未完成的请求
                            imageJob?.cancel()
                            imageJob = CoroutineScope(Dispatchers.IO).launch {
                                ApiClient.getInstance().apisUseCase().getAvatarImage(userToken, imageId)
                                    .applyIoScheduler()
                                    .subscribe({
                                        thumbLoading.isVisible = false
                                        if (it.isSuccessful && it.code() == 200 && it.body() != null) {
                                            val decodedBytes = Base64.decode(it.body()!!.data!!.file, Base64.DEFAULT)
                                            // 存入缓存
                                            imageCache[imageId] = decodedBytes
                                            // 切换到主线程更新UI
                                            withContext(Dispatchers.Main) {
                                                Glide.with(context)
                                                    .load(decodedBytes)
                                                    .override(900, 400)
                                                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                                                    .into(thumbImg)
                                            }
                                        }
                                    }, {
                                        thumbLoading.isVisible = false
                                    })
                            }
                        }
                    }
                } else {
                    // 没有图片时隐藏控件
                    thumbImg.isVisible = false
                    thumbLoading.isVisible = false
                }
            }
        }

        // ViewHolder被回收时取消请求
        override fun onViewRecycled() {
            super.onViewRecycled()
            imageJob?.cancel()
        }
    }

    // ... 其他原有代码不变 ...
}

方案二:修改数据模型缓存(更持久)

如果希望缓存和数据列表绑定,比如数据刷新时保留已加载的图片,可以给Result类新增一个字段存储缓存的图片字节:

// 假设你的Result类结构如下,新增cachedImageBytes字段
data class Result(
    val title: String?,
    val file: File?,
    // 新增缓存字段
    var cachedImageBytes: ByteArray? = null
) {
    // 注意:ByteArray需要重写equals和hashCode,避免DiffUtil判断错误
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Result

        if (title != other.title) return false
        if (file != other.file) return false
        if (cachedImageBytes != null) {
            if (other.cachedImageBytes == null) return false
            if (!cachedImageBytes.contentEquals(other.cachedImageBytes)) return false
        } else if (other.cachedImageBytes != null) return false

        return true
    }

    override fun hashCode(): Int {
        var result = title?.hashCode() ?: 0
        result = 31 * result + (file?.hashCode() ?: 0)
        result = 31 * result + (cachedImageBytes?.contentHashCode() ?: 0)
        return result
    }
}

然后修改Adapter的bind逻辑:

inner class ViewHolder(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
    private var imageJob: Job? = null

    @SuppressLint("SetTextI18n")
    fun bind(item: Result) {
        binding.apply {
            //Title
            titleTxt.text = item.title?.replace("\"","\"")
            //File
            if (item.file != null) {
                when (item.file.type) {
                    IMAGE -> {
                        thumbImg.isVisible = true
                        // 先检查item自身的缓存
                        item.cachedImageBytes?.let { cachedBytes ->
                            thumbLoading.isVisible = false
                            Glide.with(context)
                                .load(cachedBytes)
                                .override(900, 400)
                                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                                .into(thumbImg)
                            return
                        }

                        thumbLoading.isVisible = true
                        imageJob?.cancel()
                        imageJob = CoroutineScope(Dispatchers.IO).launch {
                            ApiClient.getInstance().apisUseCase().getAvatarImage(userToken, item.file.id!!)
                                .applyIoScheduler()
                                .subscribe({
                                    thumbLoading.isVisible = false
                                    if (it.isSuccessful && it.code() == 200 && it.body() != null) {
                                        val decodedBytes = Base64.decode(it.body()!!.data!!.file, Base64.DEFAULT)
                                        // 存入item的缓存字段
                                        item.cachedImageBytes = decodedBytes
                                        withContext(Dispatchers.Main) {
                                            Glide.with(context)
                                                .load(decodedBytes)
                                                .override(900, 400)
                                                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                                                .into(thumbImg)
                                        }
                                    }
                                }, {
                                    thumbLoading.isVisible = false
                                })
                        }
                    }
                }
            } else {
                thumbImg.isVisible = false
                thumbLoading.isVisible = false
            }
        }
    }

    override fun onViewRecycled() {
        super.onViewRecycled()
        imageJob?.cancel()
    }
}

方案三:利用Glide的自定义缓存Key(推荐)

Glide本身有强大的缓存机制,但直接加载字节数组时,默认缓存Key基于字节内容,无法关联图片ID。可以自定义Model和ModelLoader,让Glide用图片ID作为缓存Key,自动处理缓存:

// 1. 定义图片ID的Model类
data class ImageIdModel(val imageId: String, val token: String)

// 2. 自定义ModelLoader,负责根据ImageIdModel加载图片字节数组
class ImageIdModelLoader(private val api: ApisUseCase) : ModelLoader<ImageIdModel, ByteArray> {
    override fun handles(model: ImageIdModel): Boolean = true

    override fun buildLoadData(
        model: ImageIdModel,
        width: Int,
        height: Int,
        options: Options
    ): LoadData<ByteArray>? {
        return LoadData(
            ObjectKey(model.imageId), // 用图片ID作为缓存Key
            ImageIdFetcher(api, model)
        )
    }

    // 自定义Fetcher,负责发起API请求
    class ImageIdFetcher(private val api: ApisUseCase, private val model: ImageIdModel) : DataFetcher<ByteArray> {
        private var disposable: Disposable? = null

        override fun loadData(priority: Priority, callback: DataCallback<in ByteArray>) {
            disposable = api.getAvatarImage(model.token, model.imageId)
                .applyIoScheduler()
                .subscribe({
                    if (it.isSuccessful && it.code() == 200 && it.body() != null) {
                        val decodedBytes = Base64.decode(it.body()!!.data!!.file, Base64.DEFAULT)
                        callback.onDataReady(decodedBytes)
                    } else {
                        callback.onLoadFailed(Exception("Failed to load image"))
                    }
                }, {
                    callback.onLoadFailed(it)
                })
        }

        override fun cleanup() {
            disposable?.dispose()
        }

        override fun cancel() {
            disposable?.dispose()
        }

        override fun getDataClass(): Class<ByteArray> = ByteArray::class.java

        override fun getDataSource(): DataSource = DataSource.REMOTE
    }

    // 注册ModelLoader的Factory
    class Factory @Inject constructor(private val api: ApisUseCase) : ModelLoaderFactory<ImageIdModel, ByteArray> {
        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageIdModel, ByteArray> {
            return ImageIdModelLoader(api)
        }

        override fun teardown() {}
    }
}

在Application中初始化Glide并注册Factory:

// 在Application的onCreate方法中
override fun onCreate() {
    super.onCreate()
    Glide.get(this).registry.append(
        ImageIdModel::class.java,
        ByteArray::class.java,
        ImageIdModelLoader.Factory(ApiClient.getInstance().apisUseCase())
    )
}

最后修改Adapter的bind逻辑,直接用Glide加载ImageIdModel:

inner class ViewHolder(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
    @SuppressLint("SetTextI18n")
    fun bind(item: Result) {
        binding.apply {
            //Title
            titleTxt.text = item.title?.replace("\"","\"")
            //File
            if (item.file != null) {
                when (item.file.type) {
                    IMAGE -> {
                        thumbImg.isVisible = true
                        thumbLoading.isVisible = true
                        // 直接加载ImageIdModel,Glide会自动处理缓存
                        Glide.with(context)
                            .load(ImageIdModel(item.file.id!!, userToken))
                            .override(900, 400)
                            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                            .listener(object : RequestListener<Drawable> {
                                override fun onLoadFailed(
                                    e: GlideException?,
                                    model: Any?,
                                    target: Target<Drawable>?,
                                    isFirstResource: Boolean
                                ): Boolean {
                                    thumbLoading.isVisible = false
                                    return false
                                }

                                override fun onResourceReady(
                                    resource: Drawable?,
                                    model: Any?,
                                    target: Target<Drawable>?,
                                    dataSource: DataSource?,
                                    isFirstResource: Boolean
                                ): Boolean {
                                    thumbLoading.isVisible = false
                                    return false
                                }
                            })
                            .into(thumbImg)
                    }
                }
            } else {
                thumbImg.isVisible = false
                thumbLoading.isVisible = false
            }
        }
    }
}

这个方案完全利用Glide的内存+磁盘缓存机制,无需手动管理缓存,还能自动处理请求取消、复用等问题。

额外注意事项

  • 所有方案都加入了请求取消逻辑,避免ViewHolder复用时多个请求同时进行,减少资源浪费和内存泄漏。
  • 使用方案二时,Result类的equals和hashCode必须包含cachedImageBytes字段,否则DiffUtil无法正确识别数据变化。
  • 若图片较大,内存缓存可能占用过多内存,建议结合磁盘缓存(如方案三)或限制内存缓存大小。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 07:02:07