android 全局缓存,【Android进阶】RecyclerView之缓存(二)

前言

上一篇,说了ItemDecoration,这一篇,我们来说说RecyclerView的回收复用逻辑。

问题

假如有100个item,首屏最多展示2个半(一屏同时最多展示4个),RecyclerView 滑动时,会创建多少个viewholder?

先别急着回答,我们写个 demo 看看

首先,是item的布局

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="vertical">

android:id="@+id/tv_repeat"

android:layout_width="match_parent"

android:layout_height="200dp"

android:gravity="center" />

android:layout_width="match_parent"

android:layout_height="2dp"

android:background="@color/colorAccent" />

然后是RepeatAdapter,这里使用的是原生的Adapter

public class RepeatAdapter extends RecyclerView.Adapter {

private List list;

private Context context;

public RepeatAdapter(List list, Context context) {

this.list = list;

this.context = context;

}

@NonNull

@Override

public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false);

Log.e("cheng", "onCreateViewHolder viewType=" + i);

return new RepeatViewHolder(view);

}

@Override

public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) {

viewHolder.tv_repeat.setText(list.get(i));

Log.e("cheng", "onBindViewHolder position=" + i);

}

@Override

public int getItemCount() {

return list.size();

}

class RepeatViewHolder extends RecyclerView.ViewHolder {

public TextView tv_repeat;

public RepeatViewHolder(@NonNull View itemView) {

super(itemView);

this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat);

}

}

}

在Activity中使用

List list = new ArrayList<>();

for (int i = 0; i < 100; i++) {

list.add("第" + i + "个item");

}

RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);

rvRepeat.setLayoutManager(new LinearLayoutManager(this));

rvRepeat.setAdapter(repeatAdapter);

当我们滑动时,log如下:

427c7b0c05cc

image.png

可以看到,总共执行了7次onCreateViewHolder,也就是说,总共100个item,只创建了7个viewholder(篇幅问题,没有截到100,有兴趣的同学可以自己试试)

WHY?

通过阅读源码,我们发现,RecyclerView的缓存单位是viewholder,而获取viewholder最终调用的方法是Recycler#tryGetViewHolderForPositionByDeadline

源码如下:

@Nullable

RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {

...省略代码...

holder = this.getChangedScrapViewForPosition(position);

...省略代码...

if (holder == null) {

holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

}

...省略代码...

if (holder == null) {

View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);

if (view != null) {

holder = RecyclerView.this.getChildViewHolder(view);

}

}

...省略代码...

if (holder == null) {

holder = this.getRecycledViewPool().getRecycledView(type);

}

...省略代码...

if (holder == null) {

holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);

}

...省略代码...

}

从上到下,依次是mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool最后才是createViewHolder

ArrayList mChangedScrap = null;

final ArrayList mAttachedScrap = new ArrayList();

final ArrayList mCachedViews = new ArrayList();

private RecyclerView.ViewCacheExtension mViewCacheExtension;

RecyclerView.RecycledViewPool mRecyclerPool;

mChangedScrap

完整源码如下:

if (RecyclerView.this.mState.isPreLayout()) {

holder = this.getChangedScrapViewForPosition(position);

fromScrapOrHiddenOrCache = holder != null;

}

由于isPreLayout方法取决于mInPreLayout,而mInPreLayout默认为false,即mChangedScrap不参与回收复用逻辑。

mAttachedScrap

完整源码如下:

RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {

int scrapCount = this.mAttachedScrap.size();

int cacheSize;

RecyclerView.ViewHolder vh;

for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) {

vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize);

if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) {

vh.addFlags(32);

return vh;

}

}

}

这段代码什么时候会生效呢,那得找找什么时候将viewholder添加到mAttachedScrap的

我们在源码中全局搜索mAttachedScrap.add,发现是Recycler#scrapView()方法

void scrapView(View view) {

...省略代码...

this.mAttachedScrap.add(holder);

...省略代码...

}

什么时候调用scrapView()方法呢?

