现在开发中列表控件基本都是使用recyclerview控件,recyclerview在结构上使用了跟Listview差不多的view以及adapter之外,还使用了LayoutMananger,LayoutManager主要负责测量以及布局摆放,也正是因为LayoutManager负责了摆放,所以LayoutManager是最清楚知道View摆放位置的一层,对应的涉及到view位置更改的操作也都是由它处理的,比如Item的拖拽。
recyclerview跟Listview的区别
1.ListView是由自己来测量以及布局的,recyclerview增加了LayoutManager, 交由Manager来测量以及布局,职责更加分明
2.ListView的复用是通过ViewHolder中转来复用View,RecyclerView是通过复用ViewHolder来实现View缓存的
3.ListView是两级缓存,具体可见RecyclerBin该类的注释。ListView缓存的载体为 ActiveViews(数组) 以及mScrapViews(Array数组),这两个数组中存储的都是View对象,正因为这两个数组,所以LIstview中的type类型都是0,1,2,3这种连续的Int型数组。RecyclerView缓存主要是Recycler这个类来处理,载体跟LIstview差不多,多了一个RecyclerViewPool,但是不同的是,存储的都是ViewHolder;更好的一个点是,同一个上下文中的Recylerview可以共用RecyclerViewPool,所以可以提高内存的使用效率。
4.ListView的弊端在于,如果给某一个View设置动画,没有办法特别精细的去操作某一个View,只能整体重新测量布局然后刷新,极大的损失了资源。并不是无法实现,而是很麻烦。Recyclerview则可以局部刷新。
5.Recyclerview有一个DividerItemDeCoration,可以很方便的设置边界线,并且很好的执行动画,并且DividerItemDeCoration并不是只能在Item的边界去绘制,范围是整个Recyclerview。
ListView的运作机制如下:
通过adapter去创建view,屏幕内没有显示view时也就没有缓存,直接创建,当View放满屏幕时,再滑动,划出屏幕的view会被存储起来,即将划入屏幕的View会优先从内存中取出来复用,对应的就是咱们经常写的 Viewholder缓存代码,其中getView中的第二个参数converView,就是从内存中取出的View。
ListView缓存的载体为 ActiveViews(数组) 以及mScrapViews(Array数组),这两个数组中存储的都是View对象
这里需要注意,mScrapViews,该数组的索引即为adapter的type类型,回想咱们再编写ListViewAdapter的时候,是不是需要用一个int型去创建type类型?正因为这个数组,所以Listview中的type类型都是0,1,2,3这种连续的Int型类型。mScrapViews的索引代表类型,每一个索引对应的ArrayList中存的就是当前type的View缓存。
那这个缓存是如何运作的呢,两种情况
- 当调用notifyDataSetChange时,会将屏幕上显示的View全都放到mScrapViews中缓存起来,但时数据会被清空,而后刷新完成后,重新将缓存取出来,再调用Adapter的onBind方法重新绑定数据。
2.当滑动的时候,会先从缓存中取View,取出来的View也就对应了getView()的convertView,但是缓存中可能没有,所以咱们在写adapter时,需要判断这个View是否为null,如果为null需要重新创建。 - 那ActiveViews有什么作用呢?实际上当adapter的数据没有发生改变,但是触发了onLayout方法,如:requestLayout的时候,会使用ActiveViews存起来,并且重新读取时并不会触发adapter的getView以及onBind,那这个速度就很快了,但是ListView并没有很好的运用这一层缓存机制。那可能有人问了,我可以在data不改变的情况下notifydatasetChange,这不也没改变数据,很遗憾,只要调用notifydatasetChange,ListView就会判定数据发生改变了,所以说ListView并没有很好的运用这一层缓存机制。
Recyclerview的运行流程大概如下:
需求方Recyclerview需要显示View,会通知LayoutManager, LayoutManager并不会自己创建view,也不会直接找Adapter拿View,而是会通过Recycler这个类,先判断缓存中有无,(类似于图片的三级缓存,只不过没有本地文件缓存,直接从内存中找,没有就创建)无的话找Adapter拿,调用的是Adapter的onCreatViewHolder方法,如果有也会找Adapter重新绑定数据,对应的是onBindViewHolder,然后拿到ViewHolder之后返回给LayoutManager,然后布局摆放,最后展示出来。
Recyclerview的缓存运作大概如下:
-
Recyclerview中有一个mCachedViews集合,类型为Arraylist<ViewHolder>,该集合的默认Size=2。假设当前一屏只够展示10条item,从0到9从上往下依次摆放,当手指继续向上滑动时,0和1对应的item会划出屏幕,完全滑出屏幕后,缓存开始生效,将这两条存入mCachedViews, 此时如果改变方向手指向下滑动,滑到1完全展示时,会依据position从缓存中取出1,说明这时的ViewHolder是保留position的(不像Listview一样,先根据position取出type对应的Array)。那到这里就会产生一个问题,如果在item中展示的某些控件,需要在item划出屏幕后去释放回收资源怎么办?这不是就有问题了吗?其实谷歌已经考虑到该问题,增加了两个回调
可以在这里处理View划出屏幕之后的逻辑。
- 那如果存入的缓存数量>mCachedViews的size,存入时间最早的缓存就会被取出,放入到RecyclerViewPool中,这个池子中包含了一个SparseArray<ScrapData>ScrapViews
RecyclerViewPool为Recycler的一个内部类,ScrapData中包了一个ArrayList<ViewHolder>类型的mScrapHeap对象,最终的数据格式为 SparseArray<ArrayList<ViewHolder>>。这种格式的优势在于,key可以是不连续的Int值。
RecyclerViewPool中有一个DEFAULT_MAX_SCRAP=5,表示每种ViewType中包含的数据条数默认为5条。若某些场景下需要优化该缓存数量,可以调用setMaxRecycledViews(int viewtype,int max)来优化缓存机制。
说回缓存机制,放入到RecyclerViewPool中后,如果用户想滑回去重新展示已经放到RecyclerViewPool中的数据,就可以直接拿到缓存使用,并且该缓存是已经保存了数据的,所以不需要调用绑定数据的方法。需要注意的是,保存了数据,不代表数据就是正确的。在某些场景下,绑定数据可能会耗时,那这时就有很好的优化效果了。可以先拿到控件上的数据判断是否为目标数据,一致的话就不需要更新了,否则再重新绑定数据。
如果触发notifyDataSetChange,缓存会怎么运作呢?
触发notifyDataSetChange时,跟Listview一样,列表会判断这是一次数据变更,会将这些数据放到缓存里,那放到RecyclerViewPool中呢还是mCachedViews中呢?其实答案已经很明显了,既然判断是一次数据的大变更,那肯定需要重新绑定数据的,而mCachedViews中是直接拿来用的,不会重新绑定数据。所以是放入到RecyclerViewPool中,如果pool中的数量不够,就会调用createViewHolder以及onBindViewHolder的方法,重新创建。这样的话资源消耗就很大了,所以非必要的情况下,我们需要调用局部刷新功能以优化资源消耗。如果非要调用notifyDataSetChange的情况下优化,那可以再notifyDataSetChange之前,增大缓存池的容量,而后调用notifyDataSetChange,再缩小缓存池,只是该方式不够优雅。
以上两层的优化跟ListView的区别是不太大的。RecyclerView除了以上的两层优化,还有别的优化。
- Recyclerview的另外一层优化是局部刷新。局部刷新时可以节省大量的资源,这是显而易见的。但是现在需要考虑一个问题,当调用notifyItemInsert时,插入item的后续item都需要往下移动,那必然会导致有些item不可见。那这些不可见的item去哪里呢,这里就引入了一个AttachedScrap,这就是第三个缓存(只是咱们这篇文章中介绍的第三个,从优先级来说是第一级),放入到该缓存中,有需要会优先从这里去取。AttachedScrap是一个暂存区,当调用局部刷新开始时,先把多出来的ViewHolder存进去,当本次刷新调用直到布局完成后,会将AttachedScrap里剩余的ViewHolder拿出来存入RecycledViewPool,重点重点重点:完成一次布局之后。有点类似于垃圾回收中的老生代新生代机制。
- 当某个Item的数据发生改变,调用notifyItemChange时,该ViewHolder会被放入到ChangeScrap的ArryList中,ChangeScrap只是在布局开始到布局结束过程中临时存放,布局完成之后就把缓存挪到AttachedScrap,使用时间很短。该暂存区是否可以算作一层缓存呢?值得思考
那以上四级缓存的优先级是怎样的呢?正常情况下,首先是AttachedScrap,然后是CachedViews,最后是RecycledViewPool。局部修改刷新时,第一层为ChangeScrap缓存,其他几层同正常情况下的顺序。
总结一下:
- AttachedScrap:之前在屏幕内,由于局部刷新被挤出屏幕,比如调用notifyItemInsert插入一条新数据,就可能有一条数据被挤出屏幕。notifyItemRemove:可能会有一条数据被移除。这些数据会被判定为比较新的数据,所以在缓存优先级中最高。
- mCachedViews:屏幕内的item被滑动出RecyclerView的边界时,会根据position存入该缓存,但由于容量有限,较老的缓存会被放入RecycleViewPool,主要优化滑动或者回滚时期的ViewHolder。不需要重新绑定数据。
- mViewScrapExtation 该缓存为自定义的缓存,极少使用。
- ReccycleViewPool,在调用notifyDataSetChange时会全部缓存进该缓存。根据ViewType来缓存ViewHolder,所以ViewHolder上的数据跟postion可能都是错误的,需要重新Bind数据,在绑定数据较为耗时的情况下,可以直接判断View上的数据和正确的数据是否一致,来优化。每种ViewType的大小默认为5,开发者可修改。该回收池也可以自定义。
还没完,Recyclerview还有一个非常核心的机制。
使用过RecycleView的都知道,Recyclerview的另外一大优点是可以很轻松的给Item增加动画,极大的提高了体验。那思考一下,这个动画是如何在更新列表的同时增加上去的呢。这里就需要引出另外一层核心的机制了:pre/post-layout。
这里先说一个示例。
- 假设现在有一屏仅能显示10条item的Recycelrview,当我们调用notifyItemRemove的时候,移除index=9的item,默认的动画为index=10的item会向上移动。那么问题来了,index=10的item执行动画的初始位置是怎么计算的?有的同学说,10不就在9的下方吗?这是错误的,由于一屏展示10条,index=10的item理论上来说在屏幕外,但实际上未移除9之前,LayoutManager并未在屏幕上摆放index=10的item。
那如何计算呢?这里Recyclerview使用了预测性动画,调用notifyItemRemove时,Recyclerview一看 屏幕内没有10,就会通知LayoutManager从缓存中拿或者创建第10条摆放到9的下面,这样就有初始位置了,接下来执行动画。
这个理解之后,开始考虑下一个问题:当修改某一条Item时,默认动画为 轻轻的闪烁一下。那这个动画是如何实现的呢?为什么会闪烁一下。 - 当修改某一条Item时,调用notifyItemChange会闪烁,是因为Item执行了淡出淡入的动画。问题又来了动画执行前后是同一个View吗?首先,动画不是在同一个item对象上同时发生的,是两个View一个在执行出场动画,一个执行入场动画。嗯?为什么是两个View?这里就是pre/post-layout运作了。preLayout指的是 View change之前对应的layout,这个layout中的ViewHolder是旧的。postLayout指的是change之后对应的layout,这个ViewHolder是新的。那由于有缓存,旧的直接在缓存中取就可以,那新的呢?新的内存中可能是没有的,所以recyclerview的prelayout是从AttachedScrap以及ChangedScrap中取,postLayout只能从CachedViews以及RecyclerViewPool中取,取不到就去创建新的。
setSupportsChangeAnimations(false)可以禁用这个默认的动画。那如果我只是想禁用闪烁的动画,插入删除的动画还要用怎么办?可以调用notifyItemChanged(position,"payload")。