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

Posted wodongx123

tags:

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

文章目录


RecyclerView有四级缓存,其中一级缓存是用户自定义的缓存,四级缓存本质都是内存,是按照功能区分的。

名称数据结构容量作用
mAttachedScrap ArrayList看界面上能显示几个存放界面上显示的View
mCachedViewsArrayList2用于移除屏幕外的视图的回收与复用
mViewCacheExtension自定义自定义缓存,用户定制
mRecyclerPoolSparseArray5缓存池,用户移出屏幕View的回收和复用,会重置ViewHolder的数据

1. 缓存回收复用的原理

在RecyclerView中,四级缓存其实都是存放在内存中的数据,所以他的分级都是按照逻辑上的功能来分级的。

1.1 为什么要有四级缓存,每一级缓存的作用

一级缓存

mAttachedScrap 其实就是用户当前页面上直接可以看到的item,他们都被存放在同一个列表中以便于Recycler管理

二级缓存

mCachedViews 保存的是,刚刚移出屏幕的View,一共保存2个,他们存在的意义在于,用户如果在往下滑动一点点以后,又忽然反悔了往上滑动的时候,保证原先的那个视图可以快速的展示给用户显示,因为该View的位置以及内容都是曾经被绘制过的,所以不需要重新更新数据。

三级缓存

用户自定义缓存,我们一般不用

四级缓存

回收池,这个是一个很关键的内容,在我们实际的项目中,一旦设计到某个需要频繁的创建销毁的对象时,都会采用池的设计方式,将一定数量的对象保存起来,如果需要用到的时候,直接从池子里面取,而不需要重新创建一个新的。

目的是在于通过数据保存的方式防止频繁创建对象造成的内存抖动,从而频繁引发GC造成卡顿。

1.2 四级缓存是如何工作的

我们先把场景模拟好:我们的手机屏幕,正好可以放置五个item。每个场景都是往下滑动一个item(不会画动图)。

  1. 加载第一屏
    当刚进入页面的时候,由于我们的四级缓存都是空的,所以第一屏显示的5个item都是当场创建出来的。
    第一屏显示5个的话,就意味着,屏幕内最多可以存放6个item(上下各显示一部分),当我们手指稍微往下滑动一点,第6个item也就被创建,并且放入一级缓存中。

  2. 滑动加载更多,CachedView:当我们往下滑动的时候要去加载第7个item的时候,这个时候由于只有一级缓存是有视图的,所以我们也会新创建一个item用于显示,这个时候,第1个item被隐藏起来了,用户看不到了第1个item会被存放到mCachedView中,这样如果用户往下滑动一点点之后如果忽然反悔了,我们可以快速展示前面出现过的item。

  3. 滑动加载更多,回收池:此时,屏幕上和mCachedView都已经存满了,用户又开始往下滑动,这次就会触发完整的缓存回收复用机制,
    回收:RecyclerView会将,mCachedView的第一个(其实就是最早被加入到mCachedView的)视图,转移到回收池中。
    复用:需要新加载的View,RecyclerView会尝试重回收池里面去取出View,重新写入数据后加入到RecyclerView中。
    从图可以看出,一个一屏能显示6个item的页面,正常情况下一共会创建9个item,如果你滑的很快,或者场景比较复杂的,回收池里面的item会更多,但是不会超过回收池的上限5个。

2. 源码时序图和解读

如何去看RecyclerView滑动的源码,就要看启用这个缓存功能的起点,起点自然是滑动事件,本质也是点击事件,所以起点是RecyclerView.onTouchEvent

时序图如下:

把里面的关键方法都再看一遍,顺便整理一下里面的一些细节。

我会只截取有意义的代码部分,并且调整不同方法的顺序,让我们看的时候可以直接从上往下按顺序看。

如果某个方法中,我有进行省略代码的行为,我会注释表明,反之就是没有省略代码。
如果因为省略代码而看到了意义不明的参数,请忽略他,他并不会影响源码的阅读。

2.1 缓存回收

虽然时序图是从OnTouchEvent开始画的,但是真正的逻辑处理是从LinearLayoutManager开始,所以直接从recycleByLayoutState方法开始看

LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider 

	/** 根据LayoutState的状态来回收视图*/
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) 
    	//省略了无关代码
    	//判断当前是往上滑动还是往下滑动
    	if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) 
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
         else 
            recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
        
    

	/** 回收滚动到布局末尾后出界的视图。检查布局位置和可见位置,以确保视图不可见。*/
	private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
            int noRecycleSpace) 
		//省略了无关代码
        final int limit = scrollingOffset - noRecycleSpace;
        final int childCount = getChildCount();

		// 从最上面的视图开始遍历,找到第一个不需要回收的视图
        for (int i = 0; i < childCount; i++) 
             View child = getChildAt(i);
             if (mOrientationHelper.getDecoratedEnd(child) > limit
                     || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) 
                 recycleChildren(recycler, 0, i);
                 return;
             
        
    

	 /**
     * 回收指示以内的所有视图
     */
    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) 
        if (startIndex == endIndex) 
            return;
        
        if (endIndex > startIndex) 
            for (int i = endIndex - 1; i >= startIndex; i--) 
            	// 转到RecyclerView处理
                removeAndRecycleViewAt(i, recycler);
            
         else 
            for (int i = startIndex; i > endIndex; i--) 
                removeAndRecycleViewAt(i, recycler);
            
        
    
	

RecyclerView这个类高度解耦,可以设置横纵,或者网格类型的数据实体,所以我们在看滑动的方法调用时,要从具体的LayoutManager开始看。

LinearLayoutManager在回收机制中做的事代码看着非常复杂,但是实际上做的事很简单。以往下滑动为例,manager无非就是,把所有完全不可见的视图集合起来,去调用RecyclerView的removeAndRecyclerView方法来进行实际的回收。

RecyclerView
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 

	/**
     * 移除一个子View并且交由Recycler来回收它
     */
    public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) 
        final View view = getChildAt(index);
        removeViewAt(index);
        recycler.recycleView(view);
    

	/** RecyclerView内部的回收类,用于做View的回收机制 */
	public final class Recycler 
		
		 /** 回收一个View */
		public void recycleView(@NonNull View view) 
		 	//省略了无关代码
		 	ViewHolder holder = getChildViewHolderInt(view);
		 	recycleViewHolderInternal(holder);
		

		void recycleViewHolderInternal(ViewHolder holder) 
			//省略了无关代码
			final boolean forceRecycle = mAdapter != null
                    && transientStatePreventsRecycling
                    && mAdapter.onFailedToRecycleView(holder);
            if (forceRecycle || holder.isRecyclable()) 
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) 
                    // 获取CachedViews的数量,如果超过上限,就先把CachedViews的第一个元素给放到回收池了
                    int 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)) 
                        // 添加视图时,跳过最近预取的视图
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) 
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) 
                                break;
                            
                            cacheIndex--;
                        
                        targetCacheIndex = cacheIndex + 1;
                    
                    
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                
                if (!cached) 
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                
            
		

		/** 将CachedView中指定下标的View回收 */
        void recycleCachedViewAt(int cachedViewIndex) 
        	ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
            addViewHolderToRecycledViewPool(viewHolder, true);
            mCachedViews.remove(cachedViewIndex);
        

		/** 将视图加入到回收池里面 */
		void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean 	dispatchRecycled) 
			//省略了无关代码
            holder.mOwnerRecyclerView = null;
            getRecycledViewPool().putRecycledView(holder);
        

		
	

具体的回收就涉及到他的多级缓存了

在回收目标View的时候,他会先判断,能否把将这个View,放入到CachedViews中,如果可以的话,就会将CachedView中的第一个View,转移到回收池里面,在将目标View放入到CachedView

如果不能的情况,就将目标View直接放到回收池里面

2.2 缓存复用

为了方便源码解读,我们统一的场景为竖直排列且向下滑动

LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider 
        
	void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) 
        // 省略了无关代码
        // 通过LayoutState去获取一个View
	    View view = layoutState.next(recycler);
	    if (view == null) 
	        result.mFinished = true;
	        return;
	    

		// 将获取到的View添加到视图上面
	    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
	    if (layoutState.mScrapList == null) 
	        if (mShouldReverseLayout == (layoutState.mLayoutDirection
	                == LayoutState.LAYOUT_START)) 
	            addView(view);
	         else 
	            addView(view, 0);
	        
	     else 
	        if (mShouldReverseLayout == (layoutState.mLayoutDirection
	                == LayoutState.LAYOUT_START)) 
	            addDisappearingView(view);
	         else 
	            addDisappearingView(view, 0);
	        
	    
    

	/** 在LayoutManager填充空白时保持临时状态的Helper类 */
	static class LayoutState 

        /**
         * Current position on the adapter to get the next item.
         */
        int mCurrentPosition;

		View next(RecyclerView.Recycler recycler) 
			// 省略了无关代码
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        
    

