[关闭]
@universal 2018-07-18T09:51:42.000000Z 字数 12465 阅读 444

RecycleBin/Recycler的回收机制(下)

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实现局部刷新的功能打下了基础。


Recycler

我们知道,RecyclerView的回收复用主要靠Recycler来实现。首先,RecyclerView直接继承自ViewGroup,而Recycler则是RecyclerView中的final类,那么来看看这个类:

  1. /**
  2. * A Recycler is responsible for managing scrapped or detached item views for reuse.
  3. */
  4. public final class Recycler {
  5. final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
  6. ArrayList<ViewHolder> mChangedScrap = null;
  7. final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  8. private final List<ViewHolder>
  9. mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
  10. private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
  11. int mViewCacheMax = DEFAULT_CACHE_SIZE;
  12. RecycledViewPool mRecyclerPool;
  13. private ViewCacheExtension mViewCacheExtension;
  14. static final int DEFAULT_CACHE_SIZE = 2;
  15. .....
  16. }

官方解释:Recycler是用来管理废弃view的复用。
可以看到Recycler的属性中有mAttachedScrap、mChangedScrap、mCachedViews、mRecyclerPool、mViewCacheExtension这几个管理View的集合,这也对应了Recycler中的四级缓存:


复用

Recycler类的入口(被调用最多的)是 getViewForPosition()方法,也是复用的核心入口。方法返回我们想要的view(复用的or重新创建的)。

  1. public View getViewForPosition(int position) {
  2. return getViewForPosition(position, false);
  3. }
  4. View getViewForPosition(int position, boolean dryRun) {
  5. return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
  6. }

最终调用的还是tryGetViewHolderForPositionByDeadline()方法,下面的分析主要都是这个方法里的逻辑:

  1. /**
  2. * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
  3. * cache, the RecycledViewPool, or creating it directly.
  4. */
  5. @Nullable
  6. ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)
  7. {
  8. ViewHolder holder = null;
  9. // If there is a changed scrap, try to find from there
  10. if (mState.isPreLayout()) {
  11. holder = getChangedScrapViewForPosition(position);
  12. fromScrapOrHiddenOrCache = holder != null;
  13. }
  14. // 1) Find by position from scrap/hidden list/cache
  15. if (holder == null) {
  16. holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
  17. ...
  18. }
  19. }
  20. /**
  21. * Returns a view for the position either from attach scrap, hidden children, or cache.
  22. */
  23. ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
  24. // Try first for an exact, non-invalid match from scrap.
  25. for (int i = 0; i < scrapCount; i++) {
  26. final ViewHolder holder = mAttachedScrap.get(i);
  27. if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
  28. && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved()))
  29. {
  30. holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
  31. return holder;
  32. }
  33. }
  34. }

可以看到首先会去mChangedScrap寻找我们想要的view,如果没有再去调用getScrapOrHiddenOrCachedHolderForPosition()方法去scarpview中寻找,可以看出,先遍历mAttachedScrap集合寻找对应的viewholder。其实在上面已经提到了mAttachedScrap主要是在recyclerview重新layout的时候回收当前在屏幕上显示的viewholder,便于下次attach,这一点是和listview的activeViews类似的,它们的生命周期都是layout过程。

  1. ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
  2. .....//省略上面的代码
  3. if (!dryRun) {
  4. View view = mChildHelper.findHiddenNonRemovedView(position);
  5. if (view != null) {
  6. // This View is good to be used. We just need to unhide, detach and move to the
  7. // scrap list.
  8. final ViewHolder vh = getChildViewHolderInt(view);
  9. mChildHelper.unhide(view);
  10. int layoutIndex = mChildHelper.indexOfChild(view);
  11. if (layoutIndex == RecyclerView.NO_POSITION) {
  12. throw new IllegalStateException("layout index should not be -1 after "
  13. + "unhiding a view:" + vh);
  14. }
  15. mChildHelper.detachViewFromParent(layoutIndex);
  16. scrapView(view);
  17. vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP|ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
  18. return vh;
  19. }
  20. }
  21. // Search in our first-level recycled view cache.
  22. final int cacheSize = mCachedViews.size();
  23. for (int i = 0; i < cacheSize; i++) {
  24. final ViewHolder holder = mCachedViews.get(i);
  25. // invalid view holders may be in cache if adapter has stable ids as they can be
  26. // retrieved via getScrapOrCachedViewForId
  27. if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
  28. if (!dryRun) {
  29. mCachedViews.remove(i);
  30. }
  31. if (DEBUG) {
  32. Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
  33. + ") found match in cache: " + holder);
  34. }
  35. return holder;
  36. }
  37. }
  38. }

