【进阶】RecyclerView源码解析(三)——深度解析缓存机制

标签: RecyclerView Android 源码分析 Android进阶 RecyclerView进阶
5人阅读 评论(0) 收藏 举报
分类:

1.【进阶】RecyclerView源码解析(一)——绘制流程
2.【进阶】RecyclerView源码解析(二)——缓存机制
3.【进阶】RecyclerView源码解析(三)——深度解析缓存机制

上一篇博客从源码角度分析了RecyclerView读取缓存的步骤,让我们对于RecyclerView的缓存有了一个初步的理解,但对于RecyclerView的缓存的原理还是不能理解。本篇博客将从实际项目角度来理解RecyclerView的缓存原理。
项目的截图如下:Demo

其中可以看到,这里是一个我们经常使用RecycleView实现列表。右侧输出面板展示了ScrapView的最大数量,CacheView的数量和内容,Pool中存在的内容。左侧面板展示了onBindViewHolder和onCreateViewHolder的过程。(Demo是基于一篇博客的Demo的拓展:手摸手第二弹,可视化 RecyclerView 缓存机制)
Demo地址:RecyclerViewStudy感兴趣的可以顺手点个star~

1.ScrapViews

起初,我对于这个缓存的概念一直很模糊,我尝试过很多方法想要将这个缓存中的View读取出来看看里面的内容,但是发现这个缓存的大小总是为0,这个就让我很疑惑一个大
小总是为0的缓存还有什么作用?
无意中读到了一篇博客,这篇博客对于RecyclerView提出了Detach和Remove的概念的区别,对于RecycleView的ScrapView进行了讲解。

1.1 Detach和Remove

所以我们需要区分两个概念,DetachRemove

detach: 在ViewGroup中的实现很简单,只是将ChildView从ParentView的ChildView数组中移除,ChildView的mParent设置为null, 可以理解为轻量级的临时remove, 因
为View此时和View树还是藕断丝连, 这个函数被经常用来改变ChildView在ChildView数组中的次序。View被detach一般是临时的,在后面会被重新attach。
remove: 真正的移除,不光被从ChildView数组中除名,其他和View树各项联系也会被彻底斩断(不考虑Animation/LayoutTransition这种特殊情况), 比如焦点被清除,从TouchTarget中被移除等。

1.2 缓存作用

首先我们要了解,任何一个ViewGroup都会经历两次onLayout的过程,对应的childView就会经历detach和attach的过程,而在这个过程中,ScrapViews就起了缓存的作用,这样就不需要重复创建childView和bind。
所以ScrapView主要用于对于屏幕内的ChildView的缓存,缓存中的ViewHolder不需要重新Bind,缓存时机是在onLayout的过程中,并且用完即清空

1.3 Demo验证

我们可以看一下demo验证一下我们的想法。
首先我们重写了RecylclerView的onLayout方法。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        onLayoutListener.beforeLayout();
        super.onLayout(changed, l, t, r, b);
        onLayoutListener.afterLayout();
    }

在beforLayout时设置通过反射将RecyclerView内部的mAttachedScrap替换成我们自己重写的数据结构。

public void setAllCache() {
        try {
            Field mRecycler =
                    Class.forName("android.support.v7.widget.RecyclerView").getDeclaredField("mRecycler");
            mRecycler.setAccessible(true);
            RecyclerView.Recycler recyclerInstance =
                    (RecyclerView.Recycler) mRecycler.get(this);

            Class<?> recyclerClass = Class.forName(mRecycler.getType().getName());
            Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
            mAttachedScrap.setAccessible(true);
            mAttachedScrap.set(recyclerInstance, mAttachedRecord);
            Field mCacheViews = recyclerClass.getDeclaredField("mCachedViews");
            mCacheViews.setAccessible(true);
            mCacheViews.set(recyclerInstance, mCachedRecord);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

为什么要这样做哪?这里利用了Hook的思想。这样的话,RecyclerView内部在对mAttachedScrap进行操作的时候,比如RecyclerView内部对于mAttachedScrap的添加是使用add(T t)这个方法,这样我们设置的子类只要重写这个add(T t)的方法,在添加的时候就会调用我们子类重写的add方法。

    @Override
    public boolean add(T t) {
        RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) t;
        RcyLog.log(key + "添加---【position=" + vh.getAdapterPosition() + "】");
        if (canReset) {
            if (size() + 1 > lastSize) {
                maxSize = size() + 1;
            }
        }
        return super.add(t);
    }

    @Override
    public T remove(int index) {
        RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) get(index);
        RcyLog.log(key + "移除---【position=" + vh.getAdapterPosition() + "】");
        return super.remove(index);
    }

可以看到这里,当RecyclerView内部对mAttachedScrap进行add和remove的时候,我们都会进行打印log。并且记录一下maxSize。按照我们的猜想,RecyclerView会在onLayout的过程中对mAttachedScrap进行添加和移除操作,执行完后,mAttachedScrap的大小为0。
第一次进入应用
Log截图
可以看到我们打开应用Demo的这个操作,没有做其他任何操作,仅仅是打开,mAttachedScrap经历了添加屏幕内9个ChildView的过程,并将9个ChildView移除的过程。而mAttachedScrap的大小刚好为屏幕内可以显示的Item的数量。
为什么说不需要重写Bind哪?通过上篇博客,我们从源码角度对RecyclerView的缓存有了一个初步的了解:

