美文网首页
RecyclerView显示及缓存机制

RecyclerView显示及缓存机制

作者: 雷涛赛文 | 来源:发表于2021-02-08 19:37 被阅读0次

    一.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<AdapterDataObserver> {
        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<View> 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<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        .........
        .........
    }
    

          RecycledViewPool内部维护一个SparseArray<ScrapData>mScrap,ScrapData内部维护一个ArrayList<ViewHolder>mScrapHeap,最大值为5;

    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;
        }
        ......
        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<ViewHolder> 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显示及缓存机制

          本文链接:https://www.haomeiwen.com/subject/yusfxltx.html