RecyclerView 重读

623 阅读13分钟

本文请搭配源码阅读

RecyclerView的注释是这么写的

这是一种灵活的视图,专用于大型数据集提供有限的窗口 。

因为View树的创建涉及了大量View对象的创建的开销和JVM回收对象的开销,以及View树初始化的由于某些递归逻辑的耗时,View的回收更改复用比重新初始化View更具性能优势,在列表类的视图中,View的回收与复用是一项很重要的话题

术语说明:

Adapter: 一个内部类,负责表示单个数据项中的内容,就是说,整个数据集Collection,它负责展示其中的个体

Position: 单个数据项在数据集中的位置

Index: 因为RecyclerView是一个ViewGroup,Index就是每个子View的索引

Bind: 将数据集中的第Postion数据,映射到ViewGroup中的Index子View的绑定过程,这个工作由Adapter执行

Recycle: 以前展示过的View,会被放置在缓存中,以便之后重用,因为初始化ViewTree十分耗时,且Data所用的View是同一类型。

Scrap (废弃): 处于detach状态的View,scrapView可以被复用,要么被RecyclerView不需要修改绑定就可以复用,要么就会变成DirtyView,如果使用就需要再次绑定修改

Dirty :如果需要使用就必须由Adapter重新绑定的View

由此看来: RecyclerView的目的是什么呢 ?

将结构化的数据集,以结构化后的视图ViewTree展示出来,就是数据可视化的过程
因为数据集中的数据项内部结构相似,就可以做好一个数据项的展示过程,然后批量化将整个数据集可视化

那么,我们知道它的目的,那我们自己想一下它工作的过程

  1. 首先获取到整个的数据集DataList,然后根据顺序把每个单独的数据Data拿出来
  2. 初始化一个单元数据的展示View
  3. 把Data中的每个字段,以某种格式,赋值到展示View中去
  4. 循环123步骤,直到所有Data都以View展示出来 由此可见:最核心的工作就是步骤2-3,将Data绑定到View中去

Positions in RecyclerView: RecyclerView在Adapter和LayoutManager之间引入的一个抽象层级,目的是在计算Layout期间,可以批量的感知到数据集的变化(具体这句话是啥意思,还没太懂)

1.RecyclerView的初始化

构造方法的参数还是context,attr,说明这是一个ViewGroup

   setScrollContainer(true)
   setFocusableInTouchMode(true)
   setWillNotDraw()
   /*
   初始化了若干设置项
   然后初始化了ViewConfiguration的final对象vc,将vc的一些私有变量引用给this的成员
   */
   
   //初始化了名为AdapterHelper的对象作为this的成员
   //具体操作貌似是一个方便调用类内方法的辅助类
   initAdapterManager()
   
   //初始化名为ChildHelper的对象作为this的成员,具体功能和AdapterHelper相似
   initChildrenHelper()
   
   // 自动填充是啥 ?,内部调用了ViewCompatible,之后的一些操作同样是围绕着ViewCompat调用的
   initAutofill()
   
   TypedArray a;
   ...
   //从R.attrs到this成员变量,将xml内的参数,赋值到this的设置项
   a.cycle();
   // 根据xml的参数反射创建LayoutManager对象
   createLayoutManager(context,className,attrs)

总结:
构造方法具体工作就是初始化一些设置项,外加解析xml中参数。
在初始化ChildHelper和AdapterManager的过程中,我们明显看出了一种设计模式:

因为RecylerView内部方法太多了,方法细粒度很大,这当然有很多优点,比如可以对操作高度自定义,执行过程中可以快速定位到错误,但也因此,一些宏观上的操作,需要一大串的方法联合调用,将独立的方法编纂起来,这就需要Helper类充当中介,定义宏观上的接口,具体实现依赖主类内的方法和变量,暴露的API也通过Helper来执行


2.Adapter与LayoutManager的初始化

我们知道要使用RecyclerView,必须先初始化Adapter与LayoutManager,那么Adapter和LayoutManager又扮演着什么角色呢

2.1 Adapter

