@universal
2018-07-18T09:51:42.000000Z
字数 12465
阅读 505
view
在上篇中我们简单分析了下listview的layout过程和回收机制,那这里我们将对RecyclerView的回收机制进行一波带走,please ready!
在ListView的复用中,主要是以View为单位的,而在RecyclerView中主要是以ViewHolder为单位,其实这一点我们从各自的Adapter中就可以看出:
ListView:主要重写createViewFromResource()和getView()方法。
RecyclerView:主要重写onCreateViewHolder()和onBindViewHolder()方法。而且RecyclerView已经帮我们实现了view的复用。
注:这里的ViewHolder也不像ListView中一样仅仅保存一个view,RecyclerView中的ViewHolder不仅仅包含view,还包括view的当前position、以前的position、itemId、itemViewType、当前的状态信息等等,这些丰富的信息可以把每一个view的状态区分开来,为我们RecyclerView实现局部刷新的功能打下了基础。
我们知道,RecyclerView的回收复用主要靠Recycler来实现。首先,RecyclerView直接继承自ViewGroup,而Recycler则是RecyclerView中的final类,那么来看看这个类:
/*** A Recycler is responsible for managing scrapped or detached item views for reuse.*/public final class Recycler {final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();ArrayList<ViewHolder> mChangedScrap = null;final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();private final List<ViewHolder>mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;int mViewCacheMax = DEFAULT_CACHE_SIZE;RecycledViewPool mRecyclerPool;private ViewCacheExtension mViewCacheExtension;static final int DEFAULT_CACHE_SIZE = 2;.....}
官方解释:Recycler是用来管理废弃view的复用。
可以看到Recycler的属性中有mAttachedScrap、mChangedScrap、mCachedViews、mRecyclerPool、mViewCacheExtension这几个管理View的集合,这也对应了Recycler中的四级缓存:
scrap view:mAttachedScrap、mChangedScrap。官方对scrapview的解释是:已标记为要删除或重复使用但仍然与其父RecyclerView绑定的视图。在这一级,mAttachedScrap是用来缓存屏幕上的viewholder(主要在recyclerview的layout过程中的进行),而mChangedScrap则回收屏幕上数据有变化的viewholder。
mCachedViews:缓存屏幕外的ViewHolder,默认为2个。
mViewCacheExtension:需要用户定制,默认不实现。ViewCacheExtension是一个抽象类,帮助开发者缓存view、自定义缓存控制策略。
mRecyclerPool:缓存池,多个RecyclerView共用,但存在这里的 ViewHolder 的数据信息会被重置掉,所以需要重新调用 onBindViewHolder 来绑定数据。每种type的pool容量为5。每种type的底层是一个ArrayList,然后所有的ArrayList又放在map(sparseArray)中
Recycler类的入口(被调用最多的)是 getViewForPosition()方法,也是复用的核心入口。方法返回我们想要的view(复用的or重新创建的)。
public View getViewForPosition(int position) {return getViewForPosition(position, false);}View getViewForPosition(int position, boolean dryRun) {return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;}
最终调用的还是tryGetViewHolderForPositionByDeadline()方法,下面的分析主要都是这个方法里的逻辑:
/*** Attempts to get the ViewHolder for the given position, either from the Recycler scrap,* cache, the RecycledViewPool, or creating it directly.*/@NullableViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs){ViewHolder holder = null;// If there is a changed scrap, try to find from thereif (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 1) Find by position from scrap/hidden list/cacheif (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);...}}/*** Returns a view for the position either from attach scrap, hidden children, or cache.*/ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {// Try first for an exact, non-invalid match from scrap.for (int i = 0; i < scrapCount; i++) {final ViewHolder holder = mAttachedScrap.get(i);if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())){holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);return holder;}}}
可以看到首先会去mChangedScrap寻找我们想要的view,如果没有再去调用getScrapOrHiddenOrCachedHolderForPosition()方法去scarpview中寻找,可以看出,先遍历mAttachedScrap集合寻找对应的viewholder。其实在上面已经提到了mAttachedScrap主要是在recyclerview重新layout的时候回收当前在屏幕上显示的viewholder,便于下次attach,这一点是和listview的activeViews类似的,它们的生命周期都是layout过程。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {.....//省略上面的代码if (!dryRun) {View view = mChildHelper.findHiddenNonRemovedView(position);if (view != null) {// This View is good to be used. We just need to unhide, detach and move to the// scrap list.final ViewHolder vh = getChildViewHolderInt(view);mChildHelper.unhide(view);int layoutIndex = mChildHelper.indexOfChild(view);if (layoutIndex == RecyclerView.NO_POSITION) {throw new IllegalStateException("layout index should not be -1 after "+ "unhiding a view:" + vh);}mChildHelper.detachViewFromParent(layoutIndex);scrapView(view);vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP|ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);return vh;}}// Search in our first-level recycled view cache.final int cacheSize = mCachedViews.size();for (int i = 0; i < cacheSize; i++) {final ViewHolder holder = mCachedViews.get(i);// invalid view holders may be in cache if adapter has stable ids as they can be// retrieved via getScrapOrCachedViewForIdif (!holder.isInvalid() && holder.getLayoutPosition() == position) {if (!dryRun) {mCachedViews.remove(i);}if (DEBUG) {Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position+ ") found match in cache: " + holder);}return holder;}}}
继续看下一步,这dryRun值是从getViewForPosition这里传入的false,官方解释为如果为true代表viewholder不能从scrap或cache中移除。emmm.....不懂,先跳过,反正这里为false,继续往下。进入if,去mHiddenViews寻找合适的view,这里的mHiddenViews主要是存储当前隐藏起来的view,其实我们平时常用的滑动复用用不到这一块,所以跳过继续向下走。接下来就会去mCachedViews中寻找,如果没有则返回null。
其实这一块已经将从scrap或cache中的复用走了一遍,那我们回到tryGetViewHolderForPositionByDeadline()继续向下走
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {if (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {if (!validateViewHolderForOffsetPosition(holder)) {// recycle holder (and unscrap if relevant) since it can't be usedif (!dryRun) {// we would like to recycle this but need to make sure it is not used by// animation logic etc.holder.addFlags(ViewHolder.FLAG_INVALID);if (holder.isScrap()) {removeDetachedView(holder.itemView, false);holder.unScrap();} else if (holder.wasReturnedFromScrap()) {holder.clearReturnedFromScrapFlag();}recycleViewHolderInternal(holder);}holder = null;} else {fromScrapOrHiddenOrCache = true;}}}}
如果找到viewholder的话,判断viewholder是否符合条件,比如是否已经被remove、type and id是否匹配,如果不符合条件返回null。
if (holder == null) {final int type = mAdapter.getItemViewType(offsetPosition);// 2) Find from scrap/cache via stable ids, if existsif (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);if (holder != null) {// update positionholder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}}if (holder == null && mViewCacheExtension != null) {// We are NOT sending the offsetPosition because LayoutManager does not know it.final View view = mViewCacheExtension.getViewForPositionAndType(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 (holder.shouldIgnore()) {throw new IllegalArgumentException("getViewForPositionAndType returned"+ " a view that is ignored. You must call stopIgnoring before"+ " returning this view.");}}}}
继续向下找,这一块有两部分,第一部分还是从scrap和cache view中寻找,只不过是根据id来找,逻辑和上面分析的通过position查找类似;第二部分是从mViewCacheExtension中寻找,这部分缓存是由我们自定义的,默认为null,所以跳过。继续向下走
if (holder == null) {holder = getRecycledViewPool().getRecycledView(type);if (holder != null) {holder.resetInternal();if (FORCE_INVALIDATE_DISPLAY_LIST) {invalidateDisplayListInt(holder);}}}public ViewHolder getRecycledView(int viewType) {final ScrapData scrapData = mScrap.get(viewType);if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;return scrapHeap.remove(scrapHeap.size() - 1);}return null;}
下一步就到了pool,这一步就是从mRecyclerPool中根据type寻找viewholder,可以看到首先判断pool中是否缓存有该type的viewholder,若无返回null,若有则返回pool中最后一个viewholder缓存。拿出来就可以直接用,但是这里通过resetInternal()重置了数据,变成一张白纸,需要等待重新绑定数据。
if (holder == null) {...holder = mAdapter.createViewHolder(RecyclerView.this, type);...mRecyclerPool.factorInCreateTime(type, end - start);}
继续向下走,如果holder还为null,则调用mAdapter.createViewHolder(),重新创建新的viewholder。并且在这一步会去mRecyclerPool创建对应type的ScrapData。
//从scrap和cache中获取的则不需要rebindboolean bound = false;if (mState.isPreLayout() && holder.isBound()) {// do not update unless we absolutely have to.holder.mPreLayoutPosition = position;} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {if (DEBUG && holder.isRemoved()) {throw new IllegalStateException("Removed holder should be bound and it should"+ " come here only in pre-layout. Holder: " + holder);}final int offsetPosition = mAdapterHelper.findPositionOffset(position);bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}//设置布局参数final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();final LayoutParams rvLayoutParams;if (lp == null) {rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();holder.itemView.setLayoutParams(rvLayoutParams);} else if (!checkLayoutParams(lp)) {rvLayoutParams = (LayoutParams) generateLayoutParams(lp);holder.itemView.setLayoutParams(rvLayoutParams);} else {rvLayoutParams = (LayoutParams) lp;}rvLayoutParams.mViewHolder = holder;rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;return holder;
tryGetViewHolderForPositionByDeadline()的这点尾巴主要是对viewholder重新绑定数据、检查和设置布局参数。可以看到在rebind的判定条件里,像holder.isBound()这样的,从源码中可以看出(这里就不截取了),都是对mFlags进行不为0的判断,但是像上面提到的重置viewholder后,就会将mFlags设为0,所以就进入if语句中调用tryBindViewHolderByDeadline()方法,从而继续去调用mAdapter.bindViewHolder()。最后返回一个我们需要的holder。
到这里,recycler的复用机制已经大概明了了,可以大致理解成下图:

图中的每一步都是需要判断条件的,所以在不同的场景下可能会有不同的“解法”,每一步都有可能输出对应的viewholder,而且在上述的分析过程也省略了一些情况,像notifyDataSetChanged()或者重新setAdapter这些场景,都会触发不同的缓存策略,这里只是笼统的介绍了大致流程,遇到具体需求还得具体分析。
上面我们简单分析了下怎么拿,下面我们看看怎么放,因为回收的入口比较多,所以我们就挑个常见的滑动场景进行分析。
滑动recyclerview的时候,由layoutmanager的removeAndRecycleView()进行回收,最后会调用Recycler的recycleView()方法
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 (holder.isTmpDetached()) {removeDetachedView(view, false);}if (holder.isScrap()) {holder.unScrap();} else if (holder.wasReturnedFromScrap()){holder.clearReturnedFromScrapFlag();}recycleViewHolderInternal(holder);}
首先判断view是否是attached状态,如果是,则需要remove并且进行detached,我们在上面分析过,attached的view会存放在mAttachedScrap缓存中,这里面的view是不需要rebind的,所以这里对view解绑并且从scrap中移除。后面主要就是recycleViewHolderInternal()的逻辑。
void recycleViewHolderInternal(ViewHolder holder) {....if (forceRecycle || holder.isRecyclable()) {if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {// Retire oldest cached viewint cachedViewSize = mCachedViews.size();if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);//核心cachedViewSize--;}int targetCacheIndex = cachedViewSize;if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {// when adding the view, skip past most recently prefetched viewsint cacheIndex = cachedViewSize - 1;while (cacheIndex >= 0) {int cachedPos = mCachedViews.get(cacheIndex).mPosition;if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;}cacheIndex--;}targetCacheIndex = cacheIndex + 1;}//如果未满将最新的viewholder添加到cacheview中mCachedViews.add(targetCacheIndex, holder);//核心cached = true;}//核心if (!cached) {//添加到pool中addViewHolderToRecycledViewPool(holder, true);recycled = true;}}...}//下面的方法都是延伸展示的方法,方便理解void recycleCachedViewAt(int cachedViewIndex) {......addViewHolderToRecycledViewPool(viewHolder, true);mCachedViews.remove(cachedViewIndex);}void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {clearNestedRecyclerViewIfNotNested(holder);ViewCompat.setAccessibilityDelegate(holder.itemView, null);if (dispatchRecycled) {//回调adapter中的onViewRecycled方法dispatchViewRecycled(holder);}holder.mOwnerRecyclerView = null;getRecycledViewPool().putRecycledView(holder);}public void putRecycledView(ViewHolder scrap) {final int viewType = scrap.getItemViewType();//从map中获取对应type的ArrayListfinal ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {return;}}if (DEBUG && scrapHeap.contains(scrap)) {throw new IllegalArgumentException("this scrap item already exists");}scrap.resetInternal();scrapHeap.add(scrap);}private ScrapData getScrapDataForType(int viewType) {ScrapData scrapData = mScrap.get(viewType);if (scrapData == null) {scrapData = new ScrapData();mScrap.put(viewType, scrapData);}return scrapData;}
recycleViewHolderInternal()里面的逻辑主要是对mCachedViews和pool的操作。这里会保证mCachedViews不超过默认大小2(mViewCacheMax),一旦大于或等与这个值,就会将mcachedViews中的0号位置的viewholder放入pool中,如果不存在这种type的pool,则会去创建新的scrapData,所以一般来说在滑动recyclerview的过程中,一开始除了会创建所能看见的一屏的item数量的viewholder,后面如果继续滑动还会创建3个新的包括cacheviews中的2个和pool中的1个,之后的滑动才会对这些viewholder进行复用(这里假定一行只有一个item)。
其实,mcachedViews中存储的一直都是要回收的holder,而且因为mcachedViews中holder复用的条件较高,需要是原来位置的item才可以获取到,所以pool中的holder才是我们要复用调用的holder。
到这里,recyclerview的回收复用机制就基本过了一遍,笔记有点长,大家可以分开来看。其实recycler有些地方与listview比较像,像layout过程中scrapview的思想,但是也可以看出来RecyclerView更加注重view的回收复用,它将view的layout等操作都交给了layoutmanager,这些等后面有时间了再分析一波。