RecycleView缓存原理

  • 首先看一下为何说RecycleView(主要有适配器模式,观察者模式adapter通知更新UI)中的adapter是个适配器模式!

适配器模式

  • 在使用recycleView时最常用的就是adapter,也叫作适配器模式,那我们看看都有哪些吧
类适配器
  • 类适配器通过集成与adaptee也就是继承关系,并且实现target方法,重写target方法中调用adaptee的方法,这个是静态的方式
  • 而对象适配器通过委派与adaptee即持有adaptee对象,是动态的方式
  • 类适配器可以重定义实现行为,而对象适配器重定义适配的行为比较困难,但是添加行为较方便。
public interface Target {
    public void request();
}

public class Adaptee {

    public void specialRequest(){
        System.out.println("被适配类 具有特殊功能...");
    }
}
//类继承Adaptee并实现接口,然后通过接口方法调取adaptee的方法即可
public class AdapterInfo extends Adaptee implements Target {
    public static void main(String[] args) {
        Target adaptee = new AdapterInfo();
        adaptee.request();
    }
    @Override
    public void request() {
        super.specialRequest();
    }
}

对象适配器: 使用较多

  • 其实跟静态代理模式差不多
class Adapter implements Target{
    // 直接关联被适配类
    private Adaptee adaptee;

    // 可以通过构造函数传入具体需要适配的被适配类对象
    public Adapter (Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    public void request() {
        // 这里是使用委托的方式完成特殊功能
        this.adaptee.specificRequest();
    }
}
  • 类跟对象适配器区别:类适配器模式需要自身来创建一个adaptee,对象适配器模式可以直接使用一个已有的adaptee实例来转换接口

RecycleView中的adapter

  • 要想搞明白他的适配器模式,我们必须的看他的继承关系
//RecycleView的类继承关系
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2
//adapter的继承关系
class FooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
//Adapter
public abstract static class Adapter<VH extends RecyclerView.ViewHolder> {
        private final RecyclerView.AdapterDataObservable mObservable = new RecyclerView.AdapterDataObservable();
        private boolean mHasStableIds = false;

        public Adapter() {
        }
        
//mObservable是每一个Adapter创建一个内部类,观察者模式监听条目的变化
 static class AdapterDataObservable extends Observable<RecyclerView.AdapterDataObserver> {
        AdapterDataObservable() {
        }
  • 通过以上分析,我们使用了RecycleView.Adapter是一个抽象类定义的适配器,这个抽象类的方法要提供给RecycleView内部使用的,因此实现一个FootAdapter的实现类,然后传递给RecycleView.setAdapter(adapter)就是一个对象适配器
  • 而且对象适配器是可以随时更换的,我们在RecycleView中也是可以更换Adapter的具体实现类从而根据不同的类型加载不同的Adapter实现不同的界面!
  • 查看setAdapter的创建
 public abstract static class Adapter<VH extends ViewHolder> {
 //静态类用于Adapter数据变化的Observable,每一个类都有一个自己的Observable对象
        private final AdapterDataObservable mObservable = new AdapterDataObservable();
        
//setAdapter
  public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        requestLayout();
    }
    
//setAdapterInternal:使用心得adapter替换掉老的adapter,并且触发监听mObserver
   private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver); // 注销老的adapter观察者
            mAdapter.onDetachedFromRecyclerView(this); //解绑recycleview
        }
        ....
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            adapter.registerAdapterDataObserver(mObserver); //注册观察者
            adapter.onAttachedToRecyclerView(this);
        }
        ...
    }
RecycleView的缓存
  • 之所以使用RecycleView替换掉ListView是因为其优秀的缓存机制
ListView缓存
  • ListView只有两级缓存,其是RecycleBin缓存机制
    1. mActiveViews :一级是缓存屏幕上可见的view
    2. mScrapViews :二级缓存ListView中所有废弃的,这是一个ArrayList<View>[] 数组,每一种type对应一个自己的arrayList缓存
  • 区别: RecycleView缓存的是ViewHolder,而list只是缓存View,需要自己构建一个ViewHolder对象
