tablayout列表,左右滑动,有很多页面,怎么优化性能

83 阅读4分钟

0) 先选路线(很关键)

  • 页数 ≤ 10:直接 ViewPager2 + FragmentStateAdapter,常规优化即可。

  • 页数 10 ~ 50:仍可 ViewPager2,但务必只缓存少量页面 + 共享资源(见 §2)。

  • 页数 ≫ 50(百级)不要每页一个 Fragment。改为:

    • 方案 A:虚拟化分页(推荐) ——用“3 页滚动复用池”(环形复用 3 个 Fragment/自定义 View),根据当前位置重新绑定数据

    • 方案 B:单页 + 分段列表——一个 Fragment + 一个 RecyclerView,Tab 只是锚点/筛选,跳转到相应 section(Sticky Header/索引)。

      两个方案都避免“为每个 tab 保持一个页面实例”。


1) 如果必须用 ViewPager2(≤ ~50 页)

1.1 适配器必须选对

  • 用 FragmentStateAdapter(VP2)/ FragmentStatePagerAdapter(VP1) ,不要再用 FragmentPagerAdapter(它会常驻所有 Fragment,内存炸)。
  • 设置稳定 ID,方便增删 Tab 不触发全重建
class MyAdapter(
  fa: FragmentActivity,
  private var categories: List<Category>
) : FragmentStateAdapter(fa) {
  override fun getItemCount() = categories.size
  override fun createFragment(pos: Int) = PageFragment.new(categories[pos].id)

  override fun getItemId(pos: Int) = categories[pos].id.hashCode().toLong()
  override fun containsItem(id: Long) = categories.any { it.id.hashCode().toLong() == id }
}

1.2 控制保活与预取

viewPager.offscreenPageLimit = 1            // 仅左右各 1 页
(viewPager.getChildAt(0) as RecyclerView).overScrollMode = View.OVER_SCROLL_NEVER

不要把 offscreenPageLimit 拉大;大了=内存/CPU 压力陡增。

1.3 懒加载 & 生命周期

  • 在页面 onViewCreated 里只做轻量初始化;真正的数据加载放到**“可见后”**:
viewLifecycleOwner.lifecycleScope.launch {
  repeatOnLifecycle(Lifecycle.State.RESUMED) {   // 仅当前页 RESUMED 时跑
    viewModel.loadIfNeeded()
  }
}

VP2 会把当前页设为 RESUMED,相邻页 STARTED/CREATED;利用这个特性做懒加载。

1.4 每页里通常是列表:把可复用做到极致

  • 共享 RecycledViewPool(多页同结构的 RecyclerView 复用同一池):
val sharedPool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VIEW_TYPE_ITEM, 50)
}
class PageFragment : Fragment(R.layout.page) {
  override fun onViewCreated(v: View, s: Bundle?) {
    rv.setRecycledViewPool(sharedPool)
    rv.setHasFixedSize(true)
    (rv.layoutManager as? LinearLayoutManager)?.apply {
      isItemPrefetchEnabled = true
      initialPrefetchItemCount = 8
    }
  }
}
  • 避免嵌套滚动:页内列表尽量 isNestedScrollingEnabled = false,由父层处理;或使用 app:layout_behavior 正确接入 nested scroll。

  • 图片/视频按需加载、滑动时暂停解码/播放(Coil/Glide setPauseOnScroll 思想)。

1.5 预取相邻页的数据(而不是“创建相邻 Fragment”)

viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(pos: Int) {
    prefetch(pos + 1); prefetch(pos - 1)   // 只拉数据,不做 UI inflate
  }
})

2) 页很多时:两种“跳出 Pager 的思路”

2.1虚拟化分页(3 页环形复用)—— 强烈推荐

  • 思想:屏幕上最多显示 1 页,左右各 1 页是过渡。我们只持有 3 个“容器” (3 Fragment 或 3 自定义 View)。滑到新位置时,把最远的那个复用,给它绑定新 categoryId/数据即可。
  • 适配器伪码(自定义 View 版更轻):
class VirtualPagerAdapter : RecyclerView.Adapter<VH>() {
  override fun getItemCount() = Int.MAX_VALUE    // 视觉无限
  override fun onBindViewHolder(h: VH, pos: Int) {
    val real = pos % categories.size
    h.bind(categories[real])
  }
}
// 用 TabLayout 计算并滚动到“映射位置”(currentBase + delta)
  • 优点:内存固定、小;切页只做数据重绑,不创建新 Fragment。

2.2单页 + Section 列表

  • 一个 RecyclerView,按类目拼成多段 Section,Tab 只是锚点
  • 用 LinearSmoothScroller/SnapHelper 定位到某 Section;加 Sticky Header 实现“像分页一样”的体验。
  • 优点:只有一个页面,复用最佳;缺点:数据/状态管理在一个列表里稍复杂。

3) Tab 自身的优化(Tab 很多时)

  • 懒创建 Tab:TabLayoutMediator 绑定后不要一次性做复杂自定义 View;可在 onPageSelected 时惰性膨胀 Tab 的复杂布局。
  • 开启 mode="scrollable";避免超长文案排版抖动,预设最小宽。
  • 变动频繁时用 DiffUtil 更新数据源,再 notifyItemRangeChanged,避免全量重建。

4) 内存与渲染细节

  • 统一用 ViewBinding,减少 findViewById 与错误引用。
  • Fragment 不持有外部大对象;图片用生命周期感知加载;退出页立刻取消协程/订阅。
  • 首屏/首个 Tab:能展示占位就先展示,占位轻(Shape/渐变),不要大阴影/复杂圆角层叠。

5) 观测 & 验证(别拍脑袋)

  • 打点:切页耗时首帧内存(Java/Kotlin heap + Bitmap + Graphics);观察 OffscreenPageLimit 改动的影响。
  • adb shell dumpsys meminfo your.pkg & Android Studio Profiler 看峰值;大于 256–512MB 要警惕。
  • 卡顿排查:Systrace/System Trace 看 Choreographer 帧;如果切页掉帧,多半是页内首次 inflate/解码/布局过重→改为懒加载 + 预取。

6) 高频坑清单

  1. 用了 FragmentPagerAdapter 或把 offscreenPageLimit 设很大 → 直接 OOM/掉帧。
  2. 每个页面都各自一个 RecycledViewPool → 切页时频繁 inflate,明显卡。
  3. 页内做耗时 I/O 或 decode → 放到可见后或后台,首帧只占位。
  4. 动态增删 Tab 没有稳定 ID → 整个 ViewPager2 抖到重建。
  5. 嵌套滚动没处理好 → 滚动冲突/测量多次;用 NestedScroll 正确接入或禁掉一层滚动。

7) 一套“能直接用”的配置清单

  • ViewPager2 + FragmentStateAdapter + offscreenPageLimit=1
  • TabLayout mode="scrollable";Tab 复杂视图惰性创建
  • 页内 RecyclerView:共享 RecycledViewPool、setHasFixedSize(true)、initialPrefetchItemCount=8
  • 懒加载:repeatOnLifecycle(RESUMED) 拉数据;相邻页只预取数据****
  • 页数>50:改“3 页环形复用”或“单页多 section”方案