当我们往下滑动的时候,那么下面不是要显示一个新的View吗,layoutChunk这个方法就是先通过四级缓存机制去拿到这个View,然后再直接加到RecyclerView上面,就做了这么简单的事,每次要新显示一个View,就会调用一次这个方法。

RecyclerView
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 
        
    public final class Recycler 
    	/** 一级缓存,存放的是不需要重新绑定就可以重用的视图 */
    	final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
		/** 一级缓存,存放的是发生改变的Scrap视图,如果重用,需要重新经过adapter的绑定 */
		ArrayList<ViewHolder> mChangedScrap = null;
		/** 二级缓存mCachedViews */
		final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
		/** 三级缓存mViewCacheExtension,一般用不上 */
		private ViewCacheExtension mViewCacheExtension;
		/** 四级缓存mRecyclerPool */
		RecycledViewPool mRecyclerPool;

		@NonNull
        public View getViewForPosition(int position) 
            return getViewForPosition(position, false);
        

        View getViewForPosition(int position, boolean dryRun) 
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        

		/** 根据下标来获取ViewHolder */
		ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) 
            //省略了无关代码

			boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
			// 1 从非常规缓存mChangedScrap获取
            if (mState.isPreLayout()) 
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            

			// 2 从mAttachedScrap或者mCachedViews获取
			if (holder == null) 
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            

			
			if (holder == null) 
				// 2 从mAttachedScrap或者mCachedViews获取,和上面不同的是
				// 这次是根据视图的id来获取,不是根据视图的位置
				// 根据id来获取的功能,要设置了才能生效,一般是不生效的
				if (mAdapter.hasStableIds()) 
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) 
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    
                
                // 3 从mViewCacheExtension获取
				if (holder == null && mViewCacheExtension != null) 
                   final View view = mViewCacheExtension
                           .getViewForPositionAndType(this, position, type);
                   if (view != null) 
                       holder = getChildViewHolder(view);
                   
                
			

			// 4 从缓存池获取
			if (holder == null)  
				holder = getRecycledViewPool().getRecycledView(type);
			
			
			// 前面4步都找不到的情况下,创建一个新的
			if (holder == null) 
				holder = mAdapter.createViewHolder(RecyclerView.this, type);
			
			
			return holder;
        
    

具体如何获取View的方法,不用过多文字说明,基本就是根据缓存的优先级一级一级的去找,找不到的情况下,就创建一个新的

关于mChangedScrap

mChangedScrap 有人说是一级缓存,有人说不是,但是tryGetViewHolderForPositionByDeadline这个方法,最先判断的就是他

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) 
      ....
      if (mState.isPreLayout()) 
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            
      ....

mPreLayout这个字段,默认是false,也就是说一般情况下不会走到这个逻辑里面,那什么时候会走到该逻辑呢
简单来说会涉及到RecyclerView的预布局和动画的原理,这里就先略过。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 

	final State mState = new State();

    protected void onMeasure(int widthSpec, int heightSpec) 
    	//省略了无关代码
    	if (mAdapterUpdateDuringMeasure) 
			if (mState.mRunPredictiveAnimations) 
                mState.mInPreLayout = true;
             else 
                // consume remaining updates to provide a consistent state with the layout pass.
                mAdapterHelper.consumeUpdatesInOnePass();
                mState.mInPreLayout = false;
            
    	
	

	public static class State 
		/**
         * True if the associated @link RecyclerView is in the pre-layout step where it is having
         * its @link LayoutManager layout items where they will be at the beginning of a set of
         * predictive item animations.
         */
        boolean mInPreLayout = false;
	
	

2.3 回收池结构

public static class RecycledViewPool 
	/** 每种类型的最大存放数量*/
	private static final int DEFAULT_MAX_SCRAP = 5;
	
	static class ScrapData 
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    
    SparseArray<ScrapData> mScrap = new SparseArray<>();

	@Nullable
    public ViewHolder getRecycledView(int viewType) 
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) 
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) 
                if (!scrapHeap以上是关于RecyclerView缓存复用解析,源码解读的主要内容,如果未能解决你的问题,请参考以下文章

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

android进阶篇02RecyclerView回收复用机制源码解析

用十张图帮你解析RecyclerView的缓存复用机制了~

RecyclerView的缓存机制

RecyclerView源码分析

RecyclerView源码分析