缓存——RecyclerView源码详解

Posted 薛瑄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存——RecyclerView源码详解相关的知识,希望对你有一定的参考价值。

刚才有个朋友问我,博主发生什么事了,给我发了几张截图,我一看,哦,原来是有个大帅哔看了文章,说是,博主,我能白嫖你的文章,我说年轻人,点个赞再走,他说不点,我说点一个,他说不点,我说点一个,他说不点,我说我这文章对你有用,他不服气,说要先看看。我说可以,很快啊,看完后,就是一个复制,一个粘贴,一个网页关闭,我大意了啊,没有删除文章。按传统博客的有用为止,他说已经输了啊。 后来他说他是乱点的,这可不是乱点的啊,训练有素。我劝年轻人好好点赞,耗子尾汁,谢谢朋友们

前言

整体流程、measure、layout 详解 ——深入分析RecyclerView源码(一)
缓存 ——深入分析RecyclerView源码(二)

这篇文章,分析一下RecycleView 的缓存机制,分为两大部分: 从缓存获取View和 把View保存到缓存中

缓存的核心是交给 Recycler 类来处理的,包括存储缓存,获取缓存等。缓存的数据类型是ViewHold,它包含了itemView,mPosition 等Item的信息

从缓存获取ViewHold

先来了解Recycler中五个变量,每个变量都与缓存相关,按照缓存获取的顺序来逐一介绍

一级缓存:返回布局和内容都都有效的ViewHolder

  • 按照position或者id进行匹配
  • 命中一级缓存无需onCreateViewHolder和onBindViewHolder
  1. mAttachedScrap ArrayList : 未与RecyclerView分离的ViewHolder列表。如果仍依赖于 RecyclerView (比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中按照id和position来查找ViewHolder。在每次绘制RecycleView的时候,都会先把界面上的ViewHolder收集到mAttachedScrap,然后在绘制的时候,方便复用

  2. mChangedScrap ArrayList:表示数据已经改变的viewHolder列表,存储 notifXXX 方法时需要改变的 ViewHolder,匹配机制按照position和id进行匹配

  3. mCachedViews ArrayList:缓存ViewHolder,主要用于解决RecyclerView滑动抖动时的情况,还有用于保存Prefetch的ViewHoder。

    • 位置相同的ViewHolder,才能复用。复用的ViewHolder,不需要bindViewHolder,可直接拿去绘制。
    • 最大的数量为:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的时候计算出来的)

二级缓存:返回View

  • 按照position和type进行匹配
  • 直接返回View
  1. mViewCacheExtension ViewCacheExtension:开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者需实现方法getViewForPositionAndType(Recycler recycler, int position, int type),注意它返回的是View。

三级缓存:返回布局有效,内容无效的ViewHolder

  • layout是有效的,但是内容是无效的,需要调用bindViewHolder
  • 多个RecycleView可共享,可用于多个RecyclerView的优化
  1. mRecyclerPool ViewHolder缓存池。 当mCachedViews缓存数量达到上限,就会把mCachedViews中的一个ViewHolder存入RecyclerViewPool中,把当前的ViewHolder存入mCachedViews。
    • 按照Type来查找ViewHolder
    • 每个Type默认最多缓存5个。

这里我们在上一篇文章的layoutChunk 函数开始分析,获取一个View,然后进行布局。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) 
        // 获取View,如果缓存中没有,就新创建
        View view = layoutState.next(recycler);
       
       //省略代码
    
在LayoutState类中
        //返回下一个位置的View
        View next(RecyclerView.Recycler recycler) 
            if (mScrapList != null) 
                return nextViewFromScrapList();
            
            //通过Recycle,来获取View
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        

recycler.getViewForPosition(mCurrentPosition); 最终会调用到 tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS)
后者返回的是ViewHolder。下面就来分析一下这个函数,它是获取缓存的重点函数

在Recycle类中
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) 
            ...  省略代码 ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) 
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) 
                // 先从mAttachedScrap、Hidden、mCachedViews中 查找,位置是position 的ViewHolder 
                //这里获取到指定position的缓存,该类型的缓存,可直接复用,不需要重新onBindViewHolder
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) 
                    //检查ViewHolder 是否可用;位置是否正确;itemViewType是否匹配;若item有固定id(Stable Id),该id是否正确
                    if (!validateViewHolderForOffsetPosition(holder)) 
                        // recycle holder (and unscrap if relevant) since it can't be used
                        if (!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,如果为空,就在mCachedViews 中进行获取
            if (holder == null) 
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //判断 是否合法
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) 
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) 
                    //如果存在固定id,则去mAttachedScrap、mCachedViews 中去查找 是否有对应的ViewHolder
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) 
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    
                
                // 还没有获取到缓存 ViewHolder,就去mViewCacheExtension 去获取,这个是自定义的缓存,可能为空
                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);
                        。。。省略代码。。。
                    
                
                //还是没有获取到缓存,就去RecycledViewPool 按照itemView Type 查找,
                //找到的缓存,因为可能位置可能,所以需要重新onBinderViewHolder 的,
                if (holder == null)  // fallback to pool
                    。。。省略代码。。。
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) 
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) 
                            invalidateDisplayListInt(holder);
                        
                    
                
                //到此,所有的缓存都查找过了,还是没有的话,就去createViewHolder 创建一个新的ViewHolder
                if (holder == null) 
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) 
                        // abort - we have a deadline we can't meet
                        return null;
                    
                    //这个函数,大家应该很熟悉了
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) 
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) 
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        
                    

                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                    。。。省略代码。。。
                
            

            。。。省略代码。。。

            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()) 
                // 根据holder的mFlags 标志,来判断当前ViewHolder的状态。mFlags 是Int 类型,每个二进制位,表示一种状态
                //mFlags 默认是0 ,也就是任何状态都是 0
                //在下面的缓存回收机制,可以看到如果有ViewHolder.FLAG_INVALID、ViewHolder.FLAG_UPDATE 这里两种状态
                //是不能加入mCachedViews中的,所以也就是说mCachedViews 获取到的缓存是不需要onBindViewHolder
                。。。省略代码。。。
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //调用到onBindViewHolder,来把数据绑定到View
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            
            //获取ViewHolder 的LayoutParams,
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            //这个LayoutParams 是RecycleView 自定义的,继承于android.view.ViewGroup.MarginLayoutParams
            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;
        