//先从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;
            }
        }


         boolean 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()) {
            //如果FLAG是ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID,则需要调bind
            if (DEBUG && holder.isRemoved()) {
                throw new IllegalStateException("Removed holder should be bound and it should"
                        + " come here only in pre-layout. Holder: " + holder
                        + exceptionLabel());
            }
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }

可以看到,我们在Scrap中寻找的时候,是有一个判断!holder.isInvalid(),而对于需要bind的时候判断是否需要bind有一个判断holder.isInvalid()。所以两个条件是互斥的。

2.CacheViews

CacheViews其实就是和我们平常使用过程中息息相关的一个缓存。CacheViews缓存的特点是CacheViews内的缓存在复用的时候不需要调用bind,也就是在滑动的过程中,免去了bind的过程,提高滑动的效率。
#### 2.1 缓存源码
首先来看一下对于CacheViews内缓存的获取的源码:

/ /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 getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}

首先我们通过源码可以知道CacheViews是一个ArrayList,可以看到获取的时候是遍历CacheViews,当缓存的ViewHolder和所需要的position相同的并且有效才可以复用。
和上面分析的一样,可以知道这个缓存的ViewHolder是有效的才可以复用,所以在判断是否需要bind的时候,就不需要重新bind了。
接着来看一下缓存的源码:
既然是缓存,那肯定是滑动过程中的比较直观:
“`
@Override
public boolean onTouchEvent(MotionEvent e) {
case MotionEvent.ACTION_MOVE: {
………
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
……..
return true;
}

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    ......
        if (x != 0) {
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }
       .......
    return consumedX != 0 || consumedY != 0;
}


可以看到这里省略了部分代码,在
onTouchEvent的ACTION_MOVE事件中,可以看到,这里对canScrollVertically方法进行了判断,并最终将偏移量传给了scrollByInternal方法,而在scrollByInternal方法中,调用了LayoutManager的scrollVerticallyBy方法。而scrollVerticallyBy最后调用了scrollBy方法。

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
……
//调用了fill方法
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
……
return scrolled;
}

可以看到fill方法又调回了前一篇博客分析的**fill()**方法,这样就很明显了。而缓存的源码其实上面博客上面提到过一个方法
onLayoutChild()方法里面有个detachAndScrapAttachedViews“`方法。

public void detachAndScrapAttachedViews(Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }

    /**
     * 1.Recycle操作对应的是removeView, View被remove后调用Recycler的recycleViewHolderInternal回收其ViewHolder
     2.Scrap操作对应的是detachView,View被detach后调用Reccyler的scrapView暂存其ViewHolder
     * @param recycler
     * @param index
     * @param view
     */
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            //注意这里是remove
            removeViewAt(index);
            //往cacheview和pool中
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            //注意这里是detach
            detachViewAt(index);
            //存到scrap中
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

这里就可以看到前面所说的Remove和Detach的区别,如果是remove,会执行recycleViewHolderInternal(viewHolder);方法,而这个方法最终会将ViewHolder加入CacheView和Pool中,而当是Detach,会将View加入到ScrapViews中,注意View和ViewHolder的区别,前面提到过,ScrapViews是对View的复用,而CacheView和Pool是对ViewHolder的复用。
既然是看CacheViews,那么就看一下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 view
                int cachedViewSize = mCachedViews.size();
                //如果超过默认大小,则删除第一个
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                //从CacheViews中删除第一个,并加入到Pool中
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
        ......
                //加入缓存
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                //不然直接加入Pool中
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        .......
    }

可以看到几个关键逻辑:

1.如果超过默认大小,则会移除CacheViews中的第一个,并加入到Pool中,然后在将需要加入缓存的ViweHolder加入到CacheView中。
2.如果不能加入到CacheViews中,则加入到Pool中。

2.2 Demo验证

(1)进入应用
我们首先进入应用会发现当前CacheViews的大小是0,也就是说进入应用时没有滑动,是没有任何ViewHolder回收的,这不需要解释吧。。。,而且Bind也只走了页面渲染的0-8。
进入应用
(2)向下滑动一个,第一个移除
这时我们向下滑动,加载出第9个
滑动一个
可以看到这时候除了加载了页面的position=9,还提前加载出了position=10,执行了onBind,而这时,由于第一个移出界面,所以position=0也就被加入到了CacheViews中。
(3)向上滑动,再显示第一个
回到顶部
这时候我们会发现几个特别的点:

1.onBind的面板没有新的Log,说明新出来的position=0没有走onBind方法。
2.CacheViews中由刚才保存的position=0position=10,变成了position=10position=9
由此可见:
CacheViews中缓存的ViewHolder当被复用的时候是不会走Bind流程的

RecyclerPool

