AndroidX RecyclerView總結-Recycler

概述

通過博文記錄RecyclerView的源碼學習過程有助於鞏固自己的記憶和加深整體實現機制的理解。

《AndroidX RecyclerView總結-測量佈局》,其中LinearLayoutManager在佈局時,通過Recycler來獲取ViewHolder中的itemView進行添加和佈局。衆所周知,Recycler負責緩存ViewHolder以供複用,這裏通過追蹤源碼看看Recycler的工作機制。

源碼探究

文中源碼基於 ‘androidx.recyclerview:recyclerview:1.1.0’

緩存集合

Recycler使用了多個緩存集合進行多級緩存,接下來從LinearLayoutManager的佈局過程中看Recycler對ViewHolder的緩存和獲取的工作流程。

ViewHolder的存儲

先從ViewHolder存儲過程入手,看看各個緩存集合的作用。

佈局期間

在LinearLayoutManager的佈局方法onLayoutChildren中,在確定錨點之後填充佈局之前,會調用detachAndScrapAttachedViews方法進行臨時回收當前RecyclerView上attached的View對應的ViewHolder。

[LinearLayoutManager#onLayoutChildren]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    // 該方法中進行回收操作
    detachAndScrapAttachedViews(recycler);
    // ···
    if (mAnchorInfo.mLayoutFromEnd) {
        // ···
        // 填充佈局
        fill(recycler, mLayoutState, state, false);
        // ···
    } else {
        // ···
        fill(recycler, mLayoutState, state, false);
        // ···
    }
    // ···
}

detachAndScrapAttachedViews方法中遍歷child,依次調用scrapOrRecycleView方法:
[LinearLayoutManager#scrapOrRecycleView]

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    // 獲取該view對應的ViewHolder
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        if (DEBUG) {
            Log.d(TAG, "ignoring view " + viewHolder);
        }
        return;
    }
    // 判斷是否ViewHolder的item數據被標記無效但還未從適配器數據集中移除。hasStableIds默認返回false
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        // 將觸發detachedFromWindow和ViewGroup.removeViewAt方法
        removeViewAt(index);
        // 添加至mCachedViews或RecycledViewPool中緩存
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        // 將爲index對應的viewHolder添加FLAG_TMP_DETACHED標記,觸發ViewGroup.detachViewFromParent方法
        detachViewAt(index);
        // 添加至mAttachedScrap或mChangedScrap中緩存
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

當重新設置Adapter或調用Adapter的notifyDataSetChanged方法會給ViewHolder標記FLAG_INVALID,需要完全重新綁定View。當移除Adapter中數據集的某個item數據時會給對應的ViewHolder標記FLAG_REMOVED,但它綁定的View可能仍然需要保留以用於item動畫。同時滿足以上情況時調用recycleViewHolderInternal方法進行緩存,否則調用scrapView緩存。

這裏的scrap和recycle是兩種不同行爲。scrap表示着View仍然在RecyclerView上,只是臨時detach,稍後會再attach回來。recycle意味着View將移出RecyclerView,緩存ViewHolder實例,可能不用重新綁定View,但是對應的索引位置將不一致。

mCachedViews

recycleViewHolderInternal方法主要是對要移出RecyclerView的ViewHolder,或是item數據徹底無效或徹底移除的ViewHolder進行緩存。當RecyclerView上下滑動或item消失動畫結束或徹底移除適配器數據集中對應的item等情況都會調用該方法進行回收。

[RecyclerView#recycleViewHolderInternal]

void recycleViewHolderInternal(ViewHolder holder) {
    // 省略異常檢查部分 ···
    final boolean transientStatePreventsRecycling = holder
            .doesTransientStatePreventRecycling();
    @SuppressWarnings("unchecked")
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder);
    boolean cached = false;
    boolean recycled = false;
    // ···
    // 判斷是否強制回收或ViewHolder設置可回收,默認爲true
    if (forceRecycle || holder.isRecyclable()) {
        // mViewCacheMax默認爲2,判斷ViewHolder是否需要重新綁定
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            // 判斷mCachedViews容量是否已滿
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 移除第一個添加的ViewHolder,並通過addViewHolderToRecycledViewPool方法將其轉移到RecycledViewPool中
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            // 添加到mCachedViews中
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            // 若不滿足添加進mCachedViews的條件,則添加進RecycledViewPool中
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // DEBUG···
    }
    // ···
}

