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

Jetpack Compose中LazyVerticalGrid高效加载9000张图片的优化

优化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+张图片

需求帮助

  1. 如何优化Jetpack Compose中大量图片的加载流程?
  2. 管理大型图片网格以减少UI卡顿有哪些最佳实践?
  3. 是否应采用预加载、缓存或并行加载来提升性能?

约束条件

  • 图片需在支持动态滚动的网格中展示
  • 方案需高效支持大量图片加载

优化方案

一、优化MediaStore查询与URI加载流程

  1. 改用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导致的内存压力。

  2. 优先加载缩略图而非原图
    直接加载原图会占用大量内存,可修改查询逻辑获取MediaStore提供的缩略图URI,点击时再加载原图:

    // 修改projection添加缩略图字段
    val projection = arrayOf(
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.THUMBNAIL_URI
    )
    

    或者通过MediaStore.Images.Thumbnails.queryThumbnail单独获取缩略图,大幅降低单张图片的内存占用。

二、优化图片加载与缓存(基于Coil)

  1. 自定义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
        )
    }
    
  2. 限制图片加载尺寸
    针对网格布局,显式指定图片加载尺寸,避免加载过大的原图:

    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(mediaUri)
            .size(Size(200, 200)) // 匹配网格item的实际尺寸
            .build(),
        // 其他参数不变
    )
    

三、LazyVerticalGrid滚动性能优化

  1. 为item设置唯一key
    添加key可以避免Compose在滚动时不必要的重组:

    items(mediaItems, key = { it }) { uri ->
        uri?.let { MediaItem(mediaUri = it) }
    }
    
  2. 减少item重组范围
    将MediaItem的参数设为不可变类型,或用remember缓存可复用内容,避免每次重组都重新创建资源:

    @Composable
    fun MediaItem(mediaUri: Uri) {
        val placeholderPainter = remember { painterResource(android.R.drawable.ic_menu_gallery) }
        AsyncImage(
            model = mediaUri,
            placeholder = placeholderPainter,
            // 其他参数不变
        )
    }
    

四、预加载与并行加载策略

  1. 结合Paging与Coil预加载
    Paging 3的prefetchDistance参数可以设置提前加载的页数,Coil会自动预加载可见区域附近的图片,让用户滚动时图片已加载完成。

  2. 控制并行加载数量
    限制Coil的并行加载数,避免同时加载过多图片导致UI卡顿:

    ImageLoader.Builder(context)
        .dispatcher(Dispatchers.IO.limitedParallelism(4)) // 限制为4个并行任务
        // 其他配置
        .build()
    

内容的提问来源于stack exchange,提问作者Anthony Humphreys

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 16:25:56