前言
上章我们讲了右半部分,本章我们讲解左半部分;
如何复用原理
我们在滑动的时候,才会触发 RecyclerView 的回收复用,所以我们从 RecyclerView 的 onTouchEvent 方法入手;我们来看下滑动的时候,是怎么和 LayoutManager 关联起来的;
我们进入 onTouchEvent 的 ACTION_MOVE 看下:
typescript
public boolean onTouchEvent(MotionEvent e) {
//
...
// 省略部分代码
case MotionEvent.ACTION_MOVE:
if(scrollByInternal(xxxx)){}
break;
}
我们进入 scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e) 这个方法看下:
arduino
boolean scrollByInternal(int x, int y, MotionEvent ev) {
//
...
// 省略部分代码
scrollStep(x, y, mResuableIntPair);
}
我们进入这个 scrollStep 方法看下:
根据滑动方向,分别调用了 LayoutManager 不同的方法,我们选择其中一个进入看下:
我们选择 LinearLayoutManager 的 scrollVerticalcallBy 方法看下:
arduino
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
这里直接调用了 scrollBy 方法,我们进入这个方法看一下:
我们进入这个 fill 方法看下:
可以看到,我们在一个 while 循环中多次调用 layoutChunk 方法,这个 layoutChunk 方法就是获取 view 填充我们的 RecyclerView 的,我们进入这个方法看下:
从缓存中获取 View 并添加到 RecyclerView 中,我们进入这个 next 方法看下:
sql
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
从 Recycler 中根据位置获取一个 View,我们进入这个 getViewForPosition 看下:
arduino
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
拿到 ViewHolder 之后,直接获取它的 itemView 并返回,所有的将 ViewHolder 从缓存取出来复用的逻辑都在这里,我们来看下 ViewHolder 是如何复用的:
这里一共包含了四级缓存,对应着四级复用:
-
mChangeScrp 和 mAttachedScrp;用来缓存还在屏幕内的 ViewHolder
-
mCachedViews;用来缓存移除屏幕外地 ViewHolder
-
mViewCacheExtension;开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存
-
RecyclerViewPool;ViewHolder 缓存池
第一次缓存复用
ini
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
与动画相关的,通过位置从 mChangeScrp 中获取;
第二次缓存复用
ini
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
针对位置的,通过位置从 mAttachedScrap 和 mCachedViews 中获取;
ini
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
还是针对 mAttachedScrap 和 mCachedViews 中获取的,通过 ViewType 和 ItemId来区分,所以这个也属于第二次缓存复用;
第三次缓存复用
scss
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);
}
// 省略部分代码
...
//
}
开发给开发中的自定义扩展缓存,需要开发者自己管理 View 的创建和缓存,一般用不到;
第四次缓存复用
ini
holder = getRecycledViewPool().getRecycledView(type);
从缓存池中获取;
复用的流程我们已经梳理通了,那么拿到复用的 ItemView 之后,又是如何调用到 onBindViewHolder 以及如何调用的 onCreateViewHolder 呢?我们继续分析:
如果四级缓存中都没有可以复用的 ViewHolder 的话,那么就需要进行 ViewHolder 的创建流程了;
ini
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
创建之后,就是进行 ViewHolder 的绑定流程了;
ini
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
最终就会调用到 onBindViewHolder 方法;
整体时序图如下:
如何缓存原理
缓存发生了 RecyclerView 的 onLayout 方法中,我们进入看一下:
arduino
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
我们进入 dispatchLayout 方法看下:
我们进入这个 dispatchLayoutStep2 方法看下,这个方法最终调用到了
ini
mLayout.onLayoutChildren(mRecycler, mState);
我们进入 LinearLayoutManager 的 onLayoutChildren 方法看下,这个方法最终调用到了 detachAndScrapAttachedViews 这个方法,我们进入这个方法看下:
ini
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
这里调用了 recycler.recycleViewHolderInternal(viewHolder); 和 recycler.scrapView(view); 我们分别看下;
recycler.recycleViewHolderInternal(viewHolder) 主要用来处理 mCacheView 和 RecyclerViewPool 的缓存;
如果 ViewHolder remove、update 等发生变化的时候,不执行缓存逻辑;
recycleCachedViewAt 处理的就是 mCacheView;
scss
// 如果 mCacheView 当前的大小大于等于 mViewCacheMax(默认的mCacheView的大小)
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
addViewHolderToRecycledViewPool(viewHolder, true); 和 mCachedViews.remove(cachedViewIndex); 这两个方法执行之后,会将 ViewHolder 添加到 RecycledViewPool 中(调用 addViewHolderToRecycledViewPool 方法),同时从 mCachedViews 中移除,也就是说 RecyclerViewPool 中的数据是从 mCachedView 中来的;
当 mCachedViews 中存满之后(默认存放2个),就会把第 0 个位置的 View 添加到 RecyclerViewPool 中并从自身移除掉,第 1 个位置的 ViewHolder 移动到第 0 个位置,新进来的放到第 1 个位置;
我们接下来看下 addViewHolderToRecycledViewPool 方法的实现;
我们进入 putRecycledView 方法中看下:
先获取 viewType,然后根据 viewType 获取 ScrapData,然后获取它的 scrapHeap 集合;也就是我们的 ViewHolder 是存放在 ScrapData 中了;
我们来看下 getScrapDataForType 的方法实现:
kotlin
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
数据满了之后,直接 return 不进行缓存,也就是同一种 ViewType 的 ViewHolder 只保存 5 个;
ini
scrap.resetInternal();
清空 ViewHolder,也就是缓存池中保存的只是 ViewHolder 类型,不保存数据;
我们接下来看下 recycler.scrapView(view);
这里处理了 mAttachedScrap 和 mChangedScrap 用来缓存 ViewHolder;
整体时序图如下:
自定义LayoutManager
我们如果想实现探探的左滑右滑效果,需要自定义 LayoutManager;
scss
public class SlideLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
// 布局
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// ViewHodler回收复用
detachAndScrapAttachedViews(recycler);
int bottomPosition;
int itemCount = getItemCount();
if (itemCount < CardConfig.MAX_SHOW_COUNT) {
bottomPosition = 0;
} else {
// 布局了四张卡片
bottomPosition = itemCount - CardConfig.MAX_SHOW_COUNT;
}
for (int i = bottomPosition; i < itemCount; i++) {
// 复用
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
// 布局 -> draw -> onDraw ,onDrawOver, onLayout
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
int level = itemCount - i - 1;
if (level > 0) {
if (level < CardConfig.MAX_SHOW_COUNT - 1) {
view.setTranslationY(CardConfig.TRANS_Y_GAP * level);
view.setScaleX(1 - CardConfig.SCALE_GAP * level);
view.setScaleY(1 - CardConfig.SCALE_GAP * level);
} else {
// 最下面的那个view 与前一个view 布局一样
view.setTranslationY(CardConfig.TRANS_Y_GAP * (level - 1));
view.setScaleX(1 - CardConfig.SCALE_GAP * (level - 1));
view.setScaleY(1 - CardConfig.SCALE_GAP * (level - 1));
}
}
}
}
}
一个简单的自定义 LayoutManager, generateDefaultLayoutParams 直接抄系统的实现即可;
最终实现的效果如下:
仿 探探 的效果,gif 好卡.....;
简历润色
深度理解 RecyclerView 的缓存复用原理,可深度定制 LayoutManager;
下一章预告
带你玩转 ViewPager,实现炫酷 Banner;
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~~