RecyclerView显示及缓存机制

一.RecyclerView显示

      在使用RecyclerView时,需要结合Adapter来使用,一个RecyclerView需要一个Adapter,一个Adapter中对应着指定数量及指定type的item,即ViewHolder,那ViewHolder时如何创建及如何显示的?
      在平时使用中,通过调用RecycerView.setAdapter()来显示内容,一起看一下执行流程:

a.RecyclerView
public void setAdapter(@Nullable Adapter adapter) {
    ......
    setAdapterInternal(adapter, false, true);
    requestLayout();
}

      先执行了setAdapterInternal(adapter, false, true),看一下这个方法主要做了什么:

private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
    if (mAdapter != null) {
        mAdapter.unregisterAdapterDataObserver(mObserver);
        mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        removeAndRecycleViews();
    }
    mAdapterHelper.reset();
    final Adapter oldAdapter = mAdapter;
    mAdapter = adapter;
    if (adapter != null) {
        adapter.registerAdapterDataObserver(mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
         Layout.onAdapterChanged(oldAdapter, mAdapter);
    }
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
}

private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();

      可以看到在setAdapterInternal()里面先调用了unRegister和unDetached操作,接着执行了register和detached操作,register里面会执行adapter里面对应的register方法:

private final AdapterDataObservable mObservable = new AdapterDataObservable();
public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
    mObservable.registerObserver(observer);
}

public final void notifyDataSetChanged() {
    mObservable.notifyChanged();
}

public final void notifyItemChanged(int position) {
    mObservable.notifyItemRangeChanged(position, 1);
}

      在registerAdapterDataObserver()里面执行的是AdapterDataObservable的registerObserver()方法,当我们在本地实现的adapter内部执行notifyDataSetChanged()及notifyItemChanged()等操作时,先会调用到AdapterDataObservable内部对应的方法:

static class AdapterDataObservable extends Observable {
    public void notifyChanged() {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }

    public void notifyItemRangeInserted(int positionStart, int itemCount) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
        }
    }
    ........
    ........
}

      在AdapterDataObservable里面最终会调用到RecyclerViewDataObserver里面对应的方法,执行view刷新的操作。

private class RecyclerViewDataObserver extends AdapterDataObserver {

    @Override
    public void onChanged() {
        ......
    }

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        ......
    }
    ......
    ......
    void triggerUpdateProcessor() {
        ......
    }
}

      接着上面setAdapter()内部逻辑讲,然后调用了requestLayout(),调用该方法后,会执行onLayout():

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    dispatchLayout();
}

      在dispatchLayout()内部会调用到dispatchLayoutStep2():

private void dispatchLayoutStep2() {
    ......
    mState.mItemCount = mAdapter.getItemCount();
    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);
    ....
}

      在该方法内部,先通过adapter的getItemCount()来获取到item的数量,赋值给State()的mItemCount,后续在显示item时会使用到;然后调用mLayout的onLayoutChildren()方法,mLayout是在初始化RecyclerView时通过setLayoutManager()传入的LayoutManager,此处分析LinerLayoutManager。

b.LinerLayoutManager

      LayoutManager是用来决定RecyclerView内部item显示的布局,LinerLayoutManager线性布局,GridLayoutManager是网格布局,本文主要分析LinerLayoutManager:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    .......
    ......
    fill(recycler, mLayoutState, state, false);
    ......
    ......
}

      在onLayoutChildren()内调用了fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
    ......
    ......
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            .......
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            .......
    }
}

      在fill()内部通过while()来不断的执行layoutChunk(),先看一下while()循环的条件之一:layoutState.hasMore(state),LayoutState是LinerLayoutManager内部的一个静态类,State是RecyclerView内部的一个类,注意别搞混了;

boolean hasMore(RecyclerView.State state) {
    return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}

      通过以上看到,用到了getItemCount(),通过position跟item的数量来判断是否需要继续循环执行layoutChunk():

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        ......
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

      在layoutChunk()内部会通过layoutState.next(recycler)来获取当前position对应的view,然后通过addView(),最后执行layout来进行放置,这里我们不去关心是如何放置的,只关心是如何获取的item对应的view,主要看一下layoutState.next(recycler):