继续看下一步,这dryRun值是从getViewForPosition这里传入的false,官方解释为如果为true代表viewholder不能从scrap或cache中移除。emmm.....不懂,先跳过,反正这里为false,继续往下。进入if,去mHiddenViews寻找合适的view,这里的mHiddenViews主要是存储当前隐藏起来的view,其实我们平时常用的滑动复用用不到这一块,所以跳过继续向下走。接下来就会去mCachedViews中寻找,如果没有则返回null。
其实这一块已经将从scrap或cache中的复用走了一遍,那我们回到tryGetViewHolderForPositionByDeadline()继续向下走

  1. ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
  2. if (holder == null) {
  3. holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
  4. if (holder != null) {
  5. if (!validateViewHolderForOffsetPosition(holder)) {
  6. // recycle holder (and unscrap if relevant) since it can't be used
  7. if (!dryRun) {
  8. // we would like to recycle this but need to make sure it is not used by
  9. // animation logic etc.
  10. holder.addFlags(ViewHolder.FLAG_INVALID);
  11. if (holder.isScrap()) {
  12. removeDetachedView(holder.itemView, false);
  13. holder.unScrap();
  14. } else if (holder.wasReturnedFromScrap()) {
  15. holder.clearReturnedFromScrapFlag();
  16. }
  17. recycleViewHolderInternal(holder);
  18. }
  19. holder = null;
  20. } else {
  21. fromScrapOrHiddenOrCache = true;
  22. }
  23. }
  24. }
  25. }

如果找到viewholder的话,判断viewholder是否符合条件,比如是否已经被remove、type and id是否匹配,如果不符合条件返回null。

  1. if (holder == null) {
  2. final int type = mAdapter.getItemViewType(offsetPosition);
  3. // 2) Find from scrap/cache via stable ids, if exists
  4. if (mAdapter.hasStableIds()) {
  5. holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
  6. if (holder != null) {
  7. // update position
  8. holder.mPosition = offsetPosition;
  9. fromScrapOrHiddenOrCache = true;
  10. }
  11. }
  12. if (holder == null && mViewCacheExtension != null) {
  13. // We are NOT sending the offsetPosition because LayoutManager does not know it.
  14. final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
  15. if (view != null) {
  16. holder = getChildViewHolder(view);
  17. if (holder == null) {
  18. throw new IllegalArgumentException("getViewForPositionAndType returned"
  19. + " a view which does not have a ViewHolder");
  20. } else if (holder.shouldIgnore()) {
  21. throw new IllegalArgumentException("getViewForPositionAndType returned"
  22. + " a view that is ignored. You must call stopIgnoring before"
  23. + " returning this view.");
  24. }
  25. }
  26. }
  27. }

继续向下找,这一块有两部分,第一部分还是从scrap和cache view中寻找,只不过是根据id来找,逻辑和上面分析的通过position查找类似;第二部分是从mViewCacheExtension中寻找,这部分缓存是由我们自定义的,默认为null,所以跳过。继续向下走

  1. if (holder == null) {
  2. holder = getRecycledViewPool().getRecycledView(type);
  3. if (holder != null) {
  4. holder.resetInternal();
  5. if (FORCE_INVALIDATE_DISPLAY_LIST) {
  6. invalidateDisplayListInt(holder);
  7. }
  8. }
  9. }
  10. public ViewHolder getRecycledView(int viewType) {
  11. final ScrapData scrapData = mScrap.get(viewType);
  12. if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
  13. final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
  14. return scrapHeap.remove(scrapHeap.size() - 1);
  15. }
  16. return null;
  17. }

下一步就到了pool,这一步就是从mRecyclerPool中根据type寻找viewholder,可以看到首先判断pool中是否缓存有该type的viewholder,若无返回null,若有则返回pool中最后一个viewholder缓存。拿出来就可以直接用,但是这里通过resetInternal()重置了数据,变成一张白纸,需要等待重新绑定数据。

  1. if (holder == null) {
  2. ...
  3. holder = mAdapter.createViewHolder(RecyclerView.this, type);
  4. ...
  5. mRecyclerPool.factorInCreateTime(type, end - start);
  6. }