继续全局搜索,发现最终是Recycler#detachAndScrapAttachedViews()方法,这个方法又是什么时候会被调用的呢?

答案是LayoutManager#onLayoutChildren()。我们知道onLayoutChildren负责item的布局工作(这部分后面再说),所以,mAttachedScrap应该存放是当前屏幕上显示的viewhoder,我们来看下detachAndScrapAttachedViews的源码

public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) {

int childCount = this.getChildCount();

for(int i = childCount - 1; i >= 0; --i) {

View v = this.getChildAt(i);

this.scrapOrRecycleView(recycler, i, v);

}

}

其中,childCount即为屏幕上显示的item数量。那同学们就要问了,mAttachedScrap有啥用?

答案当然是有用的,比如说,拖动排序,比如说第1个item和第2个item 互换,这个时候,mAttachedScrap就派上了用场,直接从这里通过position拿viewholder,都不用经过onCreateViewHolder和onBindViewHolder。

mCachedViews

完整代码如下:

cacheSize = this.mCachedViews.size();

for(int i = 0; i < cacheSize; ++i) {

RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);

if (!holder.isInvalid() && holder.getLayoutPosition() == position) {

if (!dryRun) {

this.mCachedViews.remove(i);

}

return holder;

}

}

我们先来找找viewholder是在什么时候添加进mCachedViews?是在Recycler#recycleViewHolderInternal()方法

void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {

if (!holder.isScrap() && holder.itemView.getParent() == null) {

if (holder.isTmpDetached()) {

throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());

} else if (holder.shouldIgnore()) {

throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());

} else {

boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();

boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);

boolean cached = false;

boolean recycled = false;

if (forceRecycle || holder.isRecyclable()) {

if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {

int cachedViewSize = this.mCachedViews.size();

if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {

this.recycleCachedViewAt(0);

--cachedViewSize;

}

int targetCacheIndex = cachedViewSize;

if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {

int cacheIndex;

for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {

int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition;

if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {

break;

}

}

targetCacheIndex = cacheIndex + 1;

}

this.mCachedViews.add(targetCacheIndex, holder);

cached = true;

}

if (!cached) {

this.addViewHolderToRecycledViewPool(holder, true);

recycled = true;

}

}

RecyclerView.this.mViewInfoStore.removeViewHolder(holder);

if (!cached && !recycled && transientStatePreventsRecycling) {

holder.mOwnerRecyclerView = null;

}

}

} else {

throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel());

}

}

最上层是RecyclerView#removeAndRecycleViewAt方法

public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {

View view = this.getChildAt(index);

this.removeViewAt(index);

recycler.recycleView(view);

}

这个方法是在哪里调用的呢?答案是LayoutManager,我们写个demo效果看着比较直观

定义MyLayoutManager,并重写removeAndRecycleViewAt,然后添加log

class MyLayoutManager extends LinearLayoutManager {

public MyLayoutManager(Context context) {

super(context);

}

@Override

public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {

super.removeAndRecycleViewAt(index, recycler);

Log.e("cheng", "removeAndRecycleViewAt index=" + index);

}

}

将其设置给RecyclerView,然后滑动,查看日志输出情况

427c7b0c05cc

image.png

427c7b0c05cc

image.png

可以看到,每次有item滑出屏幕时,都会调用removeAndRecycleViewAt()方法,需要注意的是,此index表示的是该item在chlid中的下标,也就是在当前屏幕中的下标,而不是在RecyclerView的。

事实是不是这样的呢?让我们来看看源码,以LinearLayoutManager为例,默认是垂直滑动的,此时控制其滑动距离的方法是scrollVerticallyBy(),其调用的是scrollBy()方法