四级缓存
  1. 一级缓存: mAttachedScrap 和mChangedScrap :优先级最高的缓存,RecycleView在获取viewHolder时,优先会从这两个缓存中找,其中前者存储的是当前还在屏幕中的viewHolder,后者存储的是数据被更新的viewHolder,比如调用了adapter.notifyItemChanged()方法更新条目
    • mAttachedScrap :他表示存储在当前还在屏幕中的ViewHolder,实际上是从屏幕上分离出来的ViewHolder,但是又即将添加到屏幕上去的ViewHolder,比如上下滑动,滑出一个新的Item,此时会重新调用LayoutManager的onLayoutChildren方法,从而会将屏幕上所有的ViewHolder先scrap掉(废弃掉),添加到mAttachedScrap中去,然后在重新布局每一个ItemView时,会优先从mAttachedScrap中获取,这样效率就会非常的高,这个过程不会重新onBindViewHolder
  2. 二级缓存: mCachedViews : 默认大小为2,通常用来存储预取的viewHolder,同时在回收ViewHolder时,也会可能存储一部分的viewHolder,这部分的viewHolder通常同一级缓存差不太多,下面分析
    • 默认是2,不过通常是3,3由默认的大小2+ 预取的个数1,所以在RecycleView在首次加载时,mCacheView的size为3(LinerLayoutManager布局为例)
  3. 三级缓存:ViewCachedExtension :自定义缓存,通常用不到
  4. RecycleViewPool :根据viewType来缓存ViewHolder,每个viewType的数组大小为5,可以动态改变
ViewHolder的几个状态值
  • 通过阅读RecycleView的源码时,到处看到ViewHolder的isInvalid ,isRemoved ,isBound, isTmpDetached, isScrap , isUpdated 这几个方法,其实是个状态机
  • isInvalid : flag_invalid : 表示当前ViewHolder是否已经失效,通常由三种情况会出现这种状况
    1. 调用了adapter.notifyDataSetChanged()方法
    2. 手动调用RecycleView.invalidateItemDecorations()方法
    3. 调用RecycleView的setAdapter方法或swapAdapter()方法
  • isRemoved : Flag_removed : 表示当前ViewHolder是否被移除, 通常来说,数据源被移除了部分数据,然后调用了adapter.notifyItemRemoved()
    • 即data中移除了一条数据以后调用
    public void removeItem(int position){
       data.remove(posiiton);
       notifyItemRemoved(position);
       notifyItemRangeChanged(position, data.size() - position); //不加这个position位置会错乱的.onBindViewHolder也不会调用
    }
    
  • isBound: flag_bound : 表示当前ViewHolder是否已经调用了onBindViewHolder
  • isTmpdetached : flag_tmp_detached : 表示当前的ItemView是否从RecycleView(父View)detach掉,通常由两种情况:
    1. 手动调用了RecycleView.detachView相关方法
     public void detachViewFromParent(int offset) {
                final View view = getChildAt(offset);
                if (view != null) {
                    final ViewHolder vh = getChildViewHolderInt(view);
                    if (vh != null) {
                        if (vh.isTmpDetached() && !vh.shouldIgnore()) {
                            throw new IllegalArgumentException("called detach on an already"
                                    + " detached child " + vh + exceptionLabel());
                        }
                        if (DEBUG) {
                            Log.d(TAG, "tmpDetach " + vh);
                        }
                        vh.addFlags(ViewHolder.FLAG_TMP_DETACHED); //detachView(View view)调用的回调,设置Flag
                    }
                }
                RecyclerView.this.detachViewFromParent(offset);
            }
    
    1. 从mHideViews中获取viewHolder,会先detach掉这个ViewHolder关联的ItemView
  • isScrap : 无flag状态,用mScrapContainer是否为null来判断:表示是否在MAttachedScrap或mChangeScrap数组里面,进而表示当前ViewHolder是否被废弃掉
  • isUpdated: flag_update : 表示当前ViewHolder是否已经更新,通常由三种情况:
    1. isInvalid 方法存在的三种情况
    2. 调用了adapter.onBindviewHolder方法
    3. 调用了adapter.notifyItemChanged方法
  • 再上方我们有一个mHiddenViews,并不算入四级缓存中,因为其只在动画期间才会有元素,当动画结束后就清空了,自然不能算到四级缓存中
  1. 上方一级缓存中有两个,即如果调用了adapter.notifyItemChanged(),会重新回调到LayoutManager的onLayoutChildren()中,那么他两又有啥区别呢? 我们不妨看一段代码逻辑:
//根据viewHolder的flag状态决定是放入mAttachedScrap还是mChangedScrap中去
void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) //同时标记remove和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);
            }
        }
        
     boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
        return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
                viewHolder.getUnmodifiedPayloads());
    }
  • 则mAttachedScrap里放着两种状态的ViewHolder:
    1. 被同时标记为remove和invalid
    2. 完全没有改变的viewHolder
    3. 这个跟RecyclerView的ItemAnimator有关,如果ItemAnimator为空或者ItemAnimator的canReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。那正常情况下,什么情况返回为true呢?从SimpleItemAnimator的源码可以看出来,当ViewHolder的isInvalid方法返回为true时,会放入到 mAttachedScrap里面。也就是说,如果ViewHolder失效了,也会放到mAttachedScrap里面。
  • MchangedScrap 里面放的是isUpdated的返回true的情况,即调用了adapter.notifyItemChanged()并且RecycleView的ItemAnimator不为空时加入
  • 同时看一下mAttachedScrap / mChangedScrap 的scrap数组和mHiddenViews的区别
    1. mHiddenViews只存放动画的ViewHolder,动画结束自然就清空了,用于动画期间进行复用的可能性
    2. scrap数组和mHiddenViews两者不冲突,可能都存在相同的Viewholder,但是这并不影响,动画结束时会从mHiddenViews中移除的
RecycleView的复用
  • 通过上面的分析只是说明了四级缓存的性质,但是并没有说明他的复用原理;我们从LayoutState.next()方法开始,我们知道RecycleView是交给LayoutManager在布局itemView时,需要获取一个ViewHolder对象,通过这个方法来获取的,自然复用逻辑也是在这里的
    • next() 调用规则: RecycleView中的重写View的scrollby ->scrollByInternal() -> mLayout.scrollHorizontallyBy()/mLayout.scrollVerticallyBy()-> LayoutManager中的->scrollBy() ->fill() ->next() 滑动时调用
//我们以 LinearLayoutManager.next为例
View next(Recycler recycler) {
            if (this.mScrapList != null) {
                return this.nextViewFromScrapList();
            } else {
                View view = recycler.getViewForPosition(this.mCurrentPosition);
                this.mCurrentPosition += this.mItemDirection;
                return view;
            }
        }
  • 可以看到LinerLayoutManager中通过next获取recycleView的下一个viewHolder对象,调用recycleView.getViewForPosition 最终调用的是 tryGetViewHolderForPositionByDeadline() ,这个才是真正复用的核心
  1. 通过position方式获取对应的viewHolder:优先级比较高,因为每个viewHolder还未被该变,都是由于某一个itemView对应的viewHolder被更新导致的,所以在屏幕上其他viewHolder可以快速对应原来的ItemView
