从源码视角初步了解RecycleView

293 阅读8分钟

0 前言

RecyclerView 是 Android 中一个非常重要的组件,用于灵活、高效的展示大量数据集合。本文将从源码对RecycleView进行分析其回收复用机制。

1 RecyclerView 回收复用机制初体验:从日志分析开始

我们先对 RecyclerView 我们试着对RecyclerView的onCreateViewHolderonBindViewHolder方法添加Log日志,查看调用的时机。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        ...
        Log.d("ViewHolder","onCreateViewHolder:${itemCount}")
        return RVViewHolder(view);
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        ...
        Log.d("ViewHolder", "onBindViewHolder:$position")
    }

一开始没有滑动的时候: 在这里插入图片描述 发现onCreateViewHolderonBindViewHolder都有被调用。

当我们向上划动的时候: 在这里插入图片描述 发现一开始onCreateViewHolderonBindViewHolder都有被调用,后来只有onBindViewHolder被调用。

当我们向下滑动的时候: 在这里插入图片描述 发现只有onBindViewHolder被调用。

我们可以看出,不论是上划还是下滑,除了一开始滑动时需要调用 onCreateViewHolder 外,之后的滑动仅需要调用 onBindViewHolder 方法。这就是 RecyclerView 的四级缓存的作用:

2 初步了解 Recycler

Recycle 是 RecyclerView 的一个内部类,是 RecyclerView 缓存复用机制的核心。我们简单了解一下 Recycler 的一部分成员变量:

public final class Recycler {
	//屏幕内缓存
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();	// 未变更的屏幕内 ViewHolder
    ArrayList<ViewHolder> mChangedScrap = null;	// 数据变更的屏幕内 ViewHolder(懒加载)

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();	// 离屏缓存(最近使用的 ViewHolder)
    private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);	// 不可变视图,防止外部修改

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;	// 请求的缓存最大值(用户设置)
    int mViewCacheMax = DEFAULT_CACHE_SIZE;	// 实际生效的缓存最大值(可能被调整)

    RecycledViewPool mRecyclerPool;	// 共享缓存池(长期离屏的 ViewHolder)

    private ViewCacheExtension mViewCacheExtension;	// 开发者自定义缓存扩展

    static final int DEFAULT_CACHE_SIZE = 2;	// 默认离屏缓存容量

}

从 Recycler 的成员变量中,我们可以看出 RecyclerView 的四级缓存分别是:

  1. Scrap 缓存(屏幕内缓存):mChangedScrapmAttachedScrap,用来缓存还在屏幕内的 ViewHolder。
  2. CachedViews(离屏缓存):mCachedViews,用来缓存移出屏幕之外的 ViewHolder。
  3. 自定义缓存:mViewCacheExtension,开发者自定义缓存扩展。
  4. 共享缓存池:mRecyclerPool,ViewHolder 缓存池。

3 源码解析

3.1 onTouchEvent 切入

我们知道 RecyclerView 会对移出屏幕的 ViewHolder 回收,新进入屏幕的item将会对缓存的 ViewHolder 复用。因此我们可以从触摸方法中开始找切入点,首先先看一下 onTouchEvent 方法中处理滑动的调用流程:

/** RecyclerView.java **/
public boolean onTouchEvent(MotionEvent e) {
    // ...
    // mLayout 即为 LayoutManager
    if (mLayout == null) {
        return false;
    }
    // ...
    switch (action) {
        // ...
        case MotionEvent.ACTION_MOVE: {
            // ...
            // 根据 Down 事件中保存的信息计算出滑动距离
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            // 滑动前先进行方向判断
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                boolean startScroll = false;
                if (canScrollHorizontally){
                	if (dx > 0) {
                        dx = Math.max(0, dx - mTouchSlop);
                    } else {
                        dx = Math.min(0, dx + mTouchSlop);
                    }
                    if (dx != 0) {
                        startScroll = true;
                    }
                }
                // canScrollVertically,与横向类似
                if (startScroll) { // 标记开始滑动
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }
            
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // ...
                // scrollByInternal 处理滑动的方法
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e, TYPE_TOUCH)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // ...
            }
        }
        // ...
}

boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
    // ...
    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // scrollStep 方法中计算出了滑动消耗的距离,并且赋值给了 mReusableIntPair
        scrollStep(x, y, mReusableIntPair);
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        unconsumedX = x - consumedX;
        unconsumedY = y - consumedY;
    }
    // ...
}

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ...
    int consumedX = 0;
    int consumedY = 0;
    // 不管是水平还是垂直滑动 最终都调用到了 LayoutManager 的 scrollXXXBy 方法中
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    //...
    if (consumed != null) {
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

由于 RecyclerView 的滑动最终是交给 LayoutManager 去处理,我们以 LinearLayoutManager 的 scrollHorizontallyBy 为例( scrollVerticallyBy 类似),进去后看到:

/** LinearLayoutManager.java **/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
    RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        return 0;
    }
    return scrollBy(dx, recycler, state);
}

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    final int consumed = mLayoutState.mScrollingOffset
        + fill(recycler, mLayoutState, state, false);
    // ...
}

// 【重要】
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
		RecyclerView.State state, boolean stopOnFocusable) {
	final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // ..
        // 回收
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
	LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
	while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
		// ...
		// 复用
 		layoutChunk(recycler, state, layoutState, layoutChunkResult);
		// ...
	}
	// ...
}

在 RecyclerView 滑动时回收和复用操作会同时触发,有移除屏幕的 ViewHolder 就需要有新的 ViewHolder 来填补。 在这里我们分为回收路线和复用路线,分别来看。

3.2 回收路线

/** LinearLayoutManager **/
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    // ...
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
    	// 回收头部
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
    	// 回收尾部,与头部类似
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}
// 以 recycleViewsFromEnd 为例
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    final int childCount = getChildCount();
    // ...
    if (mShouldReverseLayout) {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // stop here
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    } else {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // stop here
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    }
}

private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    // ...
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    } else {
        for (int i = startIndex; i > endIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    removeViewAt(index);
    // 让 RecycleView 回收
    recycler.recycleView(view);
}

此时我们回到 RecyclerView 继续。

/** RecyclerView.java */
public void recycleView(@NonNull View view) {
    ViewHolder holder = getChildViewHolderInt(view);
    // ...
    recycleViewHolderInternal(holder);
    // ...
}

void recycleViewHolderInternal(ViewHolder holder) {
    // ...
    // 强制回收判断
    final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
    @SuppressWarnings("unchecked")
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder);
    
    boolean cached = false;
    boolean recycled = false;
    // ...
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // 缓存已满时,淘汰最旧的 ViewHolder
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);	//移出最旧的ViewHolder
                cachedViewSize--;
            }
            // 添加到 mCachedViews 缓存
            int targetCacheIndex = cachedViewSize;
            // ...
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        // 如果没有添加到 mCachedView 中则直接加入 RecycledViewPool 中
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // ...
    }
    // ...
}

void recycleCachedViewAt(int cachedViewIndex) {
	// 把需要操作的 ViewHolder 拿出来放到 RecycleViewPool
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    addViewHolderToRecycledViewPool(viewHolder, true);
    mCachedViews.remove(cachedViewIndex);
}

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    View itemView = holder.itemView;
    // ...
    if (dispatchRecycled) {
    	// 通知监听器 ViewHolder 已回收
        dispatchViewRecycled(holder);
    }
    holder.mOwnerRecyclerView = null;
    // 存入 RecycledViewPool
    getRecycledViewPool().putRecycledView(holder);
}

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. 当 Item 完全离屏时,LinearLayoutManager 通过 recycleByLayoutState 确定回收范围,调用 recycler.recycleView(view),此时 ViewHolder 会被自动从从 mAttachedScrap/mChangedScrap 移除。
  2. 回收的 ViewHolder 如果存在于一级缓存 mAttachedScrapmChangedScrap 中,则自动从中移除;
  3. 将 ViewHolder 加入二级缓存 mCacheViews 中,如果容量超出,则移除 index 为 0 的 ViewHolder,并且将其添加到 RecyclerViewPool 中(原来在 mCacheViews 的 index 值会减一),mCachedViews 中的 ViewHolder 不重置状态;
  4. 如果 ViewHolder 在第 2 步中没有成功添加到 mCacheViews 中则重置 ViewHolder 装填并添加进 RecyclerViewPool 缓存,且按照类型分类存储;
  5. RecyclerViewPool 中会根据 ItemViewType 获取对应 ViewHolder 的 ArrayList 容器(如果还没初始化则直接 new 一个)判断容量后添加进去,添加前会清除 ViewHolder 的一些状态信息。每个类型的缓存独立,容量不足时会丢弃新的 ViewHolder。

