【源码解析】RecyclerView的工作原理

Posted

tags:

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

参考技术A 在平时的开发过程中,当用到滑动布局时,我们用的比较多的是ListView或ScrollView,但对于RecyclerView的使用却比较少,也就是在需要用到水平滑动布局时才会想到RecyclerView。那在有了ListView的情况下,为什么Google还要推出RecyclerView呢?下面我们从源码角度来分析一下该RecyclerView的布局与缓存原理,看看其与ListView有什么区别。

RecyclerView 一般使用方式是在 Layout 中定义布局文件,然后在 Activity 中通过 findViewById 来拿到 RecyclerView 的实例对象,因此我们从 RecyclerView 的构造函数入手进行分析。

构造函数中告诉我们,可以在布局文件中通过 app:layoutManager 来设置 RecyclerView 的 LayoutManager 对象。 LayoutManager 主要负责 RecyclerView 的布局。

拿到 RecyclerView 对象后,如果在构造函数中没有设置 LayoutManager ,可以通过调用 RecyclerView 的 setLayoutManager(RecyclerView.LayoutManager layout) 方法进行设置。

然后 RecyclerView 会调用 setAdapter 方法。

setAdapterInternal 方法主要作用是将传进来的 adapter 保存到 mAdapter 变量。之后调用了 requestLayout 方法。

requestLayout 方法又调用了父类的 requestLayout 方法,最终调用了 View 的 requestLayout 方法。

上面的 mParent 的真正实例为 ViewRootImpl ,也就是说执行了 ViewRootImpl 的 requestLayout 方法。

scheduleTraversals 方法被执行,意味着后续开始执行 RecyclerView 的 onMeasure 、 onLayout 、 onDraw 方法,之后 RecyclerView 中子视图就展示出来了。

这里我们将 onLayout 方法单拎出来进行分析,因为 RecyclerView 之所以能适配多种滚动布局,主要是 onLayout 方法发挥作用。

onLayout 方法接着调用了 dispatchLayout 方法。

dispatchLayout 方法依次调用了 dispatchLayoutStep1 、 dispatchLayoutStep2 、 dispatchLayoutStep3 方法。

我们首先看 dispatchLayoutStep1 方法。

dispatchLayoutStep1 方法中调用了 mLayout 的 onLayoutChildren 方法。上面分析告诉我们, mLayout 就是 LayoutManager ,所以我们转到 LayoutManager 的 onLayoutChildren 方法。

onLayoutChildren 方法是一个空实现,其具体实现在各个子类中。我们拿 LinearLayoutManager 进行分析,看其中 onLayoutChildren 的实现。

onLayoutChildren 方法中的注释已经为我们说明了 RecyclerView 的布局算法, mAnchorInfo 为布局锚点信息,包含了子控件在Y轴上起始绘制偏移量(coordinate), itemView 在 Adapter 中的索引位置(position)和布局方向(mLayoutFromEnd)-表示start、end方向。该方法的功能是:确定布局锚点,并以此为起点向开始和结束方向填充 ItemView ,如下图所示。

在 onLayoutChildren 方法中,调用了 fill 方法,从该方法名可以知道,该方法应该是将子控件加入到 RecyclerView 中的。

fill 方法中循环调用了 layoutChunkResult 方法。

layoutChunk 方法中,layoutState的next方法将从 Recycler 获取的 View 添加到 RecyclerView 中,从而完成了整个 RecyclerView 的布局。

以上就是 RecyclerView 渲染过程的源码分析,接下来我们来分析一下 RecyclerView 的滑动过程。

RecyclerView 本质上就是一个 View ,所以我们从它的 onTouchEvent 方法入手进行分析。

onTouchEvent 方法中主要关注的是 action 为 MotionEvent.ACTION_MOVE 的情况,在滑动过程中调用了 scrollByInternal 方法。

当上下滑动时,垂直方向上的y偏移量是不等于0的,从而执行了 LayoutManager 的 scrollVerticallyBy 方法。我们拿 LinearLayoutManager 的 scrollVerticallyBy 来举例。

当上下滑动时,执行了 scrollBy 方法。

scrollBy 方法中又执行了 fill 方法,该方法的作用是向可填充区域填充 itemView ,我们具体看一下 fill 方法的实现。

fill方法中又调用了layoutChunk方法。

layoutChunk 方法中出现了一个很重要的方法,就是 LayoutManager.LayoutState 的 next 方法,该方法的实现如下。

这里通过Recycler去获取了一个可重复利用的View,若该View不存在则创建一个新View,原理和ListView的Recycler基本无异。下图展示了RecyclerView循环复用View的原理。

本文从源码的角度分析了RecyclerView的布局与滑动过程中View的缓存原理。相对于ListView来说,RecyclerView的布局和View的缓存原理与ListView差不多一致,但是RecyclerView扩展了ListView的特性,不但可以做到垂直滑动,也能做到水平滑动,并且在创建多样式滚动View方面也做得比ListView出色。可以说,RecyclerView就是ListView的一个增强版本。

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缓存复用解析,源码解读

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

RecyclerView源码分析

Spring MVC工作原理及源码解析DispatcherServlet实现原理及源码解析

Spring MVC工作原理及源码解析 ViewResolver实现原理及源码解析

Spring MVC工作原理及源码解析 HandlerMapping和HandlerAdapter实现原理及源码解析