美文网首页Android-recyclerview
温柔撕破 - RecycleView

温柔撕破 - RecycleView

作者: JackDaddy | 来源:发表于2020-08-23 16:50 被阅读0次

    前言

    相信每一个做 Android 开发的人对于 RecycleView 都不陌生,一个 APP 中用到 RecycleView 的地方肯定不少于 50%,因此对 RecycleView 深入理解还是很有必要的。因此这篇文章主要从以下几个方面来介绍 RecycleView

    • RecycleView 的使用。
    • RecycleView 是如何缓存复用的?
    • RecycleView 的优化。

    RecycleView 的使用

    public class ViewAdapter extends RecyclerView.Adapter<ViewAdapter.ChildItemHolder> {
    
        private List<Entity> mList = new ArrayList<>();
    
        public ViewAdapter(List<Entity> mList) {
            this.mList = mList;
        }
    
        @Override
        public ChildItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = View.inflate(context, R.layout.view_item_layout, null);
            return new ChildItemHolder(view);
        }
    
        @Override
        public void onBindViewHolder( ChildItemHolder holder, int position) {
            Entity entity= mList.get(position);
            holder.tvName.setText(entity.id);
        }
    
        @Override
        public int getItemCount() {
            return mList.size();
        }
    
        class ChildItemHolder extends RecyclerView.ViewHolder {
    
            TextView tvName;
    
            public ChildItemHolder(View itemView) {
                super(itemView);
                tvName = itemView.findViewById(R.id.tv_name);
            }
        }
    }
    

    以上就是 RecycleView 的简单使用,接下来要重点讲解一下 RecycleView 是如何做到 缓存 · 复用 的。

    RecycleView 复用

    我将 RecycleView 里面的缓存分为 4 级缓存:

    • mChangeScrapmAttachedScrap :用来缓存还在屏幕内的 ViewHolder
    • mCachedViews :用来缓存移除屏幕外的 ViewHolder
    • mViewCacheExtension :开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存。
    • RecycledViewPoolViewHolder缓存池。
    所谓复用,就是看 RecycleView 是如何从四级缓存中获取数据的。

    我们不妨猜想一下,RecycleView 的复用的发生时机在什么时候?当屏幕加载完一屏数据的时候,滑动到下一屏幕时,此时肯定需要重新绘制,因此猜想 RecycleView 会不会是在 ACTION_MOVE 的时候去获取缓存的呢?我们从 onTouchEventACTION_MOVE 中找一下:

    case MotionEvent.ACTION_MOVE: {
          if (mScrollState == SCROLL_STATE_DRAGGING) {
                 mLastTouchX = x - mScrollOffset[0];
                 mLastTouchY = y - mScrollOffset[1];
                  //从这个方法名猜想这里面做了什么
                  if (scrollByInternal(
                       canScrollHorizontally ? dx : 0,
                       canScrollVertically ? dy : 0,
                                vtev)) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
                        if (mGapWorker != null && (dx != 0 || dy != 0)) {
                            mGapWorker.postFromTraversal(this, dx, dy);
             }
      }
    

    我们从 setScrollState 这个地方继续跟进去看里面做了什么:

                if (x != 0) {
                    consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                    unconsumedX = x - consumedX;
                }
                if (y != 0) {
                    consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                    unconsumedY = y - consumedY;
                }
    

    这里可以看到分别是进行横向和纵向滚动时的处理,到这里就要去到 LayoutManager 去寻找了,由于几个 LayoutManager 里面的方法大同小异,这里就拿 LinearLayoutManager 来举例。我们继续跟到 LinearLayoutManager 里的 scrollVerticallyBy 方法看看里面做了什么:

    @Override
        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                RecyclerView.State state) {
            if (mOrientation == HORIZONTAL) {
                return 0;
            }
            //从这里继续跟进去
            return scrollBy(dy, recycler, state);
        }
    ----------------------------------------------------------
    final int consumed = mLayoutState.mScrollingOffset
                        //注意这个 fill 方法
                    + fill(recycler, mLayoutState, state, false);
            if (consumed < 0) {
                if (DEBUG) {
                    Log.d(TAG, "Don't have any more elements to scroll");
                }
                return 0;
            }
    

    看到这个 fill 方法,从名字猜想会不会是在里面获取缓存呢,我们继续跟进去。

     if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("LLM LayoutChunk");
                }
                //注意这个方法
                layoutChunk(recycler, state, layoutState, layoutChunkResult);
                if (VERBOSE_TRACING) {
                    TraceCompat.endSection();
                }
                if (layoutChunkResult.mFinished) {
                    break;
                }
    

    当我们进到 fill 方法里时,注释里告诉我们说只要稍加修改就可以将这个方法做成一个工具方法,这个方法其实就是一个对于填充布局的复用方法,其中填充布局主要在 layoutChunk 方法,我们从这里跟进去看看:

    //注意这里从recycler 里生成view
    View view = layoutState.next(recycler);
            if (view == null) {
                if (DEBUG && layoutState.mScrapList == null) {
                    throw new RuntimeException("received null view when unexpected");
                }
                // if we are laying out views in scrap, this may return null which means there is
                // no more items to layout.
                result.mFinished = true;
                return;
            }
    

    可以看到在这里从 RecycleView 里去拿view,如果拿到的话就通过 addView 的方式添加进去。我们的目的就是要知道如何获通过缓存加载 View 的。因此我们从 layoutState.next 继续跟进去看:

    View next(RecyclerView.Recycler recycler) {
                if (mScrapList != null) {
                    return nextViewFromScrapList();
                }
              //通过 getViewForPosition 这个方法生成view
                final View view = recycler.getViewForPosition(mCurrentPosition);
                mCurrentPosition += mItemDirection;
                return view;
            }
    

    继续看 getViewForPosition 这个方法:

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

    好累啊,继续进到 tryGetViewHolderForPositionByDeadline 这个方法看看:

    if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrapOrHiddenOrCache = holder != null;
                }
    

    首先通过 getChangedScrapViewForPositionViewHolder

    // 1) Find by position from scrap/hidden list/cache
                if (holder == null) {
                    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                    if (holder != null) {
                        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();
                                }
                                recycleViewHolderInternal(holder);
                            }
                            holder = null;
                        } else {
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
                }
    
    • 如果上一步拿不到的话就从 getScrapOrHiddenOrCachedHolderForPosition 这个方法获取 viewHolder
     // 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 这个方法获取。
    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);
                        if (view != null) {
                            holder = getChildViewHolder(view);
                            if (holder == null) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view which does not have a ViewHolder"
                                        + exceptionLabel());
                            } else if (holder.shouldIgnore()) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view that is ignored. You must call stopIgnoring before"
                                        + " returning this view." + exceptionLabel());
                            }
                        }
                    }
    
    • mViewCacheExtension.getViewForPositionAndType 获取 ViewHolder
    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);
                            }
                        }
                    }
    
    • 最后从 getRecycledViewPool().getRecycledView 获取 ViewHolder
      因此可以看出这个获取 ViewHolder 过程的思想有点类似 责任链模式。
      1. getChangedScrapViewForPosition -- mChangeScrap 与动画相关
      1. getScrapOrHiddenOrCachedHolderForPosition -- mAttachedScrap 、mCachedViews
      1. getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)
      1. mViewCacheExtension.getViewForPositionAndType -- 自定义缓存
      1. getRecycledViewPool().getRecycledView -- 从缓冲池里面获取

    从刚才一直在说回收的是 ViewHolder,所谓 ViewHolder 指的就是对 View 的包裹,包装。因此 RecycleView 回收的就是 包装View 的东西。

    如果通过上面那几种方法都没办法获取 ViewHolder 的话,就继续走下面的:

     if (holder == null) {
                        long start = getNanoTime();
                        if (deadlineNs != FOREVER_NS
                                && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                            // abort - we have a deadline we can't meet
                            return null;
                        }
                         //注意,从这里去创建 ViewHolder
                        holder = mAdapter.createViewHolder(RecyclerView.this, type);
                        if (ALLOW_THREAD_GAP_WORK) {
                            // only bother finding nested RV if prefetching
                            RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                            if (innerView != null) {
                                holder.mNestedRecyclerView = new WeakReference<>(innerView);
                            }
                        }
    
                        long end = getNanoTime();
                        mRecyclerPool.factorInCreateTime(type, end - start);
                        if (DEBUG) {
                            Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                        }
                    }
                }
    -----------------------------------
    public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
                try {
                    TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                        //看到这个方法名肯定很熟悉了吧,这就是我们平时重写的那个方法。
                    final VH holder = onCreateViewHolder(parent, viewType);
                    if (holder.itemView.getParent() != null) {
                        throw new IllegalStateException("ViewHolder views must not be attached when"
                                + " created. Ensure that you are not passing 'true' to the attachToRoot"
                                + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                    }
                    holder.mItemViewType = viewType;
                    return holder;
                } finally {
                    TraceCompat.endSection();
                }
            }
    

    上面是创建 ViewHolder 的过程,创建完之后怎么办了呢:

    if (mState.isPreLayout() && holder.isBound()) {
                    // 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);
                      //在这里进行 绑定
                    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
                }
    -----------------------------------------------------
    
    private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
                    int position, long deadlineNs) {
                holder.mOwnerRecyclerView = RecyclerView.this;
                final int viewType = holder.getItemViewType();
                long startBindNs = getNanoTime();
                if (deadlineNs != FOREVER_NS
                        && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
                    // abort - we have a deadline we can't meet
                    return false;
                }
                 //这里会不会是我们平时的重写的 bind 方法呢
                mAdapter.bindViewHolder(holder, offsetPosition);
                long endBindNs = getNanoTime();
                mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
                attachAccessibilityDelegateOnBind(holder);
                if (mState.isPreLayout()) {
                    holder.mPreLayoutPosition = position;
                }
                return true;
            }
    ---------------------------------------------------------------
    
    public final void bindViewHolder(@NonNull VH holder, int position) {
                holder.mPosition = position;
                if (hasStableIds()) {
                    holder.mItemId = getItemId(position);
                }
                holder.setFlags(ViewHolder.FLAG_BOUND,
                        ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
                TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
                //翻过千山万水终于找到你了,这里就是平时我们重写的 BindViewHolder 方法了
                onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
                holder.clearPayload();
                final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
                if (layoutParams instanceof RecyclerView.LayoutParams) {
                    ((LayoutParams) layoutParams).mInsetsDirty = true;
                }
                TraceCompat.endSection();
            }
    

    通过上面这几步,可以得出,从那 5 个链条获取不到可以使用的 缓存(ViewHolder) 的话,就去 创建(onCreateViewHolder)绑定(onBindViewHolder)。因此这也就是为什么一般 onCreateViewHolder 这个方法一般只走一遍,onBindViewHolder 会走很多遍的原因。

    RecycleView 回收

    所谓回收就是 RecycleView 如何将不需要的 ViewHolder 存进前面说的那 4 级缓存中。

    首先是 mCachedViews,它是一个 默认大小为 2ArraryList

    当 ViewHolder 可以回收,也就是 ViewHolder 没有发生改变时:
    • 首先它会去判断需要缓存的 CachedView 的大小是否超过 2 ,如果超过了就会将 CachedViews 的第 0 个 放入 缓存池
    • 然后将大小减 1,存入新的 CachedView。它的思想有点类似于队列即 先进先出
      因此 缓存池 中数据都是从 CachedViews 来的。
      缓存池 则是通过**ViewType 进行回收的,ViewHolder 存在于一个叫做 ScrapData 的实体类中,通过 ViewType 去从一个封装了 ScrapDataSparseArray 。也就是说 同一种 typeViewHolder存储在一个大小为 5 的 ScrapData 中。 cachedview & recycleViewPool 从图中可以看出 缓存池 的这种构造有点类似 HashMap1.7 的实现。
      上面说了当 ViewHolder 没发生改变时的回收方式,而当 ViewHolder 发生改变时,则直接放入 缓存池 中。对于 缓存池 还有一点需要注意的是,存入 ViewHolder 后,会将 viewHolder 中的数据清空,只保存布局信息。所以,当ViewHolder 超过 5 之后就直接丢弃。
      因此 cachView缓存池 的一大区别是 cachView 中存储的 ViewHolder 含有数据,而 缓存池 中只保存了 viewHolder 的布局信息。

    最后是 mAttachedScrapmChangedScrap
    当屏幕内的 ViewHolder 没有发生改变时(移除,变化等),就存入 mAttachedScrap,否则的话就存入mChangedScrap。这两种比较简单。

    以上就是 RecycleView 的缓存回收机制,希望对你有所帮助~

    相关文章

      网友评论

        本文标题:温柔撕破 - RecycleView

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