RecyclerView详解一,使用及缓存机制

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

本文大致会先讲解RecyclerView的基础知识及使用最后会深入讲解一点原理。当然本人知识水平有限哈太深入的东西我现在还没接触到还请大家包容阿里嘎多~

一、RecyclerView的历史与发展

既然讲到了RV那不得不先知道它怎么来的。
 
RecyclerView是Android 5.0提出的新的UI控件与其一起诞生的还有著名的Material Design以及CardView等新特性。最初位于support.v7包中这里既然提到了v7那我就简单介绍一点v4v7包以及androidx的历史发展。support-v4是Android 3.0推出的库为了加入Fragment以及向下兼容老系统即最低兼容到Android 1.6。support-v7向下兼容到Android 2.1这两个库中包含有RecyclerView、ViewPager等常用控件。随着时间的推移现在的Android系统已经发展到13了显然这两个库就有些跟不上时代了于是从Android 9.0开始Google推出了androidx以后推出的所有新特性都会加入到androidx中并且androidx包下面的API都是随着扩展库发布的这些API基本不会依赖于操作系统的具体版本所有命名中它就不再包含版本号了。
 
所以现在我们使用的RecyclerView都是包含在androidx包中。我们最开始学Android的时候肯定都接触过ListViewListView的功能也很强大在RecyclerView没出现之前开发者们使用的都是ListView来展示大量的数据。但是ListView的性能比较差之后我会对比一下二者的缓存策略扩展性也不是很好所以具有更加强大功能的RecyclerView诞生了。它包含有横向纵向排列的LinearLayoutManager、网格排列的GridLayoutManager和瀑布流排列的StaggeredGridLayoutManager。下面我先来带大家简单了解一下RecyclerView的使用。

二、RecyclerView的使用

这部分我不会讲很多毕竟学会使用它也不是很难具体的大家可以去参考一下《第一行代码》本文的重点还是放在更深一点的缓存策略回收复用LayoutManager 以及 ItemTouchHelper等分析上。

1创建数据列表

这里演示的就不搞那么复杂了

class Data(val string: String)

2创建Adapter

class BasicAdapter(private val dataList: List<Data>) : RecyclerView.Adapter<BasicAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val dataString: TextView = view.findViewById(R.id.tv_str)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.title_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = dataList[position]
        holder.dataString.text = data.string
    }

    override fun getItemCount(): Int = dataList.size
}

3Activity创建RecyclerView对象

// 这是单向布局
val layoutManager = LinearLayoutManager(this)
vb.recyclerView.layoutManager = layoutManager
val adapter = BasicAdapter(dataList)
vb.recyclerView.adapter = adapter

// 网格布局
// 将第一行代码替换为
val layoutManager = LinearLayoutManager(this 2) // 表示分两列排布

// 瀑布流布局
val layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)

演示结果就不给大家展示了比较简单一笔带过。接下来的内容就涉及到更加深入的部分了。

三、RecyclerView的缓存复用机制

RecyclerView的性能之所以强大就是得益于它的缓存机制。我们通常认为RV具有四级缓存机制而官方表示只有三级这里我还是以四级缓存来讲述。下面是RV的四级缓存结构图。
RV四级缓存结构

层级缓存变量缓存名用途
1mChangeScrap与 mAttachedScrap可见缓存用于布局过程中屏幕可见表项的回收和复用
2mCachedViews缓存列表用于移出屏幕表项的回收和复用不会清空数据
3mViewCacheExtension自定义缓存自定义一个缓存我们一般用不到
4RecycledViewPool缓存池用于移出屏幕表项的回收和复用会将ViewHolder的数据重置

在正式介绍四级缓存之前我们还需要了解一下RV的Item的几个状态。

方法FLAG含义具体场景
isInvalid()FLAG_INVALIDViewHolder的数据是无效的1. 调用了setAdapter()
2. 调用了notifyDataSetChanged()等方法
isRemoved()FLAG_REMOVEDViewHolder的数据已经被移除调用了notifyItemRemoved()
isUpdated()FLAG_UPDATEViewHolder的数据需要重新绑定1. isInvalid的几种情况
2. 调用了onBindViewHolder()
3. 调用了notifyItemChanged()
isBound()FLAG_BOUND数据已经绑定了某个Item上数据是有效状态调用了onBindViewHolder()

1. 一级缓存

1一级缓存原理

Scrap是RV中最轻量的缓存包括mChangeScrap和mAttachedScrap只是作为临时缓存的存在。主要用于缓存出现在屏幕内的item当我们通过notifyItemRemoved()notifyItemChanged()通知item发生变化的时候通过mAttachedScrap缓存没有发生变化的ViewHolder其他的则由mChangedScrap缓存添加itemView的时候快速从里面取出完成局部刷新。通过源码理解一下。