if (position >= 0 && position < RecyclerView.this.mState.getItemCount()) { //position合法
                boolean fromScrapOrHiddenOrCache = false;
                RecyclerView.ViewHolder holder = null;
                if (RecyclerView.this.mState.isPreLayout()) {
                    holder = this.getChangedScrapViewForPosition(position); //从mChangedScrap中获取
                    fromScrapOrHiddenOrCache = holder != null;
                }

                if (holder == null) {
                    holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                    if (holder != null) {
                        if (!this.validateViewHolderForOffsetPosition(holder)) { //检查当前position是否位置匹配,如果不匹配说明滑出去或者失效了
                            if (!dryRun) {
                                holder.addFlags(4);
                                if (holder.isScrap()) {
                                    RecyclerView.this.removeDetachedView(holder.itemView, false);
                                    holder.unScrap();
                                } else if (holder.wasReturnedFromScrap()) {
                                    holder.clearReturnedFromScrapFlag();
                                }
                                //两种情况,如果是滑出屏幕说明可服用,加入到mCacheViews中直接复用,如果失效则说明数据不可用,但视图可用,直接回收ViewHolder加入到viewPool中即可,每次从他里面加载都会重新走onBindViewHolder
                                this.recycleViewHolderInternal(holder);
                            }

                            holder = null;
                        } else {
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
                }


  • 以上源码分为两步:
    1. 从mChangedScrap 中获取ViewHolder,这里面存放的是更新的ViewHolder
    2. 分别从mAttachedScrap,mHiddenViews,mCachedViews获取ViewHolder
  1. 首先看从mChangedScrap中获取:如果当前是预布局阶段,则从mChangedScrap中获取ViewHolder
    • 预布局:preLayout,即当前RecycleView处于dispatchLayoutStep1阶段时,称为预布局
    • dispatchLayoutStep2阶段被称为真正的布局阶段
    • dispatchLayoutStep3称为postLayout阶段
    • 同时要想真正开启预布局,必须要有itemAnimator,并且每个recycleView对应的LayoutManager必须开启预处理动画
  • 只有当ItemAnimator不为空,被changed的ViewHolder会放在mChangedScrap数组里面。因为chang动画前后相同位置上的ViewHolder是不同的,所以当预布局时,从mChangedScrap缓存里面去,而正式布局时,不会从mChangedScrap缓存里面去,这就保证了动画前后相同位置上是不同的ViewHolder
if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
  • 第二步操作,从其他缓存中获取
 if (holder == null) {
                    holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                    if (holder != null) { 
                        if (!this.validateViewHolderForOffsetPosition(holder)) {
                            if (!dryRun) {
                                holder.addFlags(4);
                                if (holder.isScrap()) {
                                    RecyclerView.this.removeDetachedView(holder.itemView, false);
                                    holder.unScrap();
                                } else if (holder.wasReturnedFromScrap()) {
                                    holder.clearReturnedFromScrapFlag();
                                }

                                this.recycleViewHolderInternal(holder);
                            }

                            holder = null;
                        } else {
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
                }
  • 分别从mAttachedScrap. mHiddenViews,mCachedViews获取ViewHolder,但是还得判断ViewHolder是否有效,如果无效需要做一些清理操作从一级缓存中remove,然后重新放入到mCacheViews或者RecycleViewPool缓存里面
    • 因此从一级,二级缓存中取值,其ViewHolder的原本数据信息都在,直接添加到RecycleView中显示,不需要重新走onBindViewHolder()方法赋值了,只有原来的子项可以重新复用,新建的子项无法从该两级缓存中取值的
    • 而mREcycleViewPool:源码中只是缓存ViewHolder并重置数据,相当于一个新建的ViewHolder,只是不走onCreateView()方法,使用时需要重新调用onBindViewHolder()来绑定数据
  • 首先看从各种缓存中取值

final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList();
final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList(); //缓存大小有默认的2加上layoutManager的设置决定

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

    for(int i = this.mCachedViews.size() - 1; i >= 0 && this.mCachedViews.size() > this.mViewCacheMax; --i) {
        this.recycleCachedViewAt(i);
    }

}
//如果CachedViews中超过了2,则将其移到recycleViewPool缓存中去
void recycleCachedViewAt(int cachedViewIndex) {
    RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
    this.addViewHolderToRecycledViewPool(viewHolder, true);
    this.mCachedViews.remove(cachedViewIndex);
}
        


RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            int scrapCount = this.mAttachedScrap.size();
            //从mAttachedScrap中获取,如果可用则返回
            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;
                }
            }

            if (!dryRun) { //false默认值
                //从mHiddrenViews中获取
                View view = RecyclerView.this.mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    vh = RecyclerView.getChildViewHolderInt(view);
                    RecyclerView.this.mChildHelper.unhide(view);
                    int layoutIndex = RecyclerView.this.mChildHelper.indexOfChild(view);
                    if (layoutIndex == -1) {
                        throw new IllegalStateException("layout index should not be -1 after unhiding a view:" + vh + RecyclerView.this.exceptionLabel());
                    }

                    RecyclerView.this.mChildHelper.detachViewFromParent(layoutIndex);
                    this.scrapView(view);
                    vh.addFlags(8224);
                    return vh;
                }
            }
            //从mCacheViews中获取
            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;
                }
            }

            return null;
        }
  • 上面分析的是通过position来获取ViewHolder,我们验证了position合法,则viewType肯定是正确对应的,但是通过viewType获取viewHolder时,position可能失效了,通过viewType可分为三步:
    1. 如果adapter的hasStableIds = true,优先通过viewType和id两个条件寻找
    2. 如果未找到,如果adapter.hasStablleIds = false,首先viewCacheExtension里面找,如果还未找到,最后会在RecycleViewPool里面获取ViewHolder
    3. 如果以上都没有找到合适的ViewHolder,最后会调用adapter的onCreateViewHolder创建一个新的ViewHolder对象
  • 首先上面我们只是分析了前两种缓存操作,第三种个人设置的不分析,则第四种 从 RecycleViewPool中获取缓存如何做的呢?首先搞清楚他是啥?
