赞
踩
名称 | 数据结构 | 容量 | 作用 |
mAttachedScrap | ArrayList | 看界面上能显示几个 | 存放界面上显示的View |
mCachedViews | ArrayList | 2 | 用于移除屏幕外的视图的回收与复用 |
mViewCacheExtension | 自定义 | 自定义缓存,用户定制 | |
mRecyclerPool | SparseArray | 5 | 缓存池,用户移出屏幕View的回收和复用,会重置ViewHolder的数据 |
在RecyclerView中,四级缓存其实都是存放在内存中的数据,所以他的分级都是按照逻辑上的功能来分级的。
mAttachedScrap 其实就是用户当前页面上直接可以看到的item,他们都被存放在同一个列表中以便于Recycler管理
mCachedViews 保存的是,刚刚移出屏幕的View,一共保存2个,他们存在的意义在于,用户如果在往下滑动一点点以后,又忽然反悔了往上滑动的时候,保证原先的那个视图可以快速的展示给用户显示,因为该View的位置以及内容都是曾经被绘制过的,所以不需要重新更新数据。
用户自定义缓存,我们一般不用
回收池,这个是一个很关键的内容,在我们实际的项目中,一旦设计到某个需要频繁的创建销毁的对象时,都会采用池的设计方式,将一定数量的对象保存起来,如果需要用到的时候,直接从池子里面取,而不需要重新创建一个新的。
目的是在于通过数据保存的方式防止频繁创建对象造成的内存抖动,从而频繁引发GC造成卡顿。
我们先把场景模拟好:我们的手机屏幕,正好可以放置五个item。每个场景都是往下滑动一个item(不会画动图)。
加载第一屏:
当刚进入页面的时候,由于我们的四级缓存都是空的,所以第一屏显示的5个item都是当场创建出来的。
第一屏显示5个的话,就意味着,屏幕内最多可以存放6个item(上下各显示一部分),当我们手指稍微往下滑动一点,第6个item也就被创建,并且放入一级缓存中。
滑动加载更多,CachedView:当我们往下滑动的时候要去加载第7个item的时候,这个时候由于只有一级缓存是有视图的,所以我们也会新创建一个item用于显示,这个时候,第1个item被隐藏起来了,用户看不到了第1个item会被存放到mCachedView中,这样如果用户往下滑动一点点之后如果忽然反悔了,我们可以快速展示前面出现过的item。
滑动加载更多,回收池:此时,屏幕上和mCachedView都已经存满了,用户又开始往下滑动,这次就会触发完整的缓存回收复用机制,
回收:RecyclerView会将,mCachedView的第一个(其实就是最早被加入到mCachedView的)视图,转移到回收池中。
复用:需要新加载的View,RecyclerView会尝试重回收池里面去取出View,重新写入数据后加入到RecyclerView中。
从图可以看出,一个一屏能显示6个item的页面,正常情况下一共会创建9个item,如果你滑的很快,或者场景比较复杂的,回收池里面的item会更多,但是不会超过回收池的上限5个。
如何去看RecyclerView滑动的源码,就要看启用这个缓存功能的起点,起点自然是滑动事件,本质也是点击事件,所以起点是RecyclerView.onTouchEvent
时序图如下:
把里面的关键方法都再看一遍,顺便整理一下里面的一些细节。
我会只截取有意义的代码部分,并且调整不同方法的顺序,让我们看的时候可以直接从上往下按顺序看。
如果某个方法中,我有进行省略代码的行为,我会注释表明,反之就是没有省略代码。
如果因为省略代码而看到了意义不明的参数,请忽略他,他并不会影响源码的阅读。
虽然时序图是从OnTouchEvent开始画的,但是真正的逻辑处理是从LinearLayoutManager开始,所以直接从recycleByLayoutState方法开始看
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方法来进行实际的回收。
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直接放到回收池里面
为了方便源码解读,我们统一的场景为竖直排列且向下滑动
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,就会调用一次这个方法。
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 有人说是一级缓存,有人说不是,但是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; } }
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); } }
这个池是一层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
码牛学院
2021.12.13-RecycleView回收模型-四级缓存-David
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。