从文件夹加载图片至RecyclerView过慢,求快速加载优化方案
你的代码存在几个核心问题导致加载卡顿和重复加载:直接加载原图无压缩、同步遍历所有文件并加载Bitmap、无缓存机制、全量刷新列表。以下是针对性的优化建议:
1. 改用成熟的图片加载库(最关键优化)
不要手动用BitmapFactory.decodeFile加载图片,Glide、Coil这类库已经封装了异步加载、自动压缩、内存/磁盘缓存、生命周期绑定等核心能力,能解决90%的加载慢和重复加载问题。
以Kotlin友好的Coil为例,在RecyclerView的Adapter中直接加载文件路径:
// 先添加依赖(build.gradle) // implementation("io.coil-kt:coil:2.5.0") // ViewHolder中实现图片加载 class PictureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val imageView: ImageView = itemView.findViewById(R.id.image_view) fun bind(filePath: String) { imageView.load(filePath) { // 适配ImageView尺寸,自动压缩图片 size(imageView.width, imageView.height) placeholder(R.drawable.placeholder) // 加载占位图 error(R.drawable.error) // 加载失败图 // 启用内存和磁盘缓存(默认已开启) memoryCachePolicy(CachePolicy.ENABLED) diskCachePolicy(CachePolicy.ENABLED) } } }
Fragment切换返回时,图片会直接从缓存读取,无需重新解码。
2. 异步遍历图片文件,避免阻塞IO线程
你的代码同步遍历所有文件并加载Bitmap,会导致IO线程长时间被占用。应该先异步获取图片文件列表,只存储路径和文件名,不提前加载Bitmap:
suspend fun getImageFiles(): List<Picture> = withContext(Dispatchers.IO) { val path = Environment.getExternalStorageDirectory().toString() + "Here folder path" val directory = File(path) val imageExtensions = setOf("jpg", "jpeg", "png", "webp") // 过滤仅图片文件,避免加载无效资源 directory.listFiles()?.filter { file -> imageExtensions.contains(file.extension.lowercase()) }?.map { file -> // 只存路径和文件名,Bitmap交给加载库按需加载 Picture(null, file.absolutePath, file.name) } ?: emptyList() }
获取到文件列表后再更新Adapter,由加载库在列表滚动时按需加载图片。
3. 实现分页加载,避免一次性加载所有图片
如果文件夹中有数百上千张图片,一次性加载所有文件列表也会耗时。用Paging3实现分页加载,只加载当前可见区域附近的图片:
// 定义PagingSource,负责分页获取图片文件 class ImagePagingSource(private val folderPath: String) : PagingSource<Int, Picture>() { private val imageExtensions = setOf("jpg", "jpeg", "png", "webp") override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Picture> { val page = params.key ?: 0 val pageSize = params.loadSize return try { val directory = File(folderPath) val imageFiles = directory.listFiles()?.filter { imageExtensions.contains(it.extension.lowercase()) } ?: emptyList() val startIndex = page * pageSize val endIndex = minOf(startIndex + pageSize, imageFiles.size) val pageItems = imageFiles.subList(startIndex, endIndex).map { Picture(null, it.absolutePath, it.name) } LoadResult.Page( data = pageItems, prevKey = if (page == 0) null else page - 1, nextKey = if (endIndex >= imageFiles.size) null else page + 1 ) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, Picture>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey ?: state.closestPageToPosition(anchorPosition)?.nextKey } } }
配合PagingDataAdapter使用,自动处理分页和列表刷新。
4. 用DiffUtil优化列表刷新,避免全量刷新
不要调用notifyDataSetChanged(),改用DiffUtil计算数据差异,只更新变化的项:
class PictureAdapter : ListAdapter<Picture, PictureViewHolder>(DiffCallback()) { companion object { class DiffCallback : DiffUtil.ItemCallback<Picture>() { override fun areItemsTheSame(oldItem: Picture, newItem: Picture): Boolean { return oldItem.path == newItem.path // 用唯一路径判断是否为同一项 } override fun areContentsTheSame(oldItem: Picture, newItem: Picture): Boolean { return oldItem.fileName == newItem.fileName // 判断内容是否变化 } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PictureViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_picture, parent, false) return PictureViewHolder(view) } override fun onBindViewHolder(holder: PictureViewHolder, position: Int) { holder.bind(getItem(position).path) } }
更新数据时调用submitList(newList),DiffUtil会自动处理局部刷新,提升列表流畅度。
5. 绑定Fragment生命周期,避免无效加载
用图片加载库时绑定Fragment的生命周期,比如Coil的lifecycle(fragment.lifecycle),这样Fragment销毁时会自动取消未完成的加载请求,节省资源;切换回来时,缓存的图片会直接显示:
// 在Fragment的Adapter绑定中添加 imageView.load(filePath) { lifecycle(fragment.lifecycle) // 其他配置... }
6. 可选:手动Bitmap压缩(仅当无法使用加载库时)
如果必须手动加载Bitmap,一定要设置采样率压缩,避免加载原图占用大量内存:
fun decodeSampledBitmapFromFile(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true // 先获取图片尺寸,不加载Bitmap BitmapFactory.decodeFile(filePath, this) // 计算采样率,缩小图片尺寸 inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) inJustDecodeBounds = false // 开始加载压缩后的Bitmap } return BitmapFactory.decodeFile(filePath, options) } private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val height = options.outHeight val width = options.outWidth var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize }
加载时传入ImageView的宽高,压缩后再显示。
内容的提问来源于stack exchange,提问作者Anas Hafidi

