Android学习笔记06-RecyclerView原理解析

Posted by panthole on 2021-07-21

一、LayoutPosition vs AdatperPosition

  1. adapter position: 从adpter角度来看,此position表示一个item在adapter数据里的位置。
  2. layout position: 从layoutmanager角度来看, 此position表示一个item在上一次布局结束时在adapter数据中的位置,对应了用户在当前屏幕上所看到视图的位置(此时需没有新的布局计算)。

当adapter的内容改变时,recyvcerview会请求刷新一次布局。从发起请求到完成布局计算的过程中, layout position和adapter position可能会不同。因为数据发生了变化,视图还没有来得及刷新,导致item在数据层的位置(AdapterPosition)已经被更新(因为数据确实已经改变了)了,但是LayoutPosition这个代表着最近一次布局结束后item的position需要等待刷新完毕才能更新,

当调用notifyDataSetChanged()之后,此方法会重绘整个rv, 在新的位置计算出来之前, layout position可能为NO_POSITION。

但是对于notifyItemInserted, layout position不能立即获取到最新的值, 因为布局还没更新(需要< 16ms的时间刷新视图), 所以只能获取到旧的, 而adatper position却能马上获取到最新的值。

  1. 结论:
    • 在大部分时间里,adapter position与layout position都是一致的,只有在改变了Adapter数据并且调用notify** 方法直到新布局计算完成前的这段时间里,他们可能不同。
    • 当不涉及内容改变,只处理用户当前看到的屏幕上的内容时(例如改变当前屏幕展示的第2个item的内容),可以使用layout position。
    • 当涉及到内容有改变,并且需要使用最新的位置时(例如为viewholder绑定点击事件),可以使用adatper position。在粒度的notify事件中(如notifyItemInserted)等,即使布局还未刷新完毕(此时的layout position不是最新的),adatper position也能立即更新到最新的值。
    • 对于notifyDataSetChanged()需要小心处理, 此方法会重绘整个recyclerview, 在新的布局计算出来之前, layout position可能为NO_POSITION。
    • 对adapter来说(data角度),AdapterPosition代表现在,LayoutPosition代表过去。
    • 对recyclerview来说(可以理解为view角度),LayoutPosition代表现在,AdapterPosition代表将来。

二、锁定viewholder不被回收的方法

setIsRecycler + ViewCacheExtension

  1. 保存需要不被回收复用的holder的itemview,并设置其isRecycleable为false
  2. 继承ViewCacheExtension,并实现getViewForPositionAndType方法,当之前的那个holder滑回来时,返回前面保存的itemview,并调用recycler.bindViewToPosition重新绑定新的postion
  3. 给recyclerview设置viewcacheExtension
    涉及知识点: recyclerview的缓存机制\

2.1 使用动画时注意preLayout

在发现页的开发中,当正在以浮窗形式播放第3个卡片的视频时删除第1个视频卡片,此时第3个视频卡片会由于第1个卡片的消失而上移补位,回到屏幕区域中,正常流程应该是从浮窗切回卡片播放,但是实际不会从浮窗恢复到卡片播放的问题:
实质是在调用ViewCacheExtension的getViewForPositionAndType中使用的是preLayout的position,这时的position是还未删除第1个卡片时的状态,所以会从datalist中取出第4个(从0开始计算)视频的信息并与之前缓存的第3个视频的vid进行比较,肯定不会相同,于是返回null,接着重新bind一个新的holder。需要使用convertPreLayoutPositionToPostLayout将preLayout的position转变为删除item后此处真正的position。

三、Recyclerview寻焦

ViewGroup:

  1. requestChildFocus: 如果是FOCUS_BLOCK_DESCENDANTS, 直接返回,不会去设置mFocused
  2. hasFocusable: 先判断ViewGroup作为一个View自身的focusable(mViewFlags\),如果为true, 则直接返回true,如果未false,再向下遍历child, 但是如果是FOCUS_BLOCK_DESCENDANTS(mGroupFlags\), 直接返回false,否则就依次遍历child的hasFocusable,找到一个为true的就直接返回true
  3. addFocusables: 如果是FOCUS_BLOCK_DESCENDANTS,不会调用child.addFocusables
  4. requestFocus: 如果是FOCUS_BLOCK_DESCENDANTS,调用super.requesetFocus,即View的requesetFocus方法, 尝试将焦点转移到view上或者其子节点上。(如果这个view或者其parent设置了block_descendants,那么这个view将不会获得焦点)

attachViewToParent -> requestFocus

  • 未添加playerview抢焦点时,attachViewToParent里判断childHasFocus未false,不会触发requestChildFocus()
  • recyclerview layout流程: onLayout -> dispatchLayout -> dispatchLayoutStep2 ->onLayoutChildren -> fill -> layoutChunk -> addview -> dispatchChildAttached
  • requestChildOnScreen , offsetDescendantRectToMyCoords, focusSearch

findFoucs: 查找当前焦点所在,从顶层至下层查找
requesetFoucs(requesetChildFoucs): 请求焦点, 从顶层至下层请求
focusSearch: 搜寻下一个焦点, 是从当前焦点控件开始的,就是从内向外寻找

recyclerview:

  1. layoutChunk: result.mFocusable = view.isFocusable();

  2. LayoutChunkResult

  3. stopOnFocusable

  4. focusSearch

  5. onFocusSearchFailed