---> 点击查看源码分析
    // 一个没有构造方法的抽象类,这就说明Adapter的功能只是依赖于暴露的接口以供外部调用
    abstract class Adapter<VH extends ViewHolder> {
        // 通过Inflate xml或者其他方式创建一个新的ViewHolder,然后通过onBindViewHolder,将数据集中的单项
        // 数据绑定到此ViewHolder,最好可以缓存ViewHolder中的子View,来避免findViewById的多此调用
        abstract VH onCreateViewHolder(ViewGroup parent,int viewType)
        
        //更新ViewHolder中itemView的内容,来反映绑定的数据集中Position项数据
        //RecyclerView 不会在当前itemData在数据集中的位置Position发生变化时调用,除非item失效或者无法确定新位置
        //所以入参的position记录下来无意义,比如说RecyclerView按照倒序排列,
        //那么之前记录的第一个,就变成最后一个,现在点击第一个,不会触发写好的逻辑,点击它,它所在的位置也已经变成了最后一个,也与记录的position不符  
        // 如果你需要获取当前ViewHolder的位置,那就调用getAdapterPosition(),这是最准的
        //稍后具体逻辑看下调用时机
        abstract void onBindViewHolder(VH holder,int position)
        
        //部分绑定与全局绑定:
        //更新某ViewHolder所绑定的数据,通常的操作就是重新执行一遍BindViewHolder
        //假如在初始化后,单元数据项内只有部分数据改变了,所绑定的ViewHolder本身又很复杂
        //如果只更新ViewHolder中的部分view,即减少了工作量,也免去一些诸如背景闪烁的问题
        // 暂且认为 payloads就相当于一个标记位
        abstract void onBindViewHolder(VH holder,int position, List<Object> payloads)
        
        //此方法调用onCreateViewHolder(),同时还设置了holder的viewType作为成员变量
        //并且引入了TraceCompat和一些检验项,作为Systace的埋点
        final VH createViewHolder(ViewGroup parent,int viewType)
        
        //此方法调用onBindViewHolder(),同样设置了一些参数作为成员变量,并且引入了TraceCompat
        final void bindViewHolder(VH holder,int position)
        
        /*之后的方法都是声明当前数据集的单元的一些状态,诸如是否有稳定的ID,数据集的总项数等
        */
        
        /*当ViewHolder被回收时调用
        当LayoutManager不再将其View attached到RecyclerView时,表示此ViewHolder被回收了
        有可能是当前View处于不可见状态,或者处于缓存的状态,如果当前View绑定了大量的数据比如大体积Bitmap
        在此时就可以释放资源
        此方法在RecyclerView彻底清楚ViewHolder内的数据并将其发送到RecyclerPool缓存池之前调用
        因此在执行此方法的末尾之前,都可以通过getAdapterPosition()来获取当前ViewHolder的位置
        */
        void onViewRecycled(VH holder)
        
        
        /*这个方法是RecyclerView回收过程中的一个步骤,就是判断当前ItemView是不是处于瞬时状态
        
         瞬时状态:这个概念定义在View所处的状态,表示当前View是否处于动画过程中或者跟踪用户更改
         内容等不稳定的状态,属于一个约定俗称的状态,在这个状态中,View是不会被回收或更新内容
         
         由于RecyclerView也属于ViewGroup,所以ItemView本身的Animation,并不会调用此方法,此方
         法的调用,只是在ItemView内部子View处于瞬时状态,才会触发此方法
         具体调用时机还要分析下回收逻辑recyclerViewHolder
         */
        boolean onFailedToRecycleView(VH holder)
        
        /*因为RecyclerView属于ViewGroup,它通过addView()将ViewHolder中的View添加到自身的ViewTree内
        这相当于这个过程中的callback,监听attachWindow和DetachWindow这两个事件
        */
        void onViewAttachedToWindow(VH holder)
        void onViewDetachedFromWindow(VH holder)
        
        //在RecyclerView setAdapter()时调用,或者更改Adapter,是一个回调
        void onAttachToRecyclerView(RecyclerView recylerView)
        void onDetachFromRecyclerView(RecyclerView recyclerView)
        
        /*
         剩下的就是Adapter对外暴露的命令接口,负责操作RecyclerView内的Observable,进而更新
         RecyclerView的数据->View的过程,并且必须要调用此命令接口来启动RecyclerView的显示过程
         具体分析见插图1
        */
        final void notifyDataSetChanged()
        final void notifyItemChanged(int position)
        final void notifyMove...()
    }

插图1:适配器Adapter的更新过程.png

总结

