Android-recyclerview

温柔撕破 - RecycleView

2020-08-23  本文已影响0人  JackDaddy

前言

相信每一个做 Android 开发的人对于 RecycleView 都不陌生,一个 APP 中用到 RecycleView 的地方肯定不少于 50%,因此对 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 级缓存:

所谓复用,就是看 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;
                    }
                }
            }
 // 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;
                    }
                }
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());
                        }
                    }
                }
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);
                        }
                    }
                }

从刚才一直在说回收的是 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 没有发生改变时:

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

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

上一篇 下一篇

猜你喜欢

热点阅读