RecyclerView原理学习

现在开发中列表控件基本都是使用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缓存。

那这个缓存是如何运作的呢,两种情况

  1. 当调用notifyDataSetChange时,会将屏幕上显示的View全都放到mScrapViews中缓存起来,但时数据会被清空,而后刷新完成后,重新将缓存取出来,再调用Adapter的onBind方法重新绑定数据。
    2.当滑动的时候,会先从缓存中取View,取出来的View也就对应了getView()的convertView,但是缓存中可能没有,所以咱们在写adapter时,需要判断这个View是否为null,如果为null需要重新创建。
  2. 那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的缓存运作大概如下:

  1. 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划出屏幕后去释放回收资源怎么办?这不是就有问题了吗?其实谷歌已经考虑到该问题,增加了两个回调


    recyclerview-3.png

    可以在这里处理View划出屏幕之后的逻辑。

  2. 那如果存入的缓存数量>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除了以上的两层优化,还有别的优化。

  1. Recyclerview的另外一层优化是局部刷新。局部刷新时可以节省大量的资源,这是显而易见的。但是现在需要考虑一个问题,当调用notifyItemInsert时,插入item的后续item都需要往下移动,那必然会导致有些item不可见。那这些不可见的item去哪里呢,这里就引入了一个AttachedScrap,这就是第三个缓存(只是咱们这篇文章中介绍的第三个,从优先级来说是第一级),放入到该缓存中,有需要会优先从这里去取。AttachedScrap是一个暂存区,当调用局部刷新开始时,先把多出来的ViewHolder存进去,当本次刷新调用直到布局完成后,会将AttachedScrap里剩余的ViewHolder拿出来存入RecycledViewPool,重点重点重点:完成一次布局之后。有点类似于垃圾回收中的老生代新生代机制。
  2. 当某个Item的数据发生改变,调用notifyItemChange时,该ViewHolder会被放入到ChangeScrap的ArryList中,ChangeScrap只是在布局开始到布局结束过程中临时存放,布局完成之后就把缓存挪到AttachedScrap,使用时间很短。该暂存区是否可以算作一层缓存呢?值得思考

那以上四级缓存的优先级是怎样的呢?正常情况下,首先是AttachedScrap,然后是CachedViews,最后是RecycledViewPool。局部修改刷新时,第一层为ChangeScrap缓存,其他几层同正常情况下的顺序。

总结一下:

  1. AttachedScrap:之前在屏幕内,由于局部刷新被挤出屏幕,比如调用notifyItemInsert插入一条新数据,就可能有一条数据被挤出屏幕。notifyItemRemove:可能会有一条数据被移除。这些数据会被判定为比较新的数据,所以在缓存优先级中最高。
  2. mCachedViews:屏幕内的item被滑动出RecyclerView的边界时,会根据position存入该缓存,但由于容量有限,较老的缓存会被放入RecycleViewPool,主要优化滑动或者回滚时期的ViewHolder。不需要重新绑定数据。
  3. mViewScrapExtation 该缓存为自定义的缓存,极少使用。
  4. ReccycleViewPool,在调用notifyDataSetChange时会全部缓存进该缓存。根据ViewType来缓存ViewHolder,所以ViewHolder上的数据跟postion可能都是错误的,需要重新Bind数据,在绑定数据较为耗时的情况下,可以直接判断View上的数据和正确的数据是否一致,来优化。每种ViewType的大小默认为5,开发者可修改。该回收池也可以自定义。

还没完,Recyclerview还有一个非常核心的机制。

使用过RecycleView的都知道,Recyclerview的另外一大优点是可以很轻松的给Item增加动画,极大的提高了体验。那思考一下,这个动画是如何在更新列表的同时增加上去的呢。这里就需要引出另外一层核心的机制了:pre/post-layout。
这里先说一个示例。

  1. 假设现在有一屏仅能显示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时,默认动画为 轻轻的闪烁一下。那这个动画是如何实现的呢?为什么会闪烁一下。
  2. 当修改某一条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")。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容