View next(RecyclerView.Recycler recycler) {
    ........
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

      next()内部最终是通过recycler的getViewForPosition来获取view。

c.RecyclerView.Recycler
public View getViewForPosition(int position) {
     return getViewForPosition(position, false);
}

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

      从getViewForPosition()内部逻辑来看,先调用tryGetViewHolderForPositionByDeadline()来获取到ViewHolder,然后获取到ViewHolder里面的itemView,先看一下tryGetViewHolderForPositionByDeadline():

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
        boolean fromScrapOrHiddenOrCache = false;
        ViewHolder holder = null;
        // 0) If there is a changed scrap, try to find from there
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) Find by position from scrap/hidden list/cache
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            .......
        }
        if (holder == null) {
            //获取到position对应的type,需要本地实现,不实现的话,默认返回0,即一个type
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2) Find from scrap/cache via stable ids, if exists
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
                ......
            }
            if (holder == null && mViewCacheExtension != null) {
                final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
                ......
            }
            if (holder == null) { // fallback to pool
                holder = getRecycledViewPool().getRecycledView(type);
                .....
            }
            if (holder == null) {
                .......
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                .......
            }
        }

            ......
            ......
               
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            ........
            return holder;
}

      从上面可以看到,去获取holder时,会经历四步判断,即对应着四级缓存,下一节会详细介绍,如果从四级缓存中都没有获取到时,会执行createViewHolder()来进行创建:
      注意一下:viewType是从本地实现的getItemType(postion)来获取的,如果没有实现,则默认为0,即都是统一类型;如果实现了,用途有二:1.本地根据对应的type来创建不同的view;2.RecyclerViewPool根据不同的type缓存对应数量为5的viewHolder。

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
    try {
        final VH holder = onCreateViewHolder(parent, viewType);
        holder.mItemViewType = viewType;
        return holder;
     } finally {
        TraceCompat.endSection();
     }
}

      最终会回调本地adapter的onCreateViewHolder()来创建对应的viewHolder,看一下ViewHolder这个类:

public abstract static class ViewHolder {
    ......
    ......
    public ViewHolder(@NonNull View itemView) {
        if (itemView == null) {
            throw new IllegalArgumentException("itemView may not be null");
        }
        this.itemView = itemView;
    }
    ......
    ......
}

      通过以上可以看到,itemView是在构造方法中作为参数传入的,在创建本地ViewHolder时,我们会先通过LayoutInflater来inflate()对应position及itemtype的view,然后执行new ViewHolder(view)返回,此处itemView就是我们创建的view。
      接着上面分析,接下来会调用tryBindViewHolderByDeadline(),先设置flag,最终会回调本地adapter的onBindViewHolder():

private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
                int position, long deadlineNs) {
    ......
    final int viewType = holder.getItemViewType();
    mAdapter.bindViewHolder(holder, offsetPosition);
    ......
    return true;
}

public final void bindViewHolder(@NonNull VH holder, int position) {
    holder.mPosition = position;
    holder.setFlags(ViewHolder.FLAG_BOUND,
                    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
            
    onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
}

      以上就是在初始化RecyclerView,然后调用setAdapter()后的整个执行流程,用流程图总结一下:


recycleview显示流程.png

二.四级缓存机制

      RecyclerView缓存机制分为四级,分别为:Scrap(mAttachedScrap)、Cache(mCachedViews)、ViewCacheExtension(mViewCacheExtension)、RecycledViewPool(mRecyclerPool)。
      Scrap
      就是屏幕内的缓存数据,可以直接拿来复用。
      Cache
      刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO原则,把先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache里面的数据是干净的,即携带了原来的ViewHolder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。
      举个例子:当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时RecyclerView就从Cache里面找position=0的数据,如果找到了就直接拿来复用。
      ViewCacheExtension
      google留给开发者自己来自定义缓存的,ViewCacheExtension适用场景:ViewHolder位置固定、内容固定、数量有限时使用。
      使用方式如下:

//viewType类型为TYPE_SPECIAL时,设置四级缓存池RecyclerPool不存储对应类型的数据 因为需要开发者自行缓存
recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0);
//设置ViewCacheExtension缓存
recyclerView.setViewCacheExtension(new MyViewCacheExtension());
//实现自定义缓存ViewCacheExtension
class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
    @Nullable
    @Override
    public View getViewForPositionAndType(@NonNull RecyclerView.Recycler recycler, int position, int viewType) {
        //如果viewType为TYPE_SPECIAL,使用自己缓存的View去构建ViewHolder
        // 否则返回null,会使用系统RecyclerPool缓存或者从新通过onCreateViewHolder构建View及ViewHolder
        return viewType == DemoAdapter.TYPE_SPECIAL ? adapter.caches.get(position) : null;
    }
}