void scrapView(View view) {
	final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
    	if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap, they should rebound from"
                    + " recycler pool." + exceptionLabel());
        }
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
   	} else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

可以看到对于页面中显示的Item当调用 LayoutManager 类的 onLayoutChildren() 方法对views进行布局这时会将RecyclerView上的items全部暂存到一个 ArrayList 集合这里的数据是没有做修改的所以不用重新绑定 Adapter。而如果其他情况比如调用了 notifyItemChanged()notifyItemRangeChanged() 来通知数据发生了更新数据或位置发生改变那么该ViewHolder会被缓存到mChangedScrap中这里存储的是发生了变化的ViewHolder所以要重新走Adapter的绑定方法。可能我文字表述的不是很清楚下面我放一张图来助大家理解。
在这里插入图片描述
图中的itemB删除掉然后itemCitemD依次移动上来这里itemA和itemB前后参数没有发生变化虽然itemB被移除了但移除的时候它还是有效的会被打上REMOVED标签表示它是要删除的所以他们两个存储到mAttachedscrap()而itemC和itemD的位置发生了改变所以他俩要存到mChangedScrap()中去。总结来说删除itemB时ABCD都会进入Scrap缓存删除后会从Scrap中将ACD取出A的位置和数据都没有发生变化CD的位置发生了变化但数据还是原封不动。

2一级缓存复用

复用的源码在ViewHolder tryGetViewHolderForPositionByDeadline(*)方法中。我对源码的解释在代码的注释里。

ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 这里是从mChangedScrap()取ViewHolderisPreLayout()判断是否为预布局是一个特殊情况。
// 那什么是预布局呢稍后我讲LayoutManager的时候会具体说一下
if (mState.isPreLayout()) {            
	holder = getChangedScrapViewForPosition(position);            
    fromScrapOrHiddenOrCache = holder != null;
}

// OK现在我们点进getChangedScrapViewForPosition()方法中看一下是怎么取得ViewHolder
// find by position
// 这是按照position来取
for (int i = 0; i < changedScrapSize; i++) {
	final ViewHolder holder = mChangedScrap.get(i);
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
    	holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); // 添加标签表示从Scrap取
        return holder;
    }
}
// find by id
// 这是通过定义的id来取
if (mAdapter.hasStableIds()) {
	final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
    	final long id = mAdapter.getItemId(offsetPosition);
        for (int i = 0; i < changedScrapSize; i++) {
            // 从mChangedScrap中取holder
        	final ViewHolder holder = mChangedScrap.get(i);
            if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
            	holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                return holder;
            }
        }
    }
}

从这里开始就是缓存复用真正的第一步上面的是预加载的特殊情况。

// 1) Find by position from scrap/hidden list/cache
// 这里就是从mAttachedScrap()中取ViewHolder了
if (holder == null) {
	holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
    	// 这里还要检验ViewHolder的有效性
    	if (!validateViewHolderForOffsetPosition(holder)) {
        	// recycle holder (and unscrap if relevant) since it can't be used
            if (!dryRun) {
            	// we would like to recycle this but need to make sure it is not used by
                // animation logic etc.
                holder.addFlags(ViewHolder.FLAG_INVALID);
                if (holder.isScrap()) {
                	removeDetachedView(holder.itemView, false);
                    holder.unScrap();
                } else if (holder.wasReturnedFromScrap()) {
                  	holder.clearReturnedFromScrapFlag();
                }
                // 如果不满足有效性则直接回收该ViewHolder
                recycleViewHolderInternal(holder);
            }
            holder = null;
         } else {
          	fromScrapOrHiddenOrCache = true;
         }
    }
}

// 这是从mAttachedScrap()取VH的核心源码和上面的差不多。
// 只是多了几个判断条件该holder须是有效的并且未被移除。
final int scrapCount = mAttachedScrap.size();

// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
	final ViewHolder holder = mAttachedScrap.get(i);
	// 第二个条件为索引判断表示只能复用到指定位置
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
    		&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
    	holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
}

从源码中可以看到ViewHolder的复用是有顺序的首先会判断是否预布局如果是就从一级缓存中的mChangedScrap()中获取。如果没获取到就去mAttachScrap()和二级缓存中找。而一级缓存之所以说轻量首先是因为它只针对当前页面显示的这些item其次是因为它用完就会清空缓存不占空间效率也快。所以通知数据更新我们推荐使用notifyItemChanged()实现局部刷新用的是一级缓存来实现复用。而如果我们调用notifyDataChanged()来通知更新会使数据全部进行刷新不会走Scrap性能低下。