其中涉及到缓存查找的几个函数,例如 getScrapOrHiddenOrCachedHolderForPositiongetScrapOrCachedViewForId等,基本思路就是变量相关的缓存变量,来查找符合条件的ViewHolder。 这里就不展开了,下面来看一下ViewHolder 是如何被回收到这些缓存中的

回收ViewHold到缓存

缓存的获取,很简单,按照一定的顺序,在不同的缓存层级查找。回收 才代表缓存内部的逻辑,保存到哪个缓存层级,以及何时被转移。缓存回收有很多中情况,下面我们以 item 滑出屏幕 为例,来分析缓存的回收

先来一张图,对整体的回收逻辑 有个认识

从滑动事件的入口,来分析

在RecycleView 类中
    @Override
    public boolean onTouchEvent(MotionEvent e) 

      。。。省略代码。。。    
      switch (action) 
              。。。省略代码。。。
              //滑动操作,
              case MotionEvent.ACTION_MOVE: 
                。。。省略代码。。。
                final int index = e.findPointerIndex(mScrollPointerId);
              
                if (mScrollState == SCROLL_STATE_DRAGGING) 
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //这个是重点
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) 
                        getParent().requestDisallowInterceptTouchEvent(true);
                    
                    //调用预布局等操作
                    if (mGapWorker != null && (dx != 0 || dy != 0)) 
                        mGapWorker.postFromTraversal(this, dx, dy);
                    
                
             break;
      
    

在RecycleView 类中
    boolean scrollByInternal(int x, int y, MotionEvent ev) 
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) 
            //重点函数
            scrollStep(x, y, mScrollStepConsumed);
            consumedX = mScrollStepConsumed[0];
            consumedY = mScrollStepConsumed[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        
        if (!mItemDecorations.isEmpty()) 
            invalidate();
        
        //处理嵌套滑动
        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH)) 
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) 
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
         else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) 
            if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) 
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            
            considerReleasingGlowsOnScroll(x, y);
        
        if (consumedX != 0 || consumedY != 0) 
            dispatchOnScrolled(consumedX, consumedY);
        
        if (!awakenScrollBars()) 
            invalidate();
        
        return consumedX != 0 || consumedY != 0;
    
    void scrollStep(int dx, int dy, @Nullable int[] consumed) 
        //开始拦截 ,避免多余的requestLayout。主要是设置变量mInterceptRequestLayoutDepth,开启拦截就+1,关闭就-1
        //如果该值为0,表示该条件 允许绘制
        startInterceptRequestLayout();
        //表示进入布局或滚动,此时不能改变adapter的数据,否则会抛出异常。mLayoutOrScrollCounter,进入+1,退出-1
        onEnterLayoutOrScroll();

        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);
        //在x y 轴上 滑动消耗的值
        int consumedX = 0;
        int consumedY = 0;
        //下面这两个滑动函数,调用到LayoutManager中,
        if (dx != 0) 
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        
        if (dy != 0) 
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        

        TraceCompat.endSection();
        repositionShadowingViews();
        //退出布局或滚动
        onExitLayoutOrScroll();
        //关闭拦截 ,允许的requestLayout
        stopInterceptRequestLayout(false);

        if (consumed != null) 
            consumed[0] = consumedX;
            consumed[1] = consumedY;
        
    

下面来以scrollVerticallyBy 为例,来分析,下面的函数进入了LinearLayoutManager.java 中

在LinearLayoutManager.java中
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) 
        if (mOrientation == HORIZONTAL) 
            return 0;
        
        return scrollBy(dy, recycler, state);
    
    
    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) 
        if (getChildCount() == 0 || dy == 0) 
            return 0;
        
        mLayoutState.mRecycle = true;
        //确保LayoutState 不为空,LayoutState 是保存
        ensureLayoutState();
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, true, state);
        //重点是在fill 函数,
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) 
            if (DEBUG) 
                Log.d(TAG, "Don't have any more elements to scroll");
            
            return 0;
        
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        mOrientationHelper.offsetChildren(-scrolled);

        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    

关于fill 函数,在整体流程、Measure、Layout 详解——RecyclerView源码详解(一) 疑问已经分析过了,它的返回值,表示的是填充的值(即使没有显示在屏幕上),在有回收ViewHolder的功能 recycleByLayoutState(recycler, layoutState);

在LinearLayoutManager.java中
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) 
        if (!layoutState.

以上是关于缓存——RecyclerView源码详解的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView缓存复用解析,源码解读

RecyclerView缓存复用解析,源码解读

RecyclerView缓存复用解析,源码解读

RecyclerView详解一,使用及缓存机制

如何设计一个本地缓存,涨姿势了!

整体流程MeasureLayout 详解——RecyclerView源码详解