RecyclerView 缓存

113 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

RecyclerView

我们都知道和缓存相关的操作都在Recycler这个内部类中,更具体的则和Recycler中这三个方法相关:

  • getChangedScrapViewForPosition(int position)
  • getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun)
  • getScrapOrCachedViewForId(long id, int type, boolean dryRun)

getChangedScrapViewForPosition(int position)

在mChangedScrap的缓存数组中获取ViewHolder,这个函数中有两个获取ViewHolder的策略:

1.根据mChangedScrap中的viewholder的getLayoutPosition与该函数参数position对比若相同则返回该viewholder

2.如果该adapter的StableIds设置为true了(默认为假),则如果第一步没有找到该viewholder则根据adapter中getItemId(int position)与缓存mChangedScrap中的id对比若相同则返回该viewholder

getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun)

1.根据mAttachedScrap中的viewholder的getLayoutPosition与该函数参数position对比若相同则返回该viewholder

2.如果第一步没有寻找到,则前往mCachedViews中寻找,和mChangedScrap一样都是比较的getLayoutPosition的返回值

getScrapOrCachedViewForId(long id, int type, boolean dryRun)

和该函数的名字一样,它比较的是id而不是position

涉及的缓存是mAttachedScrap和mCachedViews,都是比较的id

RecyclerView中还有一个关键函数tryGetViewHolderForPositionByDeadline

所谓的四层缓存正是在这里发生

1.首先调用getScrapOrHiddenOrCachedHolderForPosition这个函数获取viewholder 也就是说:首先会去mAttachedScrap和mCachedViews中根据posotino寻找

2.若第一步未能成功,则调用getScrapOrCachedViewForId根据id寻找

3.若前两步未能成功,则会根据mViewCacheExtension获取viewholder,mViewCacheExtension是一个可自定义的缓存机制;可以通过setViewCacheExtension该方法设置,但是仅能设置一个

4.若前三步都未成功获取到viewholder,则会根据RecycledViewPool获取相应的viewholder,这时比较的是ViewType,而该viewtype正是adapter中getItemViewType的返回值。RecycledViewPool有个特点就是:如果从这里获取的holder需要重新bind。

5.若仍未能获取到holder,则会创建一个新的viewholder

6.不管在哪个缓存获取到的holder,holder中的view都需要重新测量。

LinearLayoutMnager

当RecyclerViewmeasure过程或者滑动过程都会调用fill函数用来向Recyclerview添加view。 添加多少由LayoutState和adapter的ItemCount决定。

LayoutState

滑动时对state的处理

updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
        + fill(recycler, mLayoutState, state, false);

创建recyclerview时对state的处理

updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
  • 向RecyclerView添加多少view?
//主要关注remainingSpace这个值

int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    layoutChunkResult.resetInternal();
    
    layoutChunk(recycler, state, layoutState, layoutChunkResult){
    //layoutChunk函数内部提到这里了
        View view = layoutState.next(recycler);
        layoutChunkResult.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);//获取该view的高度
        
    }
    
    remainingSpace -= layoutChunkResult.mConsumed;
 }

measure过程首先填充的大小是该RecyclerView的height,每从adapter获取一个view就会测量该view并从可填充大小中减去该view的高度(包括该view的margin)。

  • 当滑动时,并不是每次滑动都会创建一个新的view的: 1.首先会获取当前列表的最后一个view,并获取该view距离recyclerview底部距离。这个距离表示,在没有创建新view时可以滑动的距离。 2.然后把当前滑动的距离减去第一步获取到的距离,如果该差值大于0,说明需要创建一个新view来满足当前的滑动。 3.如果该差值小于等于0就不需要创建新view。

RecyclerView滑动的实质就是调用了子view的offsetTopAndBottom(dy);

回收view

回收的过程也是在fill()函数中调用的

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    
   //只有在移动时才会调用该函数,measure,layout,draw过程不会调用
   recycleByLayoutState(recycler, layoutState);
   
   .....填充view....
    
 }

先回收再填充

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
    //向下滑动
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
    //向上滑动
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

回收策略

以recycleViewsFromStart为例
for (int i = 0; i < childCount; i++) {
    View child = getChildAt(i);
    //表示如果滑动后该view仍然在可视范围内则回收[0,i)范围的view
    if (mOrientationHelper.getDecoratedEnd(child) > limit
            || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
        // stop here
        recycleChildren(recycler, 0, i){
        //内部逻辑移到这里来
        //首先会remove掉该view,然后使用回收器回收,所以重点在如何回收
            removeViewAt(index);
            recycler.recycleView(view);
        }
        return;
    }
}
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)) {
            
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        
    }
   
}

回收时只会往recycledViewpool和cachedView中存放

当cachedView的满时(cachedView初始大小为2),就会移出index = 0的holder并把该holder添加到recycledViewpool中,但是recycledViewpool满了就无法放了,由于移出holder导致空出来位置,所以就会再次把holder放进cachedView,所以cachedView中存放的是刚移出的holder,recycledViewpool中存放的是更靠前的holder

每当从缓存中取出holder后,都会把该holder移除。

对于mAttachedScrap:一般用于临时缓存,缓存那些当前正在显示,可以拿来直接用的item,如果notify更新数据时就可以把那些不会更新的holder放进mAttachedScrap,需要更新的放进mChangedScrap.

adapter的notify是如何运转

当调用notifyDataSetChanged()时,会给每一个holder添加flags

holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);

cachedView中的holder会移到recycleViewPool中 然后运行requestLayout()

所以这时就会重新布局,必然会来到onLayoutChildren()中

LinearLayoutManager的onLayoutChildren有这么一个代码:

detachAndScrapAttachedViews(recycler);

这个函数的执行是没有任何条件的;

他会把那些设置ViewHolder.FLAG_INVALID标志的holder(也就是该RecyclerView的所有子view)中的view从recyclerview中移除,然后将这些holder放到recycleViewPool中,这个缓存池每种viewType只能放5个,所以会有放满的时候,这时候就需要重新创建holder,这就是耗时的地方。而且从recycleViewPool中获取到的holder也需要重新bind。