3. 二级缓存

1二级缓存原理

CacheView用于RecyclerView列表位置产生变动时通常称为离屏缓存对刚刚移出屏幕的view进行回收。它的默认容量是2可以修改同样我们用一张图来帮助理解一下。
在这里插入图片描述

2二级缓存复用

接着上面的一级缓存复用讲。
这里还是getScrapOrHiddenOrCachedHolderForPosition()方法里的如果刚才从mAttachScrap里没取到ViewHolder那么就会走二级缓存从mCachedViews里找。

// Search in our first-level recycled view cache. 官方说这里是第一级但在我们日常使用中还是称他为第二级缓存
// 这是根据position来取
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
	final ViewHolder holder = mCachedViews.get(i);
    // invalid view holders may be in cache if adapter has stable ids as they can be
    // retrieved via getScrapOrCachedViewForId
    // 这里要对索引进行判断只有当位置对得上才能拿来复用
    // 这也就意味着从mCatchedViews中取出的ViewHolder只能复用到指定的位置。
    if (!holder.isInvalid() && holder.getLayoutPosition() == position
    		&& !holder.isAttachedToTransitionOverlay()) {
    	// 如果不在容量范围内就把ViewHolder丢出去丢到缓存池中。
    	if (!dryRun) {
        	mCachedViews.remove(i);
        }
        if (DEBUG) {
        	Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
            		+ ") found match in cache: " + holder);
        }
        return holder;
    }
}

接下来就是源码里的第三步这里是根据id来取上面的是根据position来取相差不大。

// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
	holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
    		type, dryRun);
    if (holder != null) {
    	// update position
        holder.mPosition = offsetPosition;
        fromScrapOrHiddenOrCache = true;
    }
}

// 点进getScrapOrCachedViewForId()
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
	final ViewHolder holder = mCachedViews.get(i);
    ......
}

CachedView的缓存主要是应对来回滑动的情况这时候CachedView才会真正的起作用其缓存的ViewHolder不需要重新赋值就可以直接拿来用了。而且我们还可以修改它的容量通过下面这个方法来修改。

public void setItemViewCacheSize(int size) {
    mRecycler.setViewCacheSize(size);
}

总结一下mAttachedScrapp和mCachedViews都是需要进行索引判断也就是说从这两个缓存中取出的ViewHolder只能复用到指定的位置。mCachedViews只能缓存屏幕外它容量大小的ViewHolder超出容量的部分会被移除丢到缓存池中一会我再来具体讲解缓存池。

5. 三级缓存

三级缓存ViewCacheExtension是我们自定义的缓存一般来说官方给的一、二、四级缓存就够用了我们不会用到它所以我也就一笔带过了。

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);
	......
}

从源码中分析如果我们自定义了一个缓存并且前面的一二级缓存没有找到ViewHolder系统就会从我们自定义的这个缓存里去找ViewHolder。

6. 四级缓存

好的本文的第一个重点来了在这里我会详细地分析RV的缓存池机制。
先来看看这一级的复用机制。

1缓存池结构

if (holder == null) { // fallback to pool
	if (DEBUG) {
    	Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
        		+ position + ") fetching from shared pool");
    }
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
    	holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
        	invalidateDisplayListInt(holder);
        }
    }
}

可以看到这一部分和之前的一二级缓存复用机制有很大区别没有那么多的限制条件了不用判断索引是不是指定位置。但是它需要根据itemType来区分不同类型的ViewHolder。但在了解缓存池的复用机制之前我们得先知道RecycledViewPool的基本结构。

public static class RecycledViewPool {
	//同类ViewHolder缓存个数上限为5
    private static final int DEFAULT_MAX_SCRAP = 5;
    // Tracks both pooled holders, as well as create/bind timing metadata for the given type.
    // 回收池中存放单个类型ViewHolder的容器
    static class ScrapData {
    	//同类ViewHolder存储在ArrayList中
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
    }
    //回收池中存放所有类型ViewHolder的容器
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
    //ViewHolder入池按viewType分类入池一个类型的ViewType存放在一个ScrapData中
    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;
        }
        if (DEBUG && scrapHeap.contains(scrap)) {
        	throw new IllegalArgumentException("this scrap item already exists");
        }
        scrap.resetInternal();
        //回收时ViewHolder从列表尾部插入
        scrapHeap.add(scrap);
    }
    
    //从回收池中获取ViewHolder对象
    public ViewHolder getRecycledView(int viewType) {
    	// 获取到viewType
    	final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        	final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            //复用时从列表尾部获取ViewHolder优先复用刚入池的ViewHoler
            return scrapHeap.remove(scrapHeap.size() - 1);
        }
        return null;
    }
}