這裏將無需重新綁定View的ViewHolder保存在mCachedViews中,若mCachedViews容量不足(默認上限2),則將最早添加的轉移到RecycledViewPool中。
若不滿足添加進mCachedViews的條件,則將ViewHolder添加進RecycledViewPool。

RecycledViewPool

在addViewHolderToRecycledViewPool方法中通過getRecycledViewPool方法獲取RecycledViewPool實例:
[RecyclerView#getRecycledViewPool]

RecycledViewPool getRecycledViewPool() {
    if (mRecyclerPool == null) {
        mRecyclerPool = new RecycledViewPool();
    }
    return mRecyclerPool;
}

若mRecyclerPool已經存在,則直接返回。mRecyclerPool也可通過RecyclerView.setRecycledViewPool方法傳入一個實例獲得,從而支持多個RecyclerView共用一個RecycledViewPool

這裏先看一下RecycledViewPool的主要結構:
[RecycledViewPool]

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;
        // ···
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    // ···
}

RecycledViewPool的mScrap成員以ViewHolder的viewType爲key,ScrapData爲value。ScrapData中持有一個ViewHolder集合,集合的容量是5。當添加ViewHolder時,需要先取出對應viewType的ScrapData。
(可以理解爲類似Map/<Integer, list//>的集合)

在取得RecycledViewPool實例後,調用它的putRecycledView方法進行添加:
[RecycledViewPool#putRecycledView]

public void putRecycledView(ViewHolder scrap) {
    // 獲取該ViewHolder的viewType
    final int viewType = scrap.getItemViewType();
    // 獲取viewType對應的ViewHolder集合
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    // 判斷是否達到容量上限
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    // 重置ViewHolder中的數據
    scrap.resetInternal();
    // 添加集合保存
    scrapHeap.add(scrap);
}

RecycledViewPool的作用是一個對象緩存池,避免頻繁創建ViewHolder,但是ViewHolder仍然需要重新進行View綁定。

mAttachedScrap、mChangedScrap

回到scrapOrRecycleView方法中,正常佈局情況下會進入Recycler的scrapView方法:
[Recycler#scrapView]

void scrapView(View view) {
    // 獲取該view對應的ViewHolder
    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);
    }
}

mAttachedScrap和mChangedScrap都用於緩存RecyclerView上臨時detach的ViewHolder。區別是mAttachedScrap保存的是沒有變化的ViewHolder,mChangedScrap保存的是有變化的,例如調用了Adapter.notifyItemRangeChanged方法。

滾動期間

RecyclerView觸發滾動時會調用LinearLayoutManager的scrollBy方法:
[LinearLayoutManager#scrollBy]

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    mLayoutState.mRecycle = true;
    // ···
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    // ···
}

該方法中先將mRecycle標記爲true(默認爲false,其他場景也都爲false),之後通過fill方法進行填充。

在fill方法中會判斷是否存在滾動,並對移除屏幕的View進行回收緩存:
[LinearLayoutManager#fill]

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // ···
    // 判斷是否滾動
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        // 檢查回收
        recycleByLayoutState(recycler, layoutState);
    }
    // ···
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ···
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // ···
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 檢查回收
            recycleByLayoutState(recycler, layoutState);
        }
    }
    // ···
}

在開始循環填充View前,先檢查上下滑動的回收,在每填充一個View後也進行一次檢查回收。

recycleByLayoutState方法中會判斷mRecycle變量是否爲false,該變量默認爲true,但在前面的scrollBy中會設置爲false。接着根據佈局方向從頂部或底部回收ViewHolder,通過for循環逐個回收離開屏幕的View,回收的代碼在removeAndRecycleViewAt方法中。

進入removeAndRecycleViewAt方法:
[LinearLayoutManager#removeAndRecycleViewAt]

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    // 從ViewGroup移除View
    removeViewAt(index);
    // 使用Recycler進行回收
    recycler.recycleView(view);
}

在recycleView方法中會進行一些回調和清理工作,並調用recycleViewHolderInternal方法回收ViewHolder,保存進mCachedViews或RecycledViewPool中。

ViewHolder的獲取

接着看獲取緩存的工作流程,看看各緩存集合的讀取優先級。LinearLayoutManager在layoutChunk方法中進行單個View的添加和佈局,該方法中首先通過LayoutState的next方法獲取View,而next方法中又調用Recycler的getViewForPosition方法並傳入當前適配器item數據的索引:

