Jetpack Compose中LazyVerticalGrid高效加载9000张图片的优化
问题背景
我正在用Jetpack Compose开发Android应用,需要在LazyVerticalGrid中展示设备存储里多达9000张图片。当前实现能运行,但性能和加载速度很差:用了批量加载,但UI偶尔卡顿,部分图片加载不及时,图片库越大问题越明显。
现有简化实现代码
@Composable fun MediaGridScreen(context: Context) { val mediaList = remember { mutableStateListOf<Uri>() } var lastLoadedIndex by remember { mutableStateOf(0) } val batchSize = 100 // Load batches of media URIs LaunchedEffect(lastLoadedIndex) { val newMedia = loadMediaFromDevice(context.contentResolver, lastLoadedIndex, batchSize) if (newMedia.isNotEmpty()) { mediaList.addAll(newMedia) lastLoadedIndex += newMedia.size } } LazyVerticalGrid( columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize() ) { items(mediaList.size) { index -> MediaItem(mediaUri = mediaList[index]) } } } @Composable fun MediaItem(mediaUri: Uri) { Image( painter = rememberAsyncImagePainter( model = mediaUri, placeholder = painterResource(android.R.drawable.ic_menu_gallery) ), contentDescription = null, modifier = Modifier .padding(4.dp) .aspectRatio(1f) .fillMaxWidth(), contentScale = ContentScale.Crop ) } fun loadMediaFromDevice(contentResolver: ContentResolver, startIndex: Int, batchSize: Int): List<Uri> { val mediaList = mutableListOf<Uri>() val queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI val projection = arrayOf(MediaStore.Images.Media._ID) val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC LIMIT $batchSize OFFSET $startIndex" val cursor = contentResolver.query(queryUri, projection, null, null, sortOrder) cursor?.use { val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) while (it.moveToNext()) { val id = it.getLong(idColumn) val contentUri = Uri.withAppendedPath(queryUri, id.toString()) mediaList.add(contentUri) } } return mediaList }
已尝试方案
- 用MediaStore批量加载100张图片URI,加载完成后更新
LazyVerticalGrid - 采用Coil的
rememberAsyncImagePainter处理图片加载 - 为未加载完成的图片显示占位符
遇到的问题
- 图片数量增多时滚动性能下降
- 部分图片加载耗时过长,导致UI响应迟缓
- 即便采用批量加载,应用仍难以高效处理9000+张图片
需求帮助
- 如何优化Jetpack Compose中大量图片的加载流程?
- 管理大型图片网格以减少UI卡顿有哪些最佳实践?
- 是否应采用预加载、缓存或并行加载来提升性能?
约束条件
- 图片需在支持动态滚动的网格中展示
- 方案需高效支持大量图片加载
优化方案
一、优化MediaStore查询与URI加载流程
改用Paging 3库处理分页加载
手动维护批量加载逻辑容易引发内存波动和UI阻塞,Paging 3会自动处理分页、预加载和内存管理,适配滚动行为。核心示例代码:// 定义PagingSource class MediaPagingSource(private val contentResolver: ContentResolver) : PagingSource<Int, Uri>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Uri> { val startIndex = params.key ?: 0 val pageSize = params.loadSize val mediaList = mutableListOf<Uri>() val queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI val projection = arrayOf(MediaStore.Images.Media._ID) val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC LIMIT $pageSize OFFSET $startIndex" return try { contentResolver.query(queryUri, projection, null, null, sortOrder)?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) mediaList.add(Uri.withAppendedPath(queryUri, id.toString())) } } LoadResult.Page( data = mediaList, prevKey = if (startIndex == 0) null else startIndex - pageSize, nextKey = if (mediaList.size == pageSize) startIndex + pageSize else null ) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, Uri>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey ?: state.closestPageToPosition(anchorPosition)?.nextKey } } } // 在Compose中集成Paging @Composable fun MediaGridScreen(context: Context) { val pagingConfig = PagingConfig(pageSize = 100, prefetchDistance = 200) val mediaPager = remember { Pager(pagingConfig) { MediaPagingSource(context.contentResolver) } } val mediaItems = mediaPager.flow.collectAsLazyPagingItems() LazyVerticalGrid(columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize()) { items(mediaItems, key = { it }) { uri -> uri?.let { MediaItem(mediaUri = it) } } } }Paging 3会根据滚动位置自动预加载下一页,避免一次性加载大量URI导致的内存压力。
优先加载缩略图而非原图
直接加载原图会占用大量内存,可修改查询逻辑获取MediaStore提供的缩略图URI,点击时再加载原图:// 修改projection添加缩略图字段 val projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.THUMBNAIL_URI )或者通过
MediaStore.Images.Thumbnails.queryThumbnail单独获取缩略图,大幅降低单张图片的内存占用。
二、优化图片加载与缓存(基于Coil)
自定义Coil缓存策略
配置合理的内存和磁盘缓存,减少重复加载并控制内存占用:val imageLoader = ImageLoader.Builder(context) .memoryCache { MemoryCache.Builder(context) .maxSizePercent(0.25) // 占用应用25%的内存 .build() } .diskCache { DiskCache.Builder() .directory(context.cacheDir.resolve("image_cache")) .maxSizeBytes(512 * 1024 * 1024) // 512MB磁盘缓存 .build() } .allowHardware(true) // 启用硬件位图,降低CPU占用 .crossfade(true) .build() // 在App级别提供ImageLoader CompositionLocalProvider(LocalImageLoader provides imageLoader) { // 应用Compose内容 }同时推荐使用
AsyncImage替代rememberAsyncImagePainter,它内置了更多性能优化:@Composable fun MediaItem(mediaUri: Uri) { AsyncImage( model = mediaUri, placeholder = painterResource(android.R.drawable.ic_menu_gallery), contentDescription = null, modifier = Modifier .padding(4.dp) .aspectRatio(1f) .fillMaxWidth(), contentScale = ContentScale.Crop ) }限制图片加载尺寸
针对网格布局,显式指定图片加载尺寸,避免加载过大的原图:AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(mediaUri) .size(Size(200, 200)) // 匹配网格item的实际尺寸 .build(), // 其他参数不变 )
三、LazyVerticalGrid滚动性能优化
为item设置唯一key
添加key可以避免Compose在滚动时不必要的重组:items(mediaItems, key = { it }) { uri -> uri?.let { MediaItem(mediaUri = it) } }减少item重组范围
将MediaItem的参数设为不可变类型,或用remember缓存可复用内容,避免每次重组都重新创建资源:@Composable fun MediaItem(mediaUri: Uri) { val placeholderPainter = remember { painterResource(android.R.drawable.ic_menu_gallery) } AsyncImage( model = mediaUri, placeholder = placeholderPainter, // 其他参数不变 ) }
四、预加载与并行加载策略
结合Paging与Coil预加载
Paging 3的prefetchDistance参数可以设置提前加载的页数,Coil会自动预加载可见区域附近的图片,让用户滚动时图片已加载完成。控制并行加载数量
限制Coil的并行加载数,避免同时加载过多图片导致UI卡顿:ImageLoader.Builder(context) .dispatcher(Dispatchers.IO.limitedParallelism(4)) // 限制为4个并行任务 // 其他配置 .build()
内容的提问来源于stack exchange,提问作者Anthony Humphreys