public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5; 
        SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray(); //使用SparseArray键是int型的 viewType 的值,value是ScrapData对象
        private int mAttachCount = 0;

//在分析ScrapData是RecycledViewPool静态内部类
 static class ScrapData {
            final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList(); //对于每个viewType的不同保存在不同的ArrayList集合中,相同的viewType保存在同一个集合中,且一种viewType类型最多保存5条ViewHolder
            int mMaxScrap = 5;
            long mCreateRunningAverageNs = 0L;
            long mBindRunningAverageNs = 0L;

            ScrapData() {
            }
        }
  • 则其复用代码逻辑在:
int offsetPosition;
int type;
if (holder == null) {
offsetPosition = RecyclerView.this.mAdapterHelper.findPositionOffset(position); //获取position对应的偏移量
if (offsetPosition < 0 || offsetPosition >= RecyclerView.this.mAdapter.getItemCount()) { 
//如果当前偏移量没有出现在屏幕上,即更改了数据但是却没有刷新RecycleView时,会报错的,因此我们setData以后一定要notify通知更新UI操作
    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item position " + position + "(offset:" + offsetPosition + ")." + "state:" + RecyclerView.this.mState.getItemCount() + RecyclerView.this.exceptionLabel());
}

type = RecyclerView.this.mAdapter.getItemViewType(offsetPosition); //获取当前位置对应的type
if (RecyclerView.this.mAdapter.hasStableIds()) { //如果从新hasStableIds返回true,即使通过position获取ViewHolder失败,还会尝试通过ViewType去获取ViewHolder,优先在Scrap和cached缓存中寻找
    //根据itemId ,type判断当前位置是否存在缓存中,上面只是根据position来判断
    holder = this.getScrapOrCachedViewForId(RecyclerView.this.mAdapter.getItemId(offsetPosition), type, dryRun);
    if (holder != null) {
        holder.mPosition = offsetPosition;
        fromScrapOrHiddenOrCache = true;
    }
}
//用户自定义缓存策略,没啥用
if (holder == null && this.mViewCacheExtension != null) {
    View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
    if (view != null) {
        holder = RecyclerView.this.getChildViewHolder(view);
        if (holder == null) {
            throw new IllegalArgumentException("getViewForPositionAndType returned a view which does not have a ViewHolder" + RecyclerView.this.exceptionLabel());
        }

        if (holder.shouldIgnore()) {
            throw new IllegalArgumentException("getViewForPositionAndType returned a view that is ignored. You must call stopIgnoring before returning this view." + RecyclerView.this.exceptionLabel());
        }
    }
}

if (holder == null) {
//根据type从recycleViewPool中获取holder缓存对象
    holder = this.getRecycledViewPool().getRecycledView(type); //从五个里面拿数组的最后一个,拿到从缓存数组中移除一个数据
    if (holder != null) {
        holder.resetInternal(); //只是拿到布局文件,重置里面的信息,以便后续设置值使用
        if (RecyclerView.FORCE_INVALIDATE_DISPLAY_LIST) {
            this.invalidateDisplayListInt(holder);
        }
    }
}

 if (holder == null) { //如果上方缓存中都没有值了,则调用onCreateViewHolder创建一个新的ViewHolder
    long start = RecyclerView.this.getNanoTime();
    if (deadlineNs != 9223372036854775807L && !this.mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
        return null;
    }
    
    holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type); //根据type创建一个新的ViewHolder
    if (RecyclerView.ALLOW_THREAD_GAP_WORK) {
        RecyclerView innerView = RecyclerView.findNestedRecyclerView(holder.itemView);
        if (innerView != null) {
            holder.mNestedRecyclerView = new WeakReference(innerView);
        }
    }

    long end = RecyclerView.this.getNanoTime();
    this.mRecyclerPool.factorInCreateTime(type, end - start);
}
  • 综上即为RecycleView的复用机制,下面我们看一下回收机制
