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
相关产品推荐
相关产品推荐