我们可以根据上述分析得到如下结论RecycledViewPool中的ViewHolder存储在SparseArray中并且按viewType分类存储同一类型的ViewHolder存放在一个ArrayList中。虽然没有了对索引的判断但是从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。 说来惭愧就目前以我的水平来看我大部分都只用到一种viewType。

那么现在我们再来分析缓存池的复用过程。

2缓存池复用

holder = getRecycledViewPool().getRecycledView(type);

// 这里是getRecyclerViewPool()主要作用就是new了一个RecyclerViewPool对象出来。
// 然后再根据type来从缓存池中获取对应类型的ViewHolder。
RecycledViewPool getRecycledViewPool() {
	if (mRecyclerPool == null) {
    	mRecyclerPool = new RecycledViewPool();
    }
    return mRecyclerPool;
}

这就是缓存池复用的运作机制相信大家都已经对这部分的内容有所了解了那么什么时候数据会被放到缓存池中呢

3表项放入缓存池的几种情况

item移出屏幕

如果大家有印象的话上面交代了一部分超出mCachedViews的部分会被丢到这里来。这种情况就是当你滑动屏幕item移出到屏幕之外后超出屏幕两个之外的item被缓存池回收为什么是两个之外呢因为这两个是缓存在mCachedViews中的因为它的复用效率更快优先级更高。超出两个之外的按照先入先出的原则被mCachedViews移出缓存。从这里可以看出当你在滑动屏幕的过程中mCachedViews是不断进行 “输入输出” 的。

一级缓存的ViewHolder无效

在讲一级二级缓存复用机制的时候我说过从mAttachedScrapmCachedViews中取ViewHolder时还需要检验有效性具体是怎么检验的呢我们从源码入手。

boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
	// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
    // if it is not removed, verify the type and id.
    // item是否被移除
    if (holder.isRemoved()) {
    	// 如果是被移除的返回是否为预加载
        return mState.isPreLayout();
    }
    
    // 如果不是预加载布局就检查ViewType是否和Adapter对应位置的ViewHolder的相同
    if (!mState.isPreLayout()) {
    	// don't check type if it is pre-layout.
        final int type = mAdapter.getItemViewType(holder.mPosition);
        if (type != holder.getItemViewType()) {
        	// 如果类型不相同返回false即无效
        	return false;
        }
    }

	// 这里是检查id
    if (mAdapter.hasStableIds()) {
    	return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
    }
    return true;
}

从源码上看只有当缓存中 ViewHolder 的 viewType 或 id 和 Adapter 对应位置上的属性相同时简单来说就是只有对得上号的才是有效的ViewHolder才会从一二级缓存中取出复用。否则就会将无效的ViewHolder丢到缓存池中。

还有其他几种情况我就不一一列举了大体差不多。缓存池就相当于一个回收站别人不要的都会往缓存池里塞。接下来我要讲一下从缓存池里拿出来复用的ViewHolder和前面几种有什么区别。

4从缓存池取出的ViewHolder和前面的区别

holder = getRecycledViewPool().getRecycledView(type);
// 刚才没贴的代码现在补上
if (holder != null) {
	holder.resetInternal();
    if (FORCE_INVALIDATE_DISPLAY_LIST) {
    	invalidateDisplayListInt(holder);
    }
}

这段代码什么意思呢就是当ViewHolder从缓存池取出来后判断holder是否为空如果不为空说明holder从缓存池中取出来了。那么就执行 holder.resetInternal() 意思是将取出的ViewHolder重置我们点进这个方法看一下。

void resetInternal() {
	//将ViewHolder的flag置0剩下的一些属性也将其重置要么置0要么置空
	mFlags = 0;
	mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
    mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
    clearNestedRecyclerViewIfNotNested(this);
}

// 上面重置时将flag置0这里flag与FLAG_BOUND与操作结果必为0
// 所以将flag置0相当于解绑
boolean isBound() {
	return (mFlags & FLAG_BOUND) != 0;
}

综上所述从缓存池里取出来的ViewHolder将其重置复用的时候再重新绑定数据。而一二级缓存无需再绑定数据直接拿来复用因为他们的位置和数据都没有变化。当有相同类型的表项插入列表时不用重新创建 ViewHolder 实例执行 onCreateViewHolder()从缓存池中获取即可。到这里RV的四级缓存复用机制就差不多讲完了大家也对RV有了更深一步的了解但是RV是什么时候又是怎么将这些ViewHolder填充屏幕的呢这个问题我们还需深入探讨一下LayoutManager相信大家对这个也不陌生。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6