2023年7月15日发(作者:)
Android基础知识-RecyclerView的复⽤和回收机制ListView复⽤、回收eBinRecycleBin 维护了两种类型列表,⼀种⽤于保存屏幕上可见的View,⼀个⽤于缓存滚动出屏幕的ViewListView滑动过程中,⼦View完全移出屏幕后,会加⼊RecycleBin 缓存⼦View进⼊屏幕时,从RecycleBin 中获取缓存View,⽤于数据绑定。RecyclerView 复⽤、回收滚动屏幕时,列表先执⾏ 复⽤流程、再执⾏回收流程复⽤流程:2级缓存 mCachedViews 取 > 1级缓存 RecycledViewPool 取 > teViewHolder()回收流程:遍历移除屏幕的 View,从 View的 LayoutParams 中取出 ViewHolder,塞⼊ 2级缓存 mCachedViews 如果 mCachedViews 满了(容量2),则 mCachedViews 移除第⼀个,⽤来放要回收的 ViewHolder 如果 RecycledViewPool 对应 viewType 的 List 没满(容量5),则从 mCachedViews 移除的 ViewHolder 放⼊RecycledViewPool。1、复⽤Recycler ⽤来缓存 ViewHolder 的结构体Recycler 缓存结构未滑动的操作 触发 RecyclerView layout 过程
List
存放暂时 remove 的 ViewHolderRecyclerView 会先把所有 children 先 remove 掉
然后重新 add 上去 完成⼀次 layout 的过程
暂时 remove 的 viewHolder 存放在 mAttachedScrapList
滑动时⼀级缓存 mCachedViews⼀级缓存没找到会 从⼆级缓存 RecyclerViewPool 取RecyclerViewPool 中没找到
teViewHolder 创建 ViewHolder回收回收流程滑动列表LinearLayoutManager#scrollVerticallyByLayoutManager#fillRecycler # recyclerViewRecycler#recycleViewHolderInternalmCachedView 已满mCachedView 未满mCachedViews 中移除第⼀个将最新的 ViewHolder放⼊ mCachedViewsRecycledViewPool中对应viewType的List已满RecycledViewPool中对应viewType的 List 未满存⼊ mCachedViews丢弃mCachedViews 中移除的ViewHolder把 mCachedViews 中移除的ViewHolder
数据重置后后放⼊ RecycledViewPool如果 mCachedViews 超过了存储容量( 默认容量 2 ),移除 mCachedViews 第⼀个元素,放⼊RecycledViewPool如果 RecycledViewPool对应 viewType 的 List 满了(默认每个 viewType 缓存容量 5),则丢弃 mCachedViews 中移除的 ViewHolder局部刷新1、notifyItemChanged(int position, Object payload)payload参数,传⼊⾮空Object对象2、onBindViewHolder(RecyclerHolder holder, int position, List payloads) 永远是 1,根据 payload参数值,实现局部刷新private static final String DATA_ONE = "dataOne";private static final String DATA_TWO = "dataTwo";notifyItemChanged(position, changePos == 0 ? DATA_ONE : DATA_TWO);@Overridepublic void onBindViewHolder(RecyclerHolder holder, int position, List
/* ⼀次遥控器按键的操作,不管有没有发⽣滑动,都会导致 RecyclerView 的重新 onLayout, * 那要 layout 的话,RecyclerView 会先把所有 children 先 remove 掉,然后再重新 add 上去,完成⼀次 layout 的过程。 * 那么这暂时性的 remove 掉的 viewHolder 要存放在哪呢,就是放在这个 mAttachedScrap 中了 */ final ArrayList
//应该是在当数据发⽣变化时才会涉及到的复⽤场景 ArrayList
/* 滑动过程中的回收和复⽤都是先处理的这个 List, * 这个集合⾥存的 ViewHolder 的原本数据信息都在, * 所以可以直接添加到 RecyclerView 中显⽰,不需要再次重新 onBindViewHolder() */ final ArrayList
if (holder == null && mViewCacheExtension != null) { // We are NOT sending the offsetPosition because LayoutManager does not // know it. final View view = wForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); if (holder == null) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder"); } else if (Ignore()) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view."); } } } /** * 【4】去 RecyclerViewPool ⾥取 ViewHolder, * ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认⼤⼩为5个。 */ if (holder == null) { // fallback to pool if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + position + ") fetching from shared pool"); } /* * ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder, * 那么复⽤的时候,只要 ViewPool ⾥相同的 type 有 ViewHolder 缓存的话, * 就将最后⼀个拿出来复⽤,不⽤像 mCachedViews 需要各种匹配条件,只要有就可以复⽤。 */ holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { /* 重置 ViewHolder,这样 ViewHolder 就可以当作⼀个全新的 ViewHolder 来使⽤了, /* 重置 ViewHolder,这样 ViewHolder 就可以当作⼀个全新的 ViewHolder 来使⽤了, * 这也就是为什么从这⾥拿的 ViewHolder 都需要重新 onBindViewHolder() 了。 */ nternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } /** * 【5】调⽤ teViewHolder()来创建⼀个新的ViewHolder * 如果再ViewPool 中都找不到 ViewHolder,就要创建新的 ViewHolder */ if (holder == null) { long start = getNanoTime(); if (deadlineNs != FOREVER_NS && !eateInTime(type, start, deadlineNs)) { // abort - we have a deadline we can't meet return null; } holder = ViewHolder(, type); if (ALLOW_THREAD_GAP_WORK) { // only bother finding nested RV if prefetching RecyclerView innerView = findNestedRecyclerView(ew); if (innerView != null) { dRecyclerView = new WeakReference<>(innerView); } } long end = getNanoTime(); InCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } ...}/** * ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认⼤⼩为5个。 */public static class RecycledViewPool { /* * ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder, * 那么复⽤的时候,只要 ViewPool ⾥相同的 type 有 ViewHolder 缓存的话, * 就将最后⼀个拿出来复⽤,不⽤像 mCachedViews 需要各种匹配条件,只要有就可以复⽤。 */ public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = (viewType); if (scrapData != null && !y()) { final ArrayList
}}重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑动等等之类的场景,只要重新layout,就会去回收和复⽤ ViewHolder,所以这个复⽤机制需要考虑到各种各样的场景。从以上步骤看,只考虑滑动场景,RecyclerView ⾄少有 2 级缓存,mCachedViews为⼀级缓存:该缓存的ViewHolder数据还在,只有原来的position可⽤,不需要重新绑定数据。RecyclerViewPool 为⼆级缓存:该缓存的ViewHolder数据会重置,需要重新绑定数据。2、回收机制回收机制的⼊⼝就有很多了,因为 Recycler 有各种结构体,⽐如mAttachedScrap,mCachedViews 等等,不同结构体回收的时机都不⼀样,⼊⼝也就多了。在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,然后 LayoutManager 会接着调⽤ fill() ⽅法去处理需要复⽤和回收的卡位,最终会调⽤ erView() 这个⽅法开始进⾏回收⼯作。public final class Recycler {
public void recycleView(View view) { // This public recycle method tries to make view recycle-able since layout manager // intended to recycle this view (e.g. even if it is in scrap or change cache) ViewHolder holder = getChildViewHolderInt(view); if (etached()) { removeDetachedView(view, false); } if (p()) { p(); } else if (urnedFromScrap()) { eturnedFromScrapFlag(); } //真正回收 recycleViewHolderInternal(holder); }
void recycleViewHolderInternal(ViewHolder holder) { ... boolean cached = false; boolean recycled = false; if (DEBUG && ns(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder); } if (forceRecycle || clable()) { if (mViewCacheMax > 0 && !OfTheFlags(_INVALID | _REMOVED | _UPDATE | _ADAPTER_POSITION_UNKNOWN)) {
/* 如果mCacheViews超过了存储容量, * 就从mCacheViews⾥⾯取第⼀个放⼊RecyclerViewPool中 */ int cachedViewSize = (); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !efetchIncludedPosition(ion)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = (cacheIndex).mPosition; if (!efetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; targetCacheIndex = cacheIndex + 1; } //将最新的ViewHolder放⼊mCachedViews中 (targetCacheIndex, holder); cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { // NOTE: A view can fail to be recycled when it is scrolled off while an animation // runs. In this case, the item is eventually recycled by // ItemAnimatorRestoreListener#onAnimationFinished. // TODO: consider cancelling an animation when an item is removed scrollBy, // to return it to the pool faster if (DEBUG) { Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + "re-visit here. We are still removing it from animation lists"); } } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. ViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { RecyclerView = null; } }}回收的逻辑⽐较简单,由 LayoutManager 来遍历移出屏幕的卡位,然后对每个卡位进⾏回收操作。mCachedViews 优先级⾼于 RecyclerViewPool,回收时,都是把 ViewHolder 放在 mCachedViews ⾥⾯,如果 mCachedViews 满了,那就在 mCachedViews ⾥拿⼀个 ViewHolder 扔到 ViewPool 缓存⾥,然后 mCachedViews 就可以空出位置来放新回收的 ViewHolder 了。复⽤时,也是先到 mCachedViews ⾥找 ViewHolder,但需要各种匹配条件,概括⼀下就是只有原来位置的卡位可以复⽤存在 mCachedViews ⾥的 ViewHolder,如果 mCachedViews ⾥没有,那么才去 ViewPool ⾥找。在 ViewPool ⾥的 ViewHolder 都是跟全新的 ViewHolder ⼀样,只要 type ⼀样,有找到,就可以拿出来复⽤,重新绑定下数据即可。3、问题Q1:如果向下滑动,新⼀⾏的5个卡位的显⽰会去复⽤缓存的 ViewHolder,第⼀⾏的5个卡位会移出屏幕被回收,那么在这个过程中,是先进⾏复⽤再回收?还是先回收再复⽤?还是边回收边复⽤?也就是说,新⼀⾏的5个卡位复⽤的 ViewHolder 有可能是第⼀⾏被回收的5个卡位吗?答:先复⽤再回收,新⼀⾏的5个卡位先去⽬前的 mCachedViews 和 ViewPool 的缓存中寻找复⽤,没有就重新创建,然后移出屏幕的那⾏的5个卡位再回收缓存到 mCachedViews 和 ViewPool ⾥⾯,所以新⼀⾏5个卡位和复⽤不可能会⽤到刚移出屏幕的5个卡位。Q2: 向下滑动⼀⾏后再上滑⼀⾏,在这个过程中,为什么当 RecyclerView 再次向上滑动重新显⽰第⼀⾏的5个卡位时,只有后⾯3个卡位触发了 onBindViewHolder() ⽅法,重新绑定数据呢?明明5个卡位都是复⽤的。答:滑动场景下涉及到的回收和复⽤的结构体是 mCachedViews 和 ViewPool,前者默认⼤⼩为2,后者为5。所以,当第三⾏显⽰出来后,第⼀⾏的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到 ViewPool ⾥,所以5个卡位有2个缓存在 mCachedViews ⾥,3个缓存在 ViewPool,⾄于是哪2个缓存在 mCachedViews,这是由 LayoutManager 控制。上⾯讲解的例⼦使⽤的是 GridLayoutManager,滑动时的回收逻辑则是在⽗类 LinearLayoutManager ⾥实现,回收第⼀⾏卡位时是从后往前回收(最后回收的两个会放在 mCachedViews),所以最新的两个卡位是0、1,会放在 mCachedViews ⾥,⽽2、3、4的卡位则放在 ViewPool ⾥。所以,当再次向上滑动时,第⼀⾏5个卡位会去两个结构体⾥找复⽤,之前说过,mCachedViews ⾥存放的 ViewHolder 只有原本位置的卡位才能复⽤,所以0、1两个卡位都可以直接去 mCachedViews ⾥拿 ViewHolder 复⽤,⽽且这⾥的 ViewHolder 是不⽤重新绑定数据的,⾄于2、3、4卡位则去 ViewPool ⾥找,刚好 ViewPool ⾥缓存着3个 ViewHolder,所以第⼀⾏的5个卡位都是⽤的复⽤的,⽽从 ViewPool ⾥拿的复⽤需要重新绑定数据,才会这样只有三个卡位需要重新绑定数据。Q3:接下去不管是向上滑动还是向下滑动,滑动⼏次,都不会再有 onCreateViewHolder() 的⽇志了,也就是说 RecyclerView 总共创建了17个 ViewHolder,但有时⼀⾏的5个卡位只有3个卡位需要重新绑定数据,有时却⼜5个卡位都需要重新绑定数据,这是为什么呢?答:有时⼀⾏只有3个卡位需要重新绑定的原因跟Q2⼀样,因为 mCachedView ⾥正好缓存着当前位置的 ViewHolder,本来就是它的 ViewHolder 当然可以直接拿来⽤。⽽⾄于为什么会创建了17个 ViewHolder,那是因为再第四⾏的卡位要显⽰出来时,ViewPool ⾥只有3个缓存,⽽第四⾏的卡位⼜⽤不了 mCachedViews ⾥的2个缓存,因为这两个缓存的是0、1卡位的 ViewHolder,所以就需要再重新创建2个 ViewHodler 来给第四⾏最后的两个卡位使⽤。⼆、1、Adapteradapter 在 ListView 和数据源之间起到了⼀个桥梁的作⽤,ListView 不会直接和数据源打交道,⽽是会借助 Adapter 这个桥梁来去访问真正的数据源2、RecycleBin 机制(eBin)RecycleBin 维护了两种类型列表,⼀个⽤于保存展⽰在屏幕上的View,⼀个⽤于缓存滚动出屏幕的ViewListView滑动过程中,⼦View完全移出屏幕后,会加⼊RecycleBin 缓存⼦View进⼊屏幕时,从RecycleBin 中获取缓存View,⽤于数据绑定。 public abstract class AbsListView extends AdapterView
class RecycleBin { private RecyclerListener mRecyclerListener;
//表⽰ListView中第⼀个可见元素的position值 private int mFirstActivePosition;
/* 屏幕上可见的View,存放在 mActiveViews 数组中 * Head Foot,不会存在 mActiveViews ⾥ */ private View[] mActiveViews = new View[0];
/** *离开屏幕后,View会被移动到 mScrapViews 数组中 * mViewTypeCount > 1时,存储该数组中。 * 数组下标为viewType,数组元素为该viewType对应的废弃view列表 */ private ArrayList
/** * ⽤于存储废弃(滚动出屏幕)的View, * * * 只有⼀种数据类型,ViewTypeCount == 1时,存该列表中 */ private ArrayList
/** * ListView中所有屏幕上可见的View *
* @param childCount 表⽰要存储的view的数量 * @param firstActivePosition 表⽰ListView中第⼀个可见元素的position值 */ void fillActiveViews(int childCount, int firstActivePosition) { if ( < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); Params lp = (Params) outParams(); if (lp != null && pe != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { //View塞⼊数组中 activeViews[i] = child; ... } } }
/** * 获取 ListView中 position 位置的 View * * mActiveViews当中所存储的View,⼀旦被获取了之后就会从mActiveViews当中移除, * 下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利⽤ *
* @param position 表⽰元素在ListView当中的位置 */ View getActiveView(int position) { //将position值转换成mActiveViews数组对应的下标值 int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >= 0 && index < ) { //从数组中取出匹配的View final View match = activeViews[index]; //View取出后,删除数组中对应位置的数据 activeViews[index] = null; return match; } return null;//没有找到,返回null }
/** * 将⼀个废弃(滚动出屏幕)的View进⾏缓存 * head foot不会缓存 */ void addScrapView(View scrap) { Params lp = (Params) outParams(); if (lp == null) { return; } // Don't put header or footer views or views that should be ignored // into the scrap heap int viewType = pe; //viewType < 0 时,会返回false if (!shouldRecycleViewType(viewType)) { if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } ... if (mViewTypeCount == 1) {//只有⼀种布局类型 dispatchFinishTemporaryDetach(scrap); (scrap);//存到 mCurrentScrap 列表中 } else { dispatchFinishTemporaryDetach(scrap); //多布局,根据viewType存到 mScrapViews 数组中 mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { dToScrapHeap(scrap); } }
/** * 根据position 从废弃缓存中取出⼀个View * 这些废弃View,没有顺序,直接从列表尾部获取⼀个 scrapView 返回 */ View getScrapView(int position) { ArrayList
public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } // noinspection unchecked ArrayList
发布者:admin,转转请注明出处:http://www.yc00.com/news/1689430243a246955.html
评论列表(0条)