recyclerview的recycle流程:

  1. 当一个view滑出屏幕时,变成了一个scrap view
  2. scrap view会被放置到缓存池中,变成了一个recycle view。缓存池里缓存的都是对应同一数据类型的view。
  3. 当一个新的item需要显示时,缓存池中的一个view会被取出复用。由于此时这个view还需要被adapter重新绑定后才能展示,所以这时的这个recycle view称为dirty view
  4. dirty view复用阶段: adapter定位下一个要显示的item的数据,并且将数据拷贝到dirtyview中。这些dirtyview 的引用通过与recycle view关联的viewholder中获取。
  5. 准备好的recycled view被添加到recyclerview的item队列里以备展示只用。
  6. 随着用户继续向上滑动,recycled view出现在屏幕上,最顶端的view又开始走前面几步的处理流程。

https://developer.xamarin.com/guides/android/user_interface/recyclerview/Images/04-view-recycling-sml.png
04-view-recycling-sml.png

LayoutManager:
LM用于定位在RV中显示的items.
如果是垂直布局的LinearLayoutManager的话,LayoutState.LAYOUT_END表示向下翻滚(手指向上划),反之LayoutState.LAYOUT_START表示向上翻滚。

ViewHolder生命周期

1. LayoutManager使用一个view的流程: 创建
  1. LM通过getViewForPosition(int position)向RV索取一个对应当前position的view,
  2. RV首先会通过getViewForPosition() 检查cache, 如果cache里有这个view,则直接把这个view返回给LM.
  3. 如果cache里没有这个View,这个时候RV会先找到Adapter,通过getViewType()询问这个position对应的view type是什么,拿到viewType后,又转向Recycled Pool里通过getViewHolderByType()去找这个View。
    如果找到了,在通过Adapter调用bindViewHolder()为这个view重新赋值后,把这个view返回给LM。
  4. 如果在Pool里还是没有找到,RV则会再要求Adapter调用createViewHolder()重新创建一个这个view返回给LM。
  5. 当LM最终通过getViewForPosition()得到它想要的view时,会调用addView()将这个view作为一个child添加到RV中,并且这时RV会调用onChildViewAttachedToWindow()通知Adapter有一个view添加进来了,你可以使用它哦。
2. Reserves 回收存储
  1. LM使用完一个view后,调用removeAndRecycleView()通知RV它已经使用完这个view了,需要remove(移除)并且recycle(回收)这个view。
  2. removeAndRecycleView()首先调用了childHelper的removeView(),实际上最终调用的是RV里的removeViewAt(),
  3. 在removeViewAt()里,RV会调用onViewDetachedFromWindow()通知Adapter,它移除了这个view
  4. 接着removeAndRecycleView()调用了Recycler的recycleView()
  5. Recycler不会直接销毁这个view,而是尝试缓存这个view,以防下次LM突然需要这个view时能迅速返回。所以这时会判断这个view是否为valid的状态,如果这个view(viewholder)包含FLAG_INVALID、FLAG_REMOVED、FLAG_UPDATE、FLAG_ADAPTER_POSITION_UNKNOWN中的任何一个状态,则为not valid, 否则为valid。
  6. 如果上述view为valid,则会把这个view存放在cache(mCachedViews)中。
  7. cache里存满了之后,cache里最老的view会通过recycleCachedViewAt(0)方法调用的addViewHolderToRecycledViewPool(viewHolder, true)添加到Recycled Pool中,并且从cache中删除最老的这个view。
  8. 接收到从cache转来的最老的view后,Recycler会通过dispatchViewRecycled()调用Adapter的onViewRecycled()通知Adapter最老的cache view已经回收。
  9. 如果5中判断此view不为valid,则会直接送进Recycled Pool。
3. Fancy Reserves
4.Death 1
  1. 前几步都与Reserves阶段相同
  2. 在Recycler的recycleView()中,判定是否hasTransientState; TransientState表示这个view有动画正在执行,例如淡出等动画,此时这个view是不能被其他viewholder复用的
  3. 当出现这种有transientState情况的时候,此view不能被复用,也就不能放入RecyclePool中。Recycler会调用Adapter的onFailedToRecycleView(),此方法默认是返回false
  4. 然后直接将这个holder从view holder list中remove掉
  5. 并且设置 holder.mOwnerRecyclerView为空,真正宣判次holder的生命结束
  6. 如果3中,adapter重写了onFailedToRecycleView()方法,并且返回了true,那么这个view也是可以被强行回收的
4.Death 2
1
2
3
4
5
6
7
8
9
10
11
12
13
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
//达到上限了,不缓存
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
  1. RecyclerView使用view完毕了需要将这个view放到Pool中缓存
  2. 在putRecycledView()方法中,如果判定这个view的type的VH已经有缓存了,而且达到了上限,那么么这个view也不会被缓存进来,随后被销毁掉。
    即本次不缓存的原因:已经有很多同类型的VH在池子里了。
5.ChildHelper

用于解决LM与ItemAnimator之间对于view的争夺情况
CH为LM提供了一个虚拟的children list
当LM想remove一个child的时候,会向RV请求,此时RV并不会直接走ViewGruop的remove child方法,而是委托给CH去处理这个事情。
如果此时ItemAnimator想要对这个child做一些动画,那么CH是不会立即remove这个child的,而是等ItemAnimator动画完毕后,才真正从ViewGroup里删除这个child。
但是CH对于LM提供了一个虚拟的children list, 在这个list里面这个child是被删除了的。

所以如果正在执行一个child的删除动作时,LM的getChildAt(i)与RV的getChildAt(i)返回的是有差异的。LM中返回的其实是ChildHelper给出的已经删除这个child之后的结构,RV返回的其实是ViewGroup的结构(删除动画执行完毕后,此child才会从ViewGroup中删除)