RecycleView回收机制
  • 所有的框架一单存在复用,当然首次创建以后肯定会有回收,否则怎么复用呢?同时理解这两个过程,对于我们使用RecycleView及其原理分析会更加清晰明白!
  • 首先复习一下复用过程: 回收当然也是一一对应的
    1. 从scrap数组中查找是否可用,可用直接复用
    2. 从mCachedViews数组中查找是否可用
    3. 从mHiddenViews数组中查找,这是一个关于动画的数组
    4. RecycleViewPool根据type查找是否可用,如果存在则返回布局
    5. 都没有找到就从adapter.onCreateViewHolder创建一个新的ViewHolder对象返回
  • 则对应的回收过程为:
    1. scrap数组
    2. mCachedViews数组
    3. mHiddenViews数组
    4. RecycleViewPool数组
  1. scrap数组回收
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);
    }
}
  • 在文章的最开始已经分析过add添加到两个scrap数组中的区别,这里只看scrapView都是那些地方调用的
    1. getScrapOrHiddenOrCachedHolderForPosition() : 如果从mHiddenViews中获取一个ViewHolder时,会先将这个ViewHolder从mHiddenViews中移除,然后调用scrapView方法将这个ViewHolder放入到scrap数组中
    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            // This View is good to be used. We just need to unhide, detach and move to the
            // scrap list.
            final ViewHolder vh = getChildViewHolderInt(view);
            mChildHelper.unhide(view);
            int layoutIndex = mChildHelper.indexOfChild(view);
            if (layoutIndex == RecyclerView.NO_POSITION) {
                throw new IllegalStateException("layout index should not be -1 after "
                        + "unhiding a view:" + vh + exceptionLabel());
            }
            mChildHelper.detachViewFromParent(layoutIndex);
            scrapView(view);
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            return vh;
        }
    }
    
    1. 在LayoutManager中的scrapOrRecycleView()方法中调用,有两种情况
      1. 手动调用了LayoutManager相关方法
      2. RecycleView进行了一次布局(调用了requestLayout方法)
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            detachViewAt(index);
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }
    
  1. mCacheViews回收
  • mCacheViews回收路径比较多,大致分为三类:
    1. 重新布局回收的,这种情况主要是调用adapter.notifyDataSetChange(),并且hasStableIds返回了false,这里就可以看出为何调用notifyDataSetChange()方法效率如此低效,同时也知道了为何重写hasStableIds方法可以提高效率,因为notifyDataSetChange使得RecycleView将回收的ViewHolder放在了二级 缓存中,效率自然比较低,重写hasStableIds返回true,则只刷新变化的,没变的就不会刷新了
    //notifyDataSetChange()调用Observable的轮询onChanged
     public void onChanged() {
            RecyclerView.this.assertNotInLayoutOrScroll((String)null);
            RecyclerView.this.mState.mStructureChanged = true;
            RecyclerView.this.processDataSetCompletelyChanged(true);
            if (!RecyclerView.this.mAdapterHelper.hasPendingUpdates()) {
                RecyclerView.this.requestLayout(); //请求重新布局
            }
    
        }
    //调用这个清除CacheViews
     if (RecyclerView.this.mAdapter == null || !RecyclerView.this.mAdapter.hasStableIds()) {
                this.recycleAndClearCachedViews();
            }
    
    1. 在复用时,从一级缓存里面获取到ViewHolder,但此时这个ViewHolder已经不符合一级缓存的特点了(如position失效,跟ViewType对不齐),就会从一级缓存里面移除这个ViewHolder添加到mCacheViews里面
    2. 当调用removeAnimatingView方法时,如果当前ViewHolder被标记为remove,会调用recycleViewHolderInternal()回收对应的ViewHolder调用removeAnimatingView方法的时机表示当前ItemAnimator已经做完了
  1. mHiddenViews数组
    • 一个ViewHolder回收到mHiddenView数组里面的条件比较简单,如果当前操作支持动画,就会调用到RecyclerView的addAnimatingView方法,在这个方法里面会将做动画的那个View添加到mHiddenView数组里面去。通常就是动画期间可以会进行复用,因为mHiddenViews只在动画期间才会有元素。
  2. RecycleViewPool
    • recycleViewPool同mCacheView相同,都是通过recycleViewHolderInternal方法来回收的,只是当不满足mCacheView条件是,即数量大于2时,才会放入到RecycleViewPool中
  3. 为何说重写hasStableIds方法返回true会提高效率呢?
    • 复用方面:首先看next()调用代码
    if (mAdapter.hasStableIds()) { //如果重写了,就尝试根据viewType从一级,二级缓存中去取
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                type, dryRun);
        if (holder != null) {
            // update position
            holder.mPosition = offsetPosition;
            fromScrapOrHiddenOrCache = true;
        }
    }
    
    • 回收方面:
     private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) { //如果为true,走下面
            removeViewAt(index);
            recycler.recycleViewHolderInternal(viewHolder); //全部回收到了cachedViews和RecycleViewPool中了
        } else {
            detachViewAt(index);
            recycler.scrapView(view); //回收到scrap数组中,一级缓存中
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }
    