通过这些接口的功能分析,Adapter这个类其实是连接数据集“DataCollection--ItemData” 和视图 “Viewgroup--ItemView”的纽带,从设计模式上来说它是数据系统和视图系统之间的适配器,除了定义Data-->View这个转换工作,和注入一些执行过程中的Callback外,还对外暴露了更新RecyclerView的命令型接口。

附加: DiffUtils

DiffUtils的作用是对比两个数据集的变化,以便以最小更新量来刷新当前列表,比如从ABCABBA-> CBABAC,内部对比使用了Myers算法,就是一种 “图搜索+动态规划” 解决最短路径问题,向右为删除,向下为添加,(向下向右为替换),以图的左上角圆圈为起点,右下角圆圈为终点,当箭头到达一处圆圈时,如果该坐标属于虚线方格的左上角,则可以自动跳到虚线方格的右下角,且在箭头路径中,对角线路径(0,0)->(0,n)/(n,0) ->(n,n),且不经过跳跃路径时,用(0,0)-> (1,1)表示直接替换...

算法是另一个维度的问题,之后再分析吧

Myers算法具体分析 - 传送门

WeChat9893d536bdf6f984f9f480fddc261304.png


3. ViewHolder

ViewHolder描述的是RecyclerView的子itemView和数据集的元数据metaData,就是数据和视图的结合体单元的容器