继续向下走,如果holder还为null,则调用mAdapter.createViewHolder(),重新创建新的viewholder。并且在这一步会去mRecyclerPool创建对应type的ScrapData。

  1. //从scrap和cache中获取的则不需要rebind
  2. boolean bound = false;
  3. if (mState.isPreLayout() && holder.isBound()) {
  4. // do not update unless we absolutely have to.
  5. holder.mPreLayoutPosition = position;
  6. } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
  7. if (DEBUG && holder.isRemoved()) {
  8. throw new IllegalStateException("Removed holder should be bound and it should"
  9. + " come here only in pre-layout. Holder: " + holder);
  10. }
  11. final int offsetPosition = mAdapterHelper.findPositionOffset(position);
  12. bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
  13. }
  14. //设置布局参数
  15. final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
  16. final LayoutParams rvLayoutParams;
  17. if (lp == null) {
  18. rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
  19. holder.itemView.setLayoutParams(rvLayoutParams);
  20. } else if (!checkLayoutParams(lp)) {
  21. rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
  22. holder.itemView.setLayoutParams(rvLayoutParams);
  23. } else {
  24. rvLayoutParams = (LayoutParams) lp;
  25. }
  26. rvLayoutParams.mViewHolder = holder;
  27. rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
  28. 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()方法

  1. public void recycleView(View view) {
  2. // This public recycle method tries to make view recycle-able since layout manager
  3. // intended to recycle this view (e.g. even if it is in scrap or change cache)
  4. ViewHolder holder = getChildViewHolderInt(view);
  5. if (holder.isTmpDetached()) {
  6. removeDetachedView(view, false);
  7. }
  8. if (holder.isScrap()) {
  9. holder.unScrap();
  10. } else if (holder.wasReturnedFromScrap()){
  11. holder.clearReturnedFromScrapFlag();
  12. }
  13. recycleViewHolderInternal(holder);
  14. }

首先判断view是否是attached状态,如果是,则需要remove并且进行detached,我们在上面分析过,attached的view会存放在mAttachedScrap缓存中,这里面的view是不需要rebind的,所以这里对view解绑并且从scrap中移除。后面主要就是recycleViewHolderInternal()的逻辑。

  1. void recycleViewHolderInternal(ViewHolder holder) {
  2. ....
  3. if (forceRecycle || holder.isRecyclable()) {
  4. if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
  5. | ViewHolder.FLAG_REMOVED
  6. | ViewHolder.FLAG_UPDATE
  7. | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
  8. // Retire oldest cached view
  9. int cachedViewSize = mCachedViews.size();
  10. if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
  11. recycleCachedViewAt(0);//核心
  12. cachedViewSize--;
  13. }
  14. int targetCacheIndex = cachedViewSize;
  15. if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0
  16. && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
  17. // when adding the view, skip past most recently prefetched views
  18. int cacheIndex = cachedViewSize - 1;
  19. while (cacheIndex >= 0) {
  20. int cachedPos = mCachedViews.get(cacheIndex).mPosition;
  21. if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
  22. break;
  23. }
  24. cacheIndex--;
  25. }
  26. targetCacheIndex = cacheIndex + 1;
  27. }
  28. //如果未满将最新的viewholder添加到cacheview中
  29. mCachedViews.add(targetCacheIndex, holder);//核心
  30. cached = true;
  31. }
  32. //核心
  33. if (!cached) {
  34. //添加到pool中
  35. addViewHolderToRecycledViewPool(holder, true);
  36. recycled = true;
  37. }
  38. }
  39. ...
  40. }
  41. //下面的方法都是延伸展示的方法,方便理解
  42. void recycleCachedViewAt(int cachedViewIndex) {
  43. ......
  44. addViewHolderToRecycledViewPool(viewHolder, true);
  45. mCachedViews.remove(cachedViewIndex);
  46. }
  47. void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
  48. clearNestedRecyclerViewIfNotNested(holder);
  49. ViewCompat.setAccessibilityDelegate(holder.itemView, null);
  50. if (dispatchRecycled) {
  51. //回调adapter中的onViewRecycled方法
  52. dispatchViewRecycled(holder);
  53. }
  54. holder.mOwnerRecyclerView = null;
  55. getRecycledViewPool().putRecycledView(holder);
  56. }
  57. public void putRecycledView(ViewHolder scrap) {
  58. final int viewType = scrap.getItemViewType();
  59. //从map中获取对应type的ArrayList
  60. final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
  61. if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
  62. return;
  63. }
  64. }
  65. if (DEBUG && scrapHeap.contains(scrap)) {
  66. throw new IllegalArgumentException("this scrap item already exists");
  67. }
  68. scrap.resetInternal();
  69. scrapHeap.add(scrap);
  70. }
  71. private ScrapData getScrapDataForType(int viewType) {
  72. ScrapData scrapData = mScrap.get(viewType);
  73. if (scrapData == null) {
  74. scrapData = new ScrapData();
  75. mScrap.put(viewType, scrapData);
  76. }
  77. return scrapData;
  78. }

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,这些等后面有时间了再分析一波。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注