3. LazyVerticalGrid 与网格按需加载:固定网格、自适应网格、跨列复用、避免 OOM

3.1 原理概述

LazyVerticalGrid 是 Jetpack Compose 提供的垂直滚动网格容器。它与 LazyColumn 共享相同的懒加载机制:只有进入视口(viewport)的可组合项才会被合成、布局和绘制,未进入视口的项被回收并复用。这种按需加载策略可以显著降低内存占用,但在网格场景下存在特殊挑战:

  • 测量阶段差异:网格需要同时确定行数和列数,计算跨度(span)和交叉轴尺寸。
  • OOM 诱因:若网格项包含大图片、复杂图形或未绑定的数据源,仍可能因可见项过多或图片解码缓存爆炸导致内存溢出。
  • 复用粒度:LazyVerticalGrid 的 key 管理与行内复用对滑动性能至关重要。
关键结论:LazyVerticalGrid 本身就是按需加载的最佳实践,但开发者需要配合列数策略、跨度复用及内存控制才能避免 OOM。

3.2 核心 API 与参数

参数 类型 说明
columns GridCells 固定列数(Fixed)或自适应列数(Adaptive)
contentPadding PaddingValues 网格内容边缘间距,影响滚动边界
modifier Modifier 布局修饰,如 fillMaxSize()
userScrollEnabled Boolean 是否允许用户滚动,默认 true
content LazyGridScope 扩展 网格项描述,包含 item、items 及 span 扩展

3.3 固定网格与自适应网格

3.3.1 GridCells.Fixed

指定固定列数,例如 2 列网格:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    modifier = Modifier.fillMaxSize()
) {
    items(100) { index ->
        Text("Item $index", modifier = Modifier.padding(8.dp))
    }
}

3.3.2 GridCells.Adaptive

根据可用宽度自动计算出列数,每列至少 minSize 像素:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp),
    modifier = Modifier.fillMaxSize()
) { ... }

当屏幕宽度为 360dp 时,列数计算为 floor(360 / 128) = 2;宽度 500dp 时变为 3 列。自适应网格能兼容平板和手机。

注意:Adaptive 的 minSize 是建议值,实际列宽可能略大于该值,因为 Compose 会均匀分配剩余空间。需要精确控制列宽时请使用 Fixed。

3.4 跨列复用(span)

通过 grid.span 扩展函数让一个 item 占据多列,常用于头部或宽卡片:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    modifier = Modifier.fillMaxSize()
) {
    // 占据全宽的行
    item(span = { GridItemSpan(maxLineSpan) }) {
        Text("Header", modifier = Modifier.fillMaxWidth())
    }
    // 普通两列子项
    items(10) { index ->
        Card(modifier = Modifier.padding(4.dp)) { Text("Item $index") }
    }
}

跨列复用与 key 参数配合,确保避免重新创建浪费:

items(data, key = { it.id }) { item -> ... }

3.5 避免 OOM 的策略

  1. 图片分页加载:不要一次性加载所有图片,使用 SubcomposeLayout 或第三方库(Coil/Glide)的懒加载。
  2. 内存缓存上限:设置图片解码缓存(如 Coil 的 MemoryCache)大小,避免占用过多堆内存。
  3. 数据源分页:结合 Paging 3 库,仅加载当前可见页数据,远离视口的数据自动回收。
  4. 避免大对象在 item 中持久持有:Compose 的 remember 和 derivedStateOf 应只保留轻量计算,不要缓存大量 Bitmap。
  5. 显式 key 与 remove:传递稳定且唯一的 key,帮助 Compose 判断哪些 item 可以复用。

性能调优步骤

  1. 使用 Layout Inspector 检查网格是否触发不必要的重组。
  2. 在 LazyVerticalGrid 的 content 中打印日志,观察 item 是否重复创建。
  3. 使用 Profiler 查看内存中分配的对象及 Bitmap 区域。
  4. 若图片过多,降级为 Image 的 contentScale = Crop 并设置 blurRadius = 0 关闭模糊。
LazyVerticalGrid 按需加载流程 数据源(Data) List / PagingData 可见区域计算 视口 + 预取偏移 列数 / 跨度决定 Fixed 2 / Adaptive 128dp 复用池(ReusePool) item 回收 & 重绑 合成 & 布局 & 绘制 只渲染视口内 item 🧠 避免 OOM 检查点:图片缓存 / Paging / 避免大对象持有 使用 Profiler 监控 Native 内存,设置 ImageLoader memoryCache 上限

3.6 常见错误与解决方案

错误表现 根本原因 解决方案
网格滑动时卡顿 item 中做了 I/O 或高开销计算 使用 LaunchedEffect 异步处理;启用 contentPadding 减少渲染压力
内存暴增直至 OOM 图片原始大小未缩放,所有 item 同时解码 设置 ImageRequest 的 size;使用 MemoryCache 限制总大小
item 跨列后布局错位 span 返回的 GridItemSpan 超出 maxLineSpan 确保 span 在 item 或 items 内正确使用,不要手动改写 span 计数
自适应网格在横竖屏切换时列数突变 未根据配置变化重置网格状态 对 LazyVerticalGrid 使用 key(screenWidth) 强制重置布局

3.7 调试与部署建议

  • 使用 Android Studio Layout Inspector 选中 LazyVerticalGrid,观察“LazyGridItemInfo”面板中可见 item 数量是否合理。
  • 在构建配置开启 isDebug 时,在 compositionLocalOf 中记录 item 创建次数。
  • 部署前通过 Monkey 测试或手动快速滚动验证内存稳定,控制循环回收次数。
  • 对于图片密集网格,使用 coil 的 CrossfadePlugin 时注意关闭预加载,避免非可视区域解码。
实战经验:我们在 10,000 项的网格中,通过 GridCells.Adaptive(minSize = 120.dp) + Paging 3 + Coil memoryCache(maxSize = 50 MB) 实现了流畅滚动且无 OOM 崩溃。关键一步是关闭了 Coil 的 DiskCache 预取,仅保留内存缓存。