Adapter.java
public SparseArray caches = new SparseArray<>();//开发者自行维护的缓存
Adapter#onBindViewHolder中根据position设置的缓存:caches.put(position, sHolder.itemView);

三.源码分析

      主要分析一下RecycledViewPool的流程:
      RecycledViewPool
      Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool存储时是根据itemType来存储的,每个itemType对应的缓存数量是5个;看一下主要实现逻辑:

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        final ArrayList mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray mScrap = new SparseArray<>();
    .........
    .........
}

      RecycledViewPool内部维护一个SparseArraymScrap,ScrapData内部维护一个ArrayListmScrapHeap,最大值为5;

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    ......
    scrap.resetInternal();
    scrapHeap.add(scrap);
}

private ScrapData getScrapDataForType(int viewType) {
     ScrapData scrapData = mScrap.get(viewType);
     if (scrapData == null) {
         scrapData = new ScrapData();
         mScrap.put(viewType, scrapData);
     }
     return scrapData;
}

      在执行putRecycledView(vh)时,先判断vh的type,根据type去获取ScrapData,如果获取到就返回,否则就创建一个新的ScrapData,然后加入到mScrap内,key为对应的type,再获取到ScrapData内部的mScrapHeap,判断size(),如果已经等于5,就不再存储了,否则先执行resetInternal()来清空数据,然后再加入到mScrapHeap里面;

public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}

      在执行getRecycledView(type)时,先根据type从mScrap内获取对应的ScrapData,如果不为空,则获取到ScrapData内部的mScrapHeap,再从heap中获取到ViewHolder,然后将对应位置的ViewHolder删除,后续put时再进行添加;
      直观一点如下:

image.png

      RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder在存入RecycledViewPool之前ViewHolder的数据会被全部重置(resetInternal()),相当于一个新的ViewHolder,而且Cache是根据position来获取ViewHolder,而RecycledViewPool是根据itemType获取的,如果没有重写getItemType()方法,itemType就是默认的。因为RecycledViewPool缓存的ViewHolder是全新的,所以取出来的时候需要走onBindViewHolder()方法。

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
         holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
         ......
    }
    if (holder == null) {
        .........
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
            ......
        }
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
            .......
        }
        if (holder == null) { // fallback to pool
            holder = getRecycledViewPool().getRecycledView(type);
            ........
         }
         if (holder == null) {
             //create view holder
             holder = mAdapter.createViewHolder(RecyclerView.this, type);
             .......
         }
        ......
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ......
    return holder;
}

      直观一点如下:


RecyclerView缓存机制.png

四.ReceyclerView与ListView的区别

      1)ListView布局单一,RecycleView可以根据LayoutManger有横向,瀑布和表格布局;
      2)自定义适配器中,ListView的适配器继承ArrayAdapter;RecycleView的适配器继承RecyclerAdapter,并将范类指定为子项对象类ViewHolder(内部类)。
      3)ListView优化需要自定义ViewHolder和判断convertView是否为null(不用每次都创建convertView和调用findViewById()findViewById()这个方法是比较耗性能的操作,因为这个方法要找到指定的布局文件,进行不断地解析每个节点:从最顶端的节点进行一层一层的解析查询,找到后在一层一层的返回,如果在左边没找到,就会接着解析右边,并进行相应的查询,直到找到位置。特点:xml文件被解析的时候,只要被创建出来了,其孩子的id就不会改变了。根据这个特点,可以将孩子id存入到指定的集合中,每次就可以直接取出集合中对应的元素就可以了)。 而RecyclerView是强制存在规定好的ViewHolder。
      4)绑定事件的方式不同,ListView是在主方法中ListView对象的setOnItemClickListener方法;RecyclerView则是在子项具体的View中去注册事件。
      5) 缓存方式有区别:RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;ListView缓存View,同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。
      6) 刷新方式:RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView;ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是"一锅端",将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

你可能感兴趣的:(RecyclerView显示及缓存机制)