无论存入哪级缓存,都会通过 dispatchViewRecycled(holder) 通知监听器,开发者可在此释放资源。

3.3 复用路线

接下来看看复用部分layoutChunk的代码:

/** LinearLayoutManager **/
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    // ...
}

View next(RecyclerView.Recycler recycler) {
	if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    // 最终通过 recycler 的 getViewForPosition 方法获取到了 View
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

回到 Recycler 里的 getViewForPosition

/** RecyclerView.java **/
@NonNull
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

// 【核心】
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ...
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 一级缓存
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 二级缓存
    // 1) Find by position from scrap/hidden list/cache
    // 通过 position 寻找
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // ...
    }
    if (holder == null) {
        // ...
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        // 二级缓存,通过 Id 寻找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type,dryRun);
            // ...
        }
        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);
                // ...
            }
        }
        if (holder == null) { // fallback to pool
        	// ...
            // 四级缓存,缓存池
            holder = getRecycledViewPool().getRecycledView(type);
            // ...
        }
        if (holder == null) {
            // ...
            // 没有缓存,创建新的ViewHolder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            // 预加载机制
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }
            mRecyclerPool.factorInCreateTime(type, end - start);
            // ...
        }
    }

	// 数据绑定部分
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        // 预布局
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // ...
        // 未绑定、需要更新或无效时,执行数据绑定
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
	//布局参数处理
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

我们从 tryGetViewHolderForPositionByDeadline 方法中可以看到,每个 if 都会判断 holder 是否已经拿到并复用,如果已经拿到,则后面的 if 语句都进不去,运行到后续并返回 holder ,否则 holder == null,则进行下一层的从缓存中复用。

总结一下四级缓存的复用流程:

一级缓存(mAttachedScrap → mChangedScrap) → 二级缓存(mCachedViews) → 稳定ID查找(在Scrap/CachedViews中) → 三级缓存(自定义) → 四级缓存(RecycledViewPool) → 创建新实例
  1. 一级缓存 getChangedScrapViewForPosition( ):根据 positionmChangedScrap 中获取;
  2. 二级缓存 getScrapOrHiddenOrCachedHolderForPosition( )getScrapOrCachedViewForId():优先根据 positionmAttachScrapmCacheViews 中获取(按位置查找未变更的缓存),其次根据 Id 再次从 mAttachScrapmCacheViews 中获取;
  3. 三级缓存 mViewCacheExtension.getViewForPositionAndType( ),自定义缓存,一定注意这里返回的是 View;
  4. 四级缓存 getRecycledViewPool().getRecycledView( ),根据 itemViewType 获取对应 ViewHolder 的缓存 ArrayList,再从其中尝试获取。

如果上述的所有步骤都没有复用到 ViewHolder,则调用 mAdapter.createViewHolder( ) 方法直接新建一个 ViewHolder,此时调用到的就是 adapter 中的 onCreateViewHolder( ) 方法。

上述所有过程运行结束之后,此时不论是通过复用还是新建,holder 都已经不为 null 了,则会调用 tryBindViewHolderByDeadline( ) 方法进行数据处理,adapter 的 onBindViewHolder( ) 方法就是在这里调用到的。