// 因为ViewHolder是一个存放itemView引用 及 其在RecyclerView各种操作中所保存的状态
// 所以分析其成员变量格外重要
   abstract static class ViewHolder {
    
    //包含的子View
    final View itemView
    
    //ViewHolder内部嵌套的RecyclerView
    WeakReference<RecyclerView> nestRecylerView
    
    //当前ViewHolder的位置
    int position
    //记录ViewHolder位置更改之前的旧位置,在RecyclerView更改动画中使用
    int oldPosition
    long itemId
    int itemType
    int preLayoutPosition
    
    //一个标记位
    ViewHolder mShadowHolder
    ViewHolder mShadowingHolder
    
    int mFlags
    // 各种FLAG,表示ViewHolder所在的状态
    int FLAG_BOUND = 1<<0
    int FLAG_UPDATE = 1<<1
    int FLAG_INVALID = 1<<2
    int FLAG_REMOVED = 1<<3 
    ...
    
    // 回收池容器
    Recycler mScrapContainer
    //表示当前ViewHolder 是否在回收池中
    boolean mInChangeScrap
    
    /* 在ViewHolder通过Adapter绑定和被RecyclerView发送到RecyclerViewPool之间,
       mOwnerRecyclerView就是ViewHolder要attach的recyclerView
        */
    RecyclerView mOwnerRecyclerView
    
    //之后的是对上述的成员变量操作方法
    
    //更改ViewHolder的移除和移位状态
    void flagRemoveAndOffsetPosition(
        int mNewPosition,
        int offset,
        boolean applyToPreLayout)
    {
        addFlag(ViewHolder.FLAG_REMOVED)
        offsetPosition(offset,applyToPreLayout)
        ->{
            oldPosition = position
            position = mNewPosition
    }
    
    
   }

4.Position

由于RecyclerView的界面是由Vsync信号每隔16ms刷新,(它自身是个ViewGroup)但是调用Adapter.notifyDataChange()和数据集改变可能在这期间发生很多次,并且RecyclerView还要监听Touch事件以及ItemView的回收复用,RecyclerView为了提高数据刷新的性能,所以引入了LayoutPosition和AdapterPosition

   ViewHolder findViewHolderForLayoutPosition(int postion)
   -> 
   ViewHolder findViewHolderForPosition(position,false)
   
   ViewHolder findViewHolderForAdapterPosition(int position)
   
   class ViewHolder {
   
   getAdapterPosition()
   ->
   mOwnerRecyclerView.getAdapterPositionFor(this)
   
   getLayoutPosition
   
   

专业术语: Adapter: RecyclerView.Adapter的子类,负责提供表示数据集中项目的视图。 位置:数据项在Adapter 中的位置。 索引:在调用ViewGroup.getChildAt使用的附加子视图的索引。 与位置相对。 绑定:准备子视图以显示对应于适配器内某个位置的数据的过程。 回收(视图):以前用于显示特定适配器位置的数据的视图可以放在缓存中以供以后重用,以便稍后再次显示相同类型的数据。 这可以通过跳过初始布局膨胀或构造来显着提高性能。 Scrap(视图):在布局过程中进入临时分离状态的子视图。 废弃视图可以被重用而不会与父 RecyclerView 完全分离,如果不需要重新绑定则未修改或如果视图被认为是脏的则由适配器修改。 脏(视图):必须由适配器重新绑定才能显示的子视图。 RecyclerView 中的职位: RecyclerView 在RecyclerView.Adapter和RecyclerView.LayoutManager之间引入了一个额外的抽象级别,以便能够在布局计算期间批量检测数据集的变化。 这使 LayoutManager 免于跟踪适配器更改以计算动画。 它还有助于提高性能,因为所有视图绑定同时发生并且避免了不必要的绑定。 为此,RecyclerView 中有两种position相关的方法: 布局位置:项目在最新布局计算中的位置。 这是从 LayoutManager 的角度来看的位置。 适配器位置:项目在适配器中的位置。 这是从适配器的角度来看的位置。 这两个位置是相同的,除了调度adapter.notify事件和计算更新布局之间的时间。 返回或接收LayoutPosition使用最新布局计算的位置(例如RecyclerView.ViewHolder.getLayoutPosition() 、 findViewHolderForLayoutPosition(int) )。 这些位置包括直到最后一次布局计算的所有更改。 您可以依靠这些位置与用户当前在屏幕上看到的内容保持一致。 例如,如果您在屏幕上有一个项目列表,而用户要求提供第 5个元素,您应该使用这些方法,因为它们将匹配用户所看到的内容。 另一组与位置相关的方法采用AdapterPosition*的形式。 (例如RecyclerView.ViewHolder.getAdapterPosition() 、 findViewHolderForAdapterPosition(int) )当您需要处理最新的适配器位置时,即使它们可能尚未反映到布局中,也应该使用这些方法。 例如,如果您想通过 ViewHolder 单击访问适配器中的项目,您应该使用RecyclerView.ViewHolder.getAdapterPosition() 。 请注意,如果RecyclerView.Adapter.notifyDataSetChanged()已被调用且尚未计算新布局,则这些方法可能无法计算适配器位置。 出于这个原因,您应该小心处理这些方法的NO_POSITION或null结果。 在编写RecyclerView.LayoutManager您几乎总是希望使用布局位置,而在编写RecyclerView.Adapter ,您可能希望使用适配器位置。

呈现动态数据 要在 RecyclerView 中显示可更新数据,您的适配器需要向 RecyclerView 发出插入、移动和删除信号。 您可以通过在内容更改时手动调用adapter.notify*方法来自己构建它,或者您可以使用 RecyclerView 提供的更简单的解决方案之一:

使用 DiffUtil 列出差异 如果您的 RecyclerView 显示的列表是为每次更新(例如从网络或数据库)从头开始重新获取的, DiffUtil可以计算列表版本之间的差异。 DiffUtil将两个列表作为输入并计算差异,该差异可以传递给 RecyclerView 以触发最少的动画和更新,以保持您的 UI 性能和动画有意义。 这种方法要求每个列表都在内存中用不可变的内容表示,并依赖于接收更新作为列表的新实例。 如果您的 UI 层没有实现排序,这种方法也是理想的,它只是按照给定的顺序呈现数据。 这种方法最好的部分是它可以扩展到任何任意更改——项目更新、移动、添加和删除都可以用相同的方式计算和处理。 尽管在比较时必须在内存中保留列表的两个副本,并且必须避免改变它们,但可以在列表版本之间共享未修改的元素。 对于 RecyclerView,有三种主要方法可以做到这一点。 我们建议您从ListAdapter开始,这是在后台线程上构建List diffing 的高级 API,代码最少。 AsyncListDiffer也提供了这种行为,但没有定义子类的适配器。 如果您想要更多控制, DiffUtil是您可以用来自己计算差异的低级 API。 每种方法都允许您指定应如何根据项目数据计算差异。

使用 SortedList 进行列表更改 如果您的 RecyclerView 以增量方式接收更新,例如插入项目 X,或删除项目 Y,您可以使用SortedList来管理您的列表。 您定义如何订购商品,它将自动触发 RecyclerView 可以使用的更新信号。 如果您只需要处理插入和删除事件,SortedList 就可以工作,并且它的好处是您只需要在内存中拥有一个列表副本。 它还可以使用SortedList.replaceAll(Object[])计算差异,但此方法比上面的列表差异行为更受限制