RecycleView

RecycleView

参考:https://mp.weixin.qq.com/s/auphzaQF6_wJx6dGFY6niA

描述

在 RecycleView 的 onMeasure 阶段,默认采用自动测量模式,如果自身宽高不确定,会尝试通过 LayoutManager 测量子 Item 来确定自己的宽高,所以尽可能的使用 match_parent 或固定宽高来布局 RecycleView,减少性能的损耗

在 RecycleView 的 onLayout 阶段,才会真正调用调用 LayoutManager.onLayoutChildren() 来对 Item 进行测量和布局

在 LayoutManager.onLayoutChildren() 方法中,会先通过 detachAndScrapAttachedViews() 函数回收布局,然后再通过 file() 函数进行布局,具体流程如下:

file() -> layoutChunk() -> LayoutState.next() -> addView() -> measureChildWithMargins() -> layoutDecoratedWithMargins()

可以看到 LayoutState.next() 函数里最终调用 tryGetViewHolderForPositionByDeadline() 函数,然后通过 Recyler 四级缓存或创建来获取 ViewHolder

从上面可以得出,会先回收布局,然后再从缓存中取出来,这里是不是没事找事做了?这里应该是一种职责分离吧,layout 的事归 LayoutManager 管,缓存的事归 Recycler 管。LayoutManager 不应该知道哪个 viewHolder 是否有效

可以看到 RecycleView为了提高效率,部分交互是通过 attachViewToParent 和 detachViewFromParent 来对 View 进行轻量级的添加和移除,其中 attachViewToParent 可以调整 View 的布局顺序,注意与 getChildDrawingOrder 的区别

【备注一】detach 在 ViewGroup 中的实现很简单,只是将当前 View 从 ParentView 的 ChildView 数组中移除,将当前 View 的 mParent 设置为null, 可以理解为轻量级的临时 remove。

【备注二】remove 代表真正的移除,不光从 ChildView 数组中移除,其他和View树各项联系也会被彻底斩断。

核心组件

  • LayoutManager:负责 item 的 measure 、layout
  • Recycler:四级回收、复用机制
  • SmoothScroller:滑动速度控制
    • LinearSmoothScroller
  • SnapHelper:惯性滑动控制
    • LinearSnapHelper
    • PagerSnapHelper
  • ItemAnimator:Item 动画
    • SimpleItemAnimator
    • DefaultlteAnimator
  • ItemDecoration:Item 样式装饰
    • DividerItemDecoration
    • ItemTouchHelper:可以实现侧滑删除,拖动排序
    • 可以扩展吸点悬浮的效果
  • OnItemTouchListener:手势拦截器
  • DiffUtil:差分刷新

Recyler 回收池 - RecycleView 四级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Recycler {
// #1 屏幕内,复用时不需要重新bindViewHolder,因为还会出现在屏幕上面,数据和状态是不会重置的
ArrayList<ViewHolder> mAttachedScrap;
// 屏幕上发生变化的会放入changed中,其余放入到attached中
ArrayList<ViewHolder> mChangedScrap;

// #2 划出屏幕,默认大小为2,划出去的item存放在这里,再次划进屏幕也是不需要重新绑定数据的;通过setItemViewCacheSize()这个方法调整它的大小
ArrayList<ViewHolder> mCachedViews;

// #3 自定义扩展View缓存,允许开发者自定义缓存逻辑,当一二级缓存未命中时会调用该缓存;
// 注意:该扩展缓存限制较多,因为RecyclerView不知道你创建的viewHolder
ViewCacheExtension mViewCacheExtension;

// #4 根据viewType存取ViewHolder,当mCachedViews放不下时会缓存到此
// 可通过setRecycledViewPool调整,每个类型容量默认为5
RecycledViewPool mRecyclerPool;
}

优化点

  1. 当我们调用 notifyDataSetChanged() 或 notifyItemRangeChanged(p, c) (c这个范围非常大的时候),会将所有的 ViewHolder 全部放入到 pool 中,即第四级缓存中,由于第四级缓存默认大小为5,所以当界面显示的数量大于5时,会出现丢弃的情况,造成需要重新 craete 和 bind 来补充缺失的 item,对性能影响比较大(notifyItemRangeChanged 哪怕超出屏幕的 item 都会尝试 create 和 bind,同样性能损耗较大)。如果列表显示很多行,且 notifyDataSetChanged() 函数调用较为频繁,可以考虑设置 pool 的大小。

    1
    recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);

    或者给条目设置全局ID:

    1
    2
    3
    4
    5
    6
    // adapter 中复写
    override fun getItemId(position: Int): Long {
    return position.toLong()
    }

    mAdapter.setHasStableIds(true);
  1. 当知道 Item 的改变不会影响到 RecycleView 的宽高时,可以通过 setHasFixedSize(true) 函数增加提高增删改插时的性能,原因如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // onItemRangeChanged
    // onItemRangeRemoved
    // onItemRangeMoved
    // onItemRangeInserted
    // 当 mHasFixedSize = true 时,上述四种方法都不会触发 requestLayout 重新布局
    void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
    ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
    mAdapterUpdateDuringMeasure = true;
    requestLayout();
    }
    }

    【注意】notifyDataSetChanged() 最终会触发 onChanged() 并调用 requestLayout() 重新测量布局

  2. 数据预取, Android 5.0 起,显示分为两个阶段:

    1. UI线程先处理布局信息,即创建和绑定View
    2. 渲染(Render)线程把指令发送给GPU

    在不卡顿的前提下,帧与帧之间UI线程会有闲置的时机,RecycleView 在新条目即将进入视野前,会花大量的时间来创建和绑定 View,造成下一帧出现卡顿,数据预取的思想就是利用闲置的线程,提前将下一帧创建和绑定 View 的工作移至前一帧的空闲时间来完成。

    如果使用 RecycleView 内部的 LayoutManager,已经自动优化了。

    需要手动优化有以下两种场景:

    1. 嵌套 RecyclerView:内部的 LayoutManager 需要调用 LinearLayoutManager#setInitialPrefetchItemCount 来设置预存取的数量,如:至少展示三个条目,则需要传入4
    2. 自定义 LayoutManager:需要重写 LayoutManager#collectInitialPrefetchPositions 方法实现预取逻辑,参考:https://juejin.cn/post/6844903661382959118
  3. 在 TabLayout+ViewPager+RecyclerView 的场景中,多个界面的条目布局是一致的,可以考虑多个 RecyclerView 共用一个 RecycledViewPool 来减少创建 ViewHolder 的开销

  4. 在条目中需要加载图片等场景,滚动时加载或只加载内存中的图片

  5. 尽可能的使用局部刷新函数,善用 DiffUtil

  6. 避免在 onCreate 和 onBind 中创建过多对象

  7. RecycleView 和 Item 尽可能的不要使用 wrap_content

  8. 不要在 ScrollView 中添加 RecycleView