其实根据前一节的讲解,我们已经对RecycleView的缓存有了一个很具体的了解了,RecyclerPool其实是RecyclerView区分ListView的一个亮点。利用这级缓存我们可以实现多个RecyclerView之间的ViewHolder的复用。(关于这一点的利用我准备在下一篇博客对RecycleView使用的技巧进行举例讲解)

3.1 缓存源码

首先我们看一下ReyclerPool的结构。

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    }

可以看到RecyclerPool内部其实是一个SparseArray,可想而知,key就是我们的ViewType,而Value是ArrayList。
我们来看一下RecyclerPool的put方法。

public void putRecycledView(ViewHolder scrap) {
        final int viewType = scrap.getItemViewType();
        final ArrayList<ViewHolder> 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");
        }
        //重置ViewHolder
        scrap.resetInternal();
        scrapHeap.add(scrap);
    }

其中resetInternal方法值得我们注意。

void resetInternal() {
        mFlags = 0;
        mPosition = NO_POSITION;
        mOldPosition = NO_POSITION;
        mItemId = NO_ID;
        mPreLayoutPosition = NO_POSITION;
        mIsRecyclableCount = 0;
        mShadowedHolder = null;
        mShadowingHolder = null;
        clearPayload();
        mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
        mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
        clearNestedRecyclerViewIfNotNested(this);
    }

可以看到所有被put进入RecyclerPool中的ViewHolder都会被重置,这也就意味着RecyclerPool中的ViewHolder再被复用的时候是需要重新Bind的。这一点就可以区分和CacheViews中缓存的区别。

总结

还是那篇Bugly博客中的图片吧(都怪我太懒了。。。)
缓存总结
看过上面的分析,这张图片就很好理解了。

最后

给大家分享几篇我认为不错的RecyclerView源码分析的博客吧,我的分析其中有些地方就是从这些博客中学习来的。

1.Bugly分析ListView和RecyclerView的区别的,建议深入了解后再看
2.CSDN的一个大神的分析,分了有6篇博客,值得一读
3.一篇很好的RecyclerView的源码分析博客,适合深入阅读
4.可视化RecyclerView缓存机制,也就是本篇博客Demo的参考
5.一篇将RecyclerView的缓存讲的通俗易懂的博客,源码不是比较深入,但是很好理解
。。。还有一些就不上了,以上5篇是我认为很值得反复阅读学习的。

下篇博客可能是RecyclerView分析系列的结尾篇了,可能从实际使用角度分析一些我所了解的RecyclerView的一些进阶知识

查看评论

spring源码深度解析.pdf

  • 2018年04月16日 11:30
  • 95.06MB
  • 下载

最全面的RecyclerView源码解析

相信很多人用RecyclerView已经很久了,但还是不得不感叹 RecyclerView的强大,性能、扩展性等方面都很强大。网上看了很多源码方面对RecyclerView,觉得还不够全面,而且自己不...
  • jieqiang3
  • jieqiang3
  • 2017-04-04 23:12:56
  • 2141

Spring源码深度解析20171225

  • 2018年02月27日 17:56
  • 94.5MB
  • 下载

Spring源码深度解析 高清 带书签 网盘链接 0积分

  • 2016年10月22日 16:00
  • 48B
  • 下载

RecyclerView源码解析之缓存机制

RecyclerView源码解析之缓存机制 一、简介 RecyclerView是谷歌官方出的一个用于大量数据展示的新控件,可以代替传统的ListView,更加强大和灵活。 事实上,Recyc...
  • jett2357
  • jett2357
  • 2017-06-05 09:55:10
  • 195

SPRING技术内幕,Spring源码深度解析

 SPRING技术内幕,Spring源码深度解析  SPRING技术内幕:深入解析SPRING架构与设计原理(第2版)【带书签】.pdf: http://www.t00y.com/...
  • Cloud_Strife_1985
  • Cloud_Strife_1985
  • 2014-11-23 14:25:11
  • 3048

Spring源码深度解析pdf

  • 2017年11月24日 16:49
  • 94.5MB
  • 下载

spring源码深度解析(笔记一)

优秀的源码设计思想以及实现方式都是相通的,随着各种开源软件的发展,各家都会融合别家优秀之处;最后的结果就是所有的开源软件从设计上或者实现上都变得越来越相似. 《spring源码深度解析》基于sprin...
  • ganxiaojieke
  • ganxiaojieke
  • 2016-12-19 09:32:57
  • 482

Spring源码深度解析.pdf

  • 2017年11月29日 15:58
  • 95.08MB
  • 下载

《spring源码深度解析》读书笔记_初读①

一、Spring模块总结⑴ Core Cotainer :包含 Core,Beanx,Context,和expression Language模块。 Beans和Core模块是框架的基础部分,提供I...
  • baidu_16859039
  • baidu_16859039
  • 2016-03-14 22:35:45
  • 719
    个人资料
    持之以恒
    等级:
    访问量: 2万+
    积分: 794
    排名: 6万+
    其他平台
    Github:https://github.com/DrownCoder
    最新评论