阅读 1207

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回调执行开发者实现的具体绑定逻辑。