当前位置:   article > 正文

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

onfailedtorecycleview


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);
            }
        }
    }
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

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);
        }

		
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

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

在回收目标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;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

当我们往下滑动的时候,那么下面不是要显示一个新的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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

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

关于mChangedScrap

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

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ....
      if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
      ....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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;
	}
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

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.get(i).isAttachedToTransitionOverlay()) {
                    return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }

    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;
        }
        scrap.resetInternal();
        scrapHeap.add(scrap);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

这个池是一层SparseArray包着一层ArrayList

外层的SparseArray的key是viewType,也就是说,我们创建的不同类型的视图在回收池里面是分开存的。

内层的ArrayList就是具体存ViewHolder的,最大容量为5,当存的时候如果超过最大容量了,就会放弃这次保存,直接丢弃。

附:时序图代码

@startuml

participant RecyclerView
participant LinearLayoutManager
participant RecyclerView.Recycler as recycler
participant LayoutState

RecyclerView -> RecyclerView : onTouchEvent
activate RecyclerView
RecyclerView -> RecyclerView : scrollByInternal
activate RecyclerView
RecyclerView -> RecyclerView : scrollStep
activate RecyclerView
	RecyclerView -> RecyclerView : scrollStep
	RecyclerView -> LinearLayoutManager : scrollVerticallyBy
	activate LinearLayoutManager
	LinearLayoutManager -> LinearLayoutManager : scrollBy
	LinearLayoutManager -> LinearLayoutManager : fill(关键方法)

	alt 这部分是缓存的回收机制
	activate LinearLayoutManager
		LinearLayoutManager -> LinearLayoutManager : recycleByLayoutState
		activate LinearLayoutManager
		LinearLayoutManager -> LinearLayoutManager : recycleViewsFromStart(也有FromEnd方法)
		LinearLayoutManager -> LinearLayoutManager : recycleChildren
		LinearLayoutManager -> RecyclerView : removeAndRecycleViewAt
		activate RecyclerView
			RecyclerView -> recycler : recycleView
			activate recycler
			recycler -> recycler : recycleViewHolderInternal
					activate recycler
					deactivate recycler
			deactivate recycler
		deactivate RecyclerView
	end

	alt 这部分是缓存的复用机制
	deactivate LinearLayoutManager
	LinearLayoutManager -> LinearLayoutManager : layoutChunk
	activate LinearLayoutManager
		LinearLayoutManager -> LayoutState : next
		activate LayoutState
		LayoutState -> recycler : getViewForPosition
			activate recycler
			recycler -> recycler : tryGetViewHolderForPositionByDeadline
		deactivate LayoutState
	end
	

deactivate RecyclerView
deactivate RecyclerView
deactivate RecyclerView

@enduml
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

参考材料

码牛学院
2021.12.13-RecycleView回收模型-四级缓存-David

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/286659
推荐阅读
相关标签
  

闽ICP备14008679号