int scrollBy(int dy, Recycler recycler, State state) {

if (this.getChildCount() != 0 && dy != 0) {

this.mLayoutState.mRecycle = true;

this.ensureLayoutState();

int layoutDirection = dy > 0 ? 1 : -1;

int absDy = Math.abs(dy);

this.updateLayoutState(layoutDirection, absDy, true, state);

int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);

if (consumed < 0) {

return 0;

} else {

int scrolled = absDy > consumed ? layoutDirection * consumed : dy;

this.mOrientationHelper.offsetChildren(-scrolled);

this.mLayoutState.mLastScrollDelta = scrolled;

return scrolled;

}

} else {

return 0;

}

}

关键代码是fill()方法中的recycleByLayoutState(),判断滑动方向,从第一个还是最后一个开始回收。

private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) {

if (layoutState.mRecycle && !layoutState.mInfinite) {

if (layoutState.mLayoutDirection == -1) {

this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);

} else {

this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset);

}

}

}

扯的有些远了,让我们回顾下recycleViewHolderInternal()方法,当cachedViewSize >= this.mViewCacheMax时,会移除第1个,也就是最先加入的viewholder,mViewCacheMax是多少呢?

public Recycler() {

this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);

this.mRequestedCacheMax = 2;

this.mViewCacheMax = 2;

}

mViewCacheMax为2,也就是mCachedViews的初始化大小为2,超过这个大小时,viewholer将会被移除,放到哪里去了呢?带着这个疑问我们继续往下看

mViewCacheExtension

ViewCacheExtension 这个类需要使用者通过 setViewCacheExtension() 方法传入,RecyclerView自身并不会实现它,一般正常的使用也用不到。

mRecyclerPool

我们带着之前的疑问,继续看源码,之前提到mCachedViews初始大小为2,超过这个大小,最先放入的会被移除,移除的viewholder到哪里去了呢?我们来看recycleCachedViewAt()方法的源码

void recycleCachedViewAt(int cachedViewIndex) {

RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);

this.addViewHolderToRecycledViewPool(viewHolder, true);

this.mCachedViews.remove(cachedViewIndex);

}

addViewHolderToRecycledViewPool()方法

void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) {

RecyclerView.clearNestedRecyclerViewIfNotNested(holder);

if (holder.hasAnyOfTheFlags(16384)) {

holder.setFlags(0, 16384);

ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null);

}

if (dispatchRecycled) {

this.dispatchViewRecycled(holder);

}

holder.mOwnerRecyclerView = null;

this.getRecycledViewPool().putRecycledView(holder);

}

可以看到,该viewholder被添加到mRecyclerPool中

我们继续看看RecycledViewPool的源码

public static class RecycledViewPool {

private static final int DEFAULT_MAX_SCRAP = 5;

SparseArray mScrap = new SparseArray();

private int mAttachCount = 0;

public RecycledViewPool() {

}

...省略代码...

}

static class ScrapData {

final ArrayList mScrapHeap = new ArrayList();

int mMaxScrap = 5;

long mCreateRunningAverageNs = 0L;

long mBindRunningAverageNs = 0L;

ScrapData() {

}

}

可以看到,其内部有一个SparseArray用来存放viewholder。

总结

总共有mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool4级缓存,其中mAttachedScrap只保存布局时,屏幕上显示的viewholder,一般不参与回收、复用(拖动排序时会参与);

mCachedViews主要保存刚移除屏幕的viewholder,初始大小为2;

mViewCacheExtension为预留的缓存池,需要自己去实现;

mRecyclerPool则是最后一级缓存,当mCachedViews满了之后,viewholder会被存放在mRecyclerPool,继续复用。

其中,mAttachedScrap、mCachedViews为精确匹配,即为对应position的viewholder才会被复用;

mRecyclerPool为模糊匹配,只匹配viewType,所以复用时,需要调用onBindViewHolder为其设置新的数据。

回答之前的疑问

当滑出第6个item时,这时mCachedViews中存放着第1、2个item,屏幕上显示的是第3、4、5、6个item,再滑出第7个item时,不存在能复用的viewholder,所以调用onCreateViewHolder创建了一个新的viewholder,并且把第1个viewholder放入mRecyclerPool,以备复用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值