数据加载
  • 对于上方无论是从缓存中去数据还是新建onCreateView中获取一个ViewHolder,当然需要加载数据了,通过上面分析我们知道,缓存ViewHolder在一级二级缓存中都带有数据的,可以直接复用,但是对于四级或者新建的都相当于重建一个ViewHolder,其中是没有数据的,我们需要绑定走OnBindViewHolder
    • 依然是recycleView中的tryGetViewHolderForPositionByDeadline方法中
//bound状态的解释: 该ViewHolder已绑定到一个位置; mPosition,mItemId和mItemViewType均有效。
if (mState.isPreLayout() && holder.isBound()) { //如果当前是bound,就不用在tryBindViewHolderByDeadline-> onBindViewHolder
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    if (DEBUG && holder.isRemoved()) {
        throw new IllegalStateException("Removed holder should be bound and it should"
                + " come here only in pre-layout. Holder: " + holder
                + exceptionLabel());
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    //该方法会调用重写的onBindViewHolder
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

//RecycleViewPool中去ViewHolder调用 resetInternal()重置ViewHolder
void resetInternal() {
    mFlags = 0; 
    ...
}

//而上方的判断hold.isBound() 
boolean isBound() {
    return (mFlags & FLAG_BOUND) != 0; //0 & 任何数 = 0 返回false
}
//因此从viewPool池中和oncreateView重建的都会重新绑定数据,而其他缓存则不用,可以直接复用
  • 至此,recycleView布局和加载数据都讲解完成了,布局的加载是委托给LayoutManager实现,而数据是托付给adapter的onBindViewHolder,recycleView只负责对viewHolder的缓存和交互,通过观察者模式对adapter设置Observable,一旦有数据更新则通知view重绘requestLayout
常见问题
  1. RecycleView 的notifyDataSetChanged导致图片闪烁的原因
    • 仅使用notifyDataSetChanged,在重新布局时,会先remove View,然后根据相关情形,缓存到mCachedViews ,ViewCahceExtension(个人实现),RecycleViewPool中在调用LayoutManager.next()方法时,去recycleView中取值复用,如果在pool中都无法取到,则会直接调用oncreateViewHolder重新inflate创建View,导致闪烁
    • 使用notifyDataSetChanged() + hasStableIds() true ,+ 复写getItemId() :防止条目重复抖动 或者刷新单个条目,会缓存在mAttachedScrap,因此不会重新createViewHolder
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335