Android进阶之路Android自定义控件性能优化

谈谈RecyclerView中的缓存

2022-02-13  本文已影响0人  Kael_祈求者

Android深入理解RecyclerView的缓存机制

RecyclerView在项目中的使用已经很普遍了,可以说是项目中最高频使用的一个控件了。除了布局灵活性、丰富的动画,RecyclerView还有优秀的缓存机制,本文尝试通过源码深入了解一下RecyclerView中的缓存机制。

1. RecyclerView缓存机制与性能优化关系

RecyclerView做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载等等。其优化的点很多,在这些看似独立的点之间,其实存在一个枢纽:Adapter。因为所有的ViewHolder的创建和内容的绑定都需要经过Adaper的两个函数onCreateViewHolder和onBindViewHolder。

因此我们性能优化的本质就是要减少这两个函数的调用时间和调用的次数。如果我们想对RecyclerView做性能优化,必须清楚的了解到我们的每一步操作背后,onCreateViewHolder和onBindViewHolder调用了多少次。因此,了解RecyclerView的缓存机制是RecyclerView性能优化的基础。

为了理解缓存的应用场景,本文首先会简单介绍一下RecyclerView的绘制原理,然后再分析其缓存实现原理。

image

2. 绘制原理简述

  1. RecyclerView.requestLayout开始发生绘制,忽略Measure的过程
  2. 在Layout的过程会通过LayoutManager.fill去将RecyclerView填满
  3. LayoutManager.fill会调用LayoutManager.layoutChunk去生成一个具体的ViewHolder
  4. 然后LayoutManager就会调用Recycler.getViewForPosition向Recycler去要ViewHolder
  5. Recycler首先去一级缓存(Cache)里面查找是否命中,如果命中直接返回。如果一级缓存没有找到,则去三级缓存查找,如果三级缓存找到了则调用Adapter.bindViewHolder来绑定内容,然后返回。如果三级缓存没有找到,那么就通过Adapter.createViewHolder创建一个ViewHolder,然后调用Adapter.bindViewHolder绑定其内容,然后返回为Recycler。
  6. 一直重复步骤3-5,知道创建的ViewHolder填满了整个RecyclerView为止。

RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。我们知道设置RecyclerView时需要设置LayoutManager,LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。以LinearLayoutManager为例,当RecyclerView重新布局时会依次执行下面几个方法:

上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是从RecyclerView的回收机制实现类Recycler中获取合适的View,下面主要就来从看这个Recycler#getViewForPosition()的实现。

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

//根据传入的position获取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //预布局 属于特殊情况 从mChangedScrap中获取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
        //2、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //如果Adapter中声明了Id,尝试从id中获取,这里不属于缓存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存需要开发者实现
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、从缓存池mRecyclerPool中尝试获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder创建一个
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、如果需要绑定数据,会调用Adapter#onBindViewHolder来绑定数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述逻辑用流程图表示:

3. 缓存机制

3.1 源码简析

RecyclerView在Recyler里面实现ViewHolder的缓存,Recycler里面的实现缓存的主要包含以下5个对象:

public final class Recycler {

final ArrayList mAttachedScrap = new ArrayList<>();

ArrayList mChangedScrap = null;