[Recycler#getViewForPosition]

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

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

通過tryGetViewHolderForPositionByDeadline獲取到ViewHolder後,取裏面的itemView返回。

tryGetViewHolderForPositionByDeadline方法較長,這裏分成幾部分看:

從緩存集合中查找

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // 檢查索引越界
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                + "(" + position + "). Item count:" + mState.getItemCount()
                + exceptionLabel());
    }
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        // 如果是預佈局則從mChangedScrap中查找
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        // 先從mAttachedScrap中查找,再從ChildHelper的mHiddenViews(保留用於動畫的View)
        // 中查找,再從mCachedViews中查找
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            // 檢查ViewHolder是否無效
            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();
                    }
                    // 重新回收該ViewHolder
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }

    // ···
}

優先mChangedScrap或mAttachedScrap中查找,找不到再從mCachedViews中查找。

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ···
    
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
            throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                    + "position " + position + "(offset:" + offsetPosition + ")."
                    + "state:" + mState.getItemCount() + exceptionLabel());
        }

        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        // hasStableIds默認返回false
        if (mAdapter.hasStableIds()) {
            // 先從mAttachedScrap中查找,再從mCachedViews中查找
            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.
            // 從ViewCacheExtension中查找,
            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");
            }
            // 從RecycledViewPool中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        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;
            }
            // 觸發Adapter的onCreateViewHolder回調創建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");
            }
        }
    }
    
    // ···
}

緩存集合查找優先級:mAttachedScrap->mCachedViews->ViewCacheExtension->RecycledViewPool

  • ViewCacheExtension說明
    ViewCacheExtension抽象類需要開發者繼承,實現getViewForPositionAndType方法,完成具體的緩存策略。RecyclerView.mViewCacheExtension默認爲null,通過RecyclerView.setViewCacheExtension方法設置。

新建ViewHolder

當從以上緩存集合中都沒有找到可用的ViewHolder後,會Adapter.createViewHolder方法進行創建。

[Adapter#createViewHolder]

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
    try {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        // onCreateViewHolder由開發者實現,返回具體的ViewHolder
        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();
    }
}

觸發了onCreateViewHolder回調方法返回ViewHolder。

綁定View

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ···
    
    boolean bound = false;
    // 判斷是否需要進行View綁定操作
    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);
        // 綁定View
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 生成RecyclerView.LayoutParams,並使之和ViewHolder互相持有引用
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
    
    // ···
}

若判斷ViewHolder未綁定或需要重新綁定,則調用tryBindViewHolderByDeadline方法進行綁定:
[Recycler#tryBindViewHolderByDeadline]

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;
    }
    // 調用Adapter的bindViewHolder方法進行綁定
    mAdapter.bindViewHolder(holder, offsetPosition);
    long endBindNs = getNanoTime();
    mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
    attachAccessibilityDelegateOnBind(holder);
    if (mState.isPreLayout()) {
        holder.mPreLayoutPosition = position;
    }
    return true;
}

該方法中調用Adapter的bindViewHolder方法,bindViewHolder又調用Adapter的onBindViewHolder回調方法,又開發者實現具體的綁定邏輯。

總結

在Recycler中的緩存集合:

  • mAttachedScrap、mChangedScrap
    填充佈局前緩存當前RecyclerView上的ViewHolder,ViewHolder會短暫detach,不會remove。mAttachedScrap和mChangedScrap的區別是mChangedScrap保存需要局部刷新的ViewHolder,例如Adapter.notifyItemRangeChanged指定範圍之間的。查找優先級最高。

  • mCachedViews
    RecyclerView上下滑動或Adapter數據集變更或item移出等導致ViewHolder索引位置失效或item內容變化等數據無效的情況下緩存ViewHolder。容量上限默認是2。優先級次高。

  • RecycledViewPool
    當往mCachedViews添加但mCachedViews超限時,會將mCachedViews裏面最早添加的ViewHolder轉存到RecycledViewPool中,同時重置該ViewHolder的數據。RecycledViewPool中根據viewType區分保存ViewHolder,每種viewType的默認存儲上限是5。可以多個RecyclerView共用一個RecycledViewPool。優先級最低。

  • ViewCacheExtension
    默認爲空,需要開發者自行繼承實現,只有獲取緩存時調用,返回緩存View。優先級介於mCachedViews和RecycledViewPool之間。

當沒有可用緩存時,會通過Adapter的onCreateViewHolder回調返回開發者創建的ViewHolder。在獲取到ViewHolder後,判斷該ViewHolder是否未綁定View(新創建的)或需要重新綁定View(數據無效、數據重置),若要綁定的話,通過Adapter的onBindViewHolder回調執行開發者實現的具體綁定邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章