RecycleView

RecycleView

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

描述

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

RecycleViewonLayout 阶段,才会真正调用调用 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为了提高效率,部分交互是通过 attachViewToParentdetachViewFromParent 来对 View 进行轻量级的添加和移除,其中 attachViewToParent 可以调整 View 的布局顺序,注意与 getChildDrawingOrder 的区别

【备注一】detachViewGroup 中的实现很简单,只是将当前 ViewParentViewChildView 数组中移除,将当前 ViewmParent 设置为null, 可以理解为轻量级的临时 remove

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

核心组件

  • LayoutManager:负责 itemmeasurelayout
  • Recycler:四级回收、复用机制
  • SmoothScroller:滑动速度控制
    • LinearSmoothScroller
  • SnapHelper:惯性滑动控制
    • LinearSnapHelper
    • PagerSnapHelper
  • ItemAnimatorItem 动画
    • SimpleItemAnimator
    • DefaultlteAnimator
  • ItemDecorationItem 样式装饰
    • 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时,会出现丢弃的情况,造成需要重新 craetebind 来补充缺失的 item,对性能影响比较大(notifyItemRangeChanged 哪怕超出屏幕的 item 都会尝试 createbind,同样性能损耗较大)。如果列表显示很多行,且 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. 避免在 onCreateonBind 中创建过多对象

  7. RecycleViewItem 尽可能的不要使用 wrap_content

  8. 不要在 ScrollView 中添加 RecycleView