  public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;
                //mCachedViews的默认大小
        static final int DEFAULT_CACHE_SIZE = 2;

3.2 缓存机制图解

RecyclerView在设计的时候讲上述5个缓存对象分为了3级。每次创建ViewHolder的时候,会按照优先级依次查询缓存创建ViewHolder。每次讲ViewHolder缓存到Recycler缓存的时候,也会按照优先级依次缓存进去。三级缓存分别是:

image

3.3 实例讲解

image
void recycleViewHolderInternal(ViewHolder holder) {
            if (forceRecycle || holder.isRecyclable()) {
                //只有缓存数量大于0,才会放到mCachedViews里
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        //缓存满了移除第一个,放到缓存池中
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //加入缓存
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    //放到缓存池中
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mBindingAdapter = null;
                holder.mOwnerRecyclerView = null;
            }
        }

4. 实战应用

1.使用自定义ViewCacheExtension

//把缓存池中该类型的size设为0 
viewPool.setMaxRecycledViews(CacheUtil.TYPE_SPECIAL, 0)
 recyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
      override fun getViewForPositionAndType(recycler: RecyclerView.Recycler, position: Int, type: Int): View? {
             if (type != CacheUtil.TYPE_SPECIAL) {
                 return null
             }
             if (adapter.cacheView.isEmpty()) {
                 return null
             }
              return adapter.cacheView[0]
       }
})
 class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        val dataList = ArrayList<Int>()
        val cacheView = ArrayList<View>()
        fun setData(list: List<Int>) {
            dataList.addAll(list)
            notifyDataSetChanged()
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            return when (viewType) {
                CacheUtil.TYPE_SPECIAL -> {
                    val view = LayoutInflater.from(parent.context)
                        .inflate(R.layout.item_type_special, parent, false)
                    cacheView.add(view)
                    SpecialViewHolder(view)
                }
            else -> EmptyViewHolder(TextView(parent.context))
        }
}  

使用自定义ViewCacheExtension后,view离屏后再回来不会走onBindViewHolder()方法。

2.让某个ViewHolder不缓存

holder.setIsRecyclable(false),这样的话每次都会走onCreateViewHolder()和onBindViewHolder()方法

3.缓存优化

1.提前初始化viewHolder,放到缓存池中

viewPool.putRecycledView(adapter.onCreateViewHolder(recyclerView, 1))

2.提前初始化view,在onCreateViewHolder的时候去取view

3.自定义ViewCacheExtension

4.适当的增加cacheSize

4.公用缓存池,比如多个viewPager+fragment场景使用,或者全局单利缓存池,感觉用户不大。

5. 注意事项

1.mChangedScrap什么时候会有值

void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

有2中做法有值

第一种

 val anim = DefaultItemAnimator()
 anim.supportsChangeAnimations = true
 recyclerView.itemAnimator = anim
 adapter.notifyItemChanged(1)
 adapter.notifyItemRangeChanged(1, 3)

第二种

 class MyItemAnim : DefaultItemAnimator() {
        override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
            return false
        }

        override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder, payloads: MutableList<Any>): Boolean {
            return false
        }
    }
adapter.notifyItemChanged(1)
adapter.notifyItemChanged(1,"哈哈") 
adapter.notifyItemRangeChanged(1, 3, "哈哈")

2.recyclerView.setItemViewCacheSize(0)代表缓存失效吗?

不会,因为prefetch(GapWorker中的一个方法)之后mViewCacheMax会变成mRequestedCacheMax + extraCache

 @Override
 public void addPosition(int layoutPosition, int pixelDistance) {
       mCount++;
}

 void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
        mCount = 0;
       //layout.mPrefetchMaxCountObserved默认值0
       if (mCount > layout.mPrefetchMaxCountObserved) {
             layout.mPrefetchMaxCountObserved = mCount;
             layout.mPrefetchMaxObservedInInitialPrefetch = nested;
             view.mRecycler.updateViewCacheSize();
        }
}

public void setViewCacheSize(int viewCount) {
       mRequestedCacheMax = viewCount;
       updateViewCacheSize();
}

 void updateViewCacheSize() {
      int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
      mViewCacheMax = mRequestedCacheMax + extraCache;

      // first, try the views that can be recycled
      or (int i = mCachedViews.size() - 1;
             i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
           recycleCachedViewAt(i);
      }
}

有2种方式可以让缓存失效

第一种

recyclerView.setItemViewCacheSize(-1)

第二种

recyclerView.setItemViewCacheSize(0)

layoutManager.isItemPrefetchEnabled = false

设置不缓存后,来回滑动让view进入屏幕离开屏幕,viewHolder的item时会多次走onBindViewHolder()方法。

6. RecyclerView性能优化方向总结

上一篇下一篇

猜你喜欢

热点阅读