RecyclerView分页场景中多次查询Room数据库Bitmap致应用崩溃,如何解决?
这个问题我太熟了——RecyclerView快速滚动时触发大量单次数据库查询,加上没利用好缓存,直接把IO线程甚至主线程堵死,导致卡顿甚至崩溃。咱们一步步来解决,从最容易见效的到核心优化:
1. 立刻启用Glide的缓存机制(最快见效)
你提到没启用Glide缓存,这简直是浪费Glide最大的优势!Glide默认自带内存和磁盘缓存,但如果你的glideRequestOptionsForCache无意中禁用了,得重新配置:
val glideRequestOptionsForCache = RequestOptions() .diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存原始图片和转换后的版本 .memoryCacheStrategy(MemoryCacheStrategy.ALL) // 启用内存缓存 .placeholder(R.drawable.black_color_rectangle)
这样滚动时,已经加载过的图片会直接从内存/磁盘缓存取,不用再反复查询数据库。
2. 批量查询图片,砍掉高频IO开销
100个条目发起100次数据库请求是卡顿的核心——单次查询的IO开销累加起来直接拖垮性能。我们改成批量查询:
第一步:修改DAO,增加批量查询方法
@Dao interface ImageHolderDatabaseDao { // 保留原有单条查询(作为兜底) @Query("SELECT * FROM $IMAGE_HOLDER_TABLE WHERE contentId = :contentId") suspend fun getImageHolder(contentId : String): ImageHolder // 新增批量查询方法,一次查多个contentId的图片 @Query("SELECT * FROM $IMAGE_HOLDER_TABLE WHERE contentId IN (:contentIds)") suspend fun getImageHolders(contentIds: List<String>): List<ImageHolder> }
第二步:在Paging加载时批量预加载图片
因为你用的是Paging 3,建议在PagingSource或者ViewModel层,当加载一页文本数据时,一次性提取所有contentId,批量查询对应的图片并存入内存缓存:
// 在ViewModel里定义内存缓存,用LruCache限制内存占用 private val imageMemoryCache = LruCache<String, Bitmap>(100) // 缓存100张图片,可根据内存调整 fun preloadImagesForCurrentPage(contentIds: List<String>) { viewModelScope.launch(Dispatchers.IO) { val imageHolders = imageHolderDatabaseDao.getImageHolders(contentIds) imageHolders.forEach { holder -> imageMemoryCache.put(holder.contentId, holder.imageBitmap) } } }
之后在Adapter的onBindViewHolder里,先从内存缓存取,取不到再发起单条查询(作为兜底)。
3. 优化协程使用,避免内存泄漏和主线程阻塞
你现在用自定义的CoroutineScope(Dispatchers.Main),很容易导致内存泄漏!应该改用生命周期绑定的协程Scope,同时明确把数据库操作放在IO线程:
修改Adapter里的图片加载逻辑
// 移除自定义Scope,改用ViewHolder的生命周期Scope private fun populatePhotoFromLocalDb( senderUserId: String, lifecycleScope: LifecycleCoroutineScope, binding: ItemBinding ) { lifecycleScope.launch(Dispatchers.IO) { // 先查内存缓存 val cachedBitmap = imageMemoryCache.get(senderUserId) if (cachedBitmap != null) { withContext(Dispatchers.Main) { loadImageIntoView(cachedBitmap, binding) } return@launch } // 缓存没有,查数据库 val imageHolder = imageHolderDatabaseDao.getImageHolder(senderUserId) imageMemoryCache.put(senderUserId, imageHolder.imageBitmap) // 切回主线程更新UI withContext(Dispatchers.Main) { loadImageIntoView(imageHolder.imageBitmap, binding) } } } private fun loadImageIntoView(bitmap: Bitmap, binding: ItemBinding) { Glide.with(binding.root.context) .load(bitmap) .apply(glideRequestOptionsForCache) .transition(DrawableTransitionOptions.withCrossFade(glideCrossFadeDuration)) .listener(glideFinishedLoadingListener(senderUserId, true, null)) .into(binding.image) }
在onBindViewHolder里调用时,传入ViewHolder的生命周期Scope:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { // ...绑定TextView逻辑 val item = getItem(position) holder.binding.lifecycleOwner?.let { lifecycleOwner -> populatePhotoFromLocalDb(item.contentId, lifecycleOwner.lifecycleScope, holder.binding) } }
4. 优化Bitmap存储,从根源减少内存占用
把Bitmap直接存在Room的ByteArray里是非常耗内存的——一张1080p的Bitmap可能占几MB,100张就是几百MB,很容易触发OOM。建议改成存储图片的本地文件路径:
修改ImageHolder数据类
@Entity(tableName = IMAGE_HOLDER_TABLE) data class ImageHolder( @PrimaryKey(autoGenerate = false) var contentId: String = "", var imagePath : String, // 存储图片在本地的绝对路径 var timeStamp : Long = System.currentTimeMillis() )
然后Glide直接加载文件路径,让Glide自己处理Bitmap的解码、复用和缓存:
Glide.with(binding.root.context) .load(File(imageHolder.imagePath)) .apply(glideRequestOptionsForCache) .into(binding.image)
5. RecyclerView基础优化补全
最后再补几个基础优化,减少不必要的性能损耗:
- 给RecyclerView设置
setHasFixedSize(true),避免每次数据变化都重新计算布局尺寸 - 确保ViewHolder的Binding是用
ItemViewBinding.inflate正确创建的,复用机制正常 - 如果图片尺寸固定,给ImageView设置固定的
width和height,避免Glide重复计算图片尺寸
内容的提问来源于stack exchange,提问作者Anudeep Ananth

