开启掘金成长之旅!这是我参与「掘金日新计划 · 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。