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 的策略
- 图片分页加载:不要一次性加载所有图片,使用
SubcomposeLayout或第三方库(Coil/Glide)的懒加载。 - 内存缓存上限:设置图片解码缓存(如 Coil 的
MemoryCache)大小,避免占用过多堆内存。 - 数据源分页:结合 Paging 3 库,仅加载当前可见页数据,远离视口的数据自动回收。
- 避免大对象在 item 中持久持有:Compose 的
remember和derivedStateOf应只保留轻量计算,不要缓存大量 Bitmap。 - 显式 key 与 remove:传递稳定且唯一的
key,帮助 Compose 判断哪些 item 可以复用。
性能调优步骤
- 使用 Layout Inspector 检查网格是否触发不必要的重组。
- 在
LazyVerticalGrid的content中打印日志,观察 item 是否重复创建。 - 使用 Profiler 查看内存中分配的对象及 Bitmap 区域。
- 若图片过多,降级为
Image的contentScale = Crop并设置blurRadius = 0关闭模糊。
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 预取,仅保留内存缓存。