【Android】RecyclerView回收复用机制

概述

RecyclerView 是 Android 中用于高效显示大量数据的视图组件,它是 ListView 的升级版本,支持更灵活的布局和功能。

我们创建一个RecyclerView的Adapter:

public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerView.MyHolder> {

    private List<String> strings;
    private Context context;

    public MyRecyclerView(List<String> strings, Context context) {
        this.strings = strings;
        this.context = context;
    }

    @NonNull
    @Override
    public MyRecyclerView.MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

        View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
        MyRecyclerView.MyHolder viewHolder = new MyHolder(view);
        Log.d("MyRecyclerView", "onCreateViewHolder: ");
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull MyRecyclerView.MyHolder holder, int position) {
        holder.textView.setText("第" + position + "项");
        Log.d("MyRecyclerView", "onBindViewHolder: " + position);
    }

    @Override
    public int getItemCount() {
        return strings == null ? 0 : strings.size();
    }

    public class MyHolder extends RecyclerView.ViewHolder {
        private TextView textView;
        public MyHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(android.R.id.text1);
        }
    }
}

我们在onCreateViewHolder和onBindViewHolder都打印log。

onCreateViewHolder()会在创建一个新view的时候调用,onBindViewHolder()会在已存在view,绑定数据的时候调用。

我们来看一下运行时打印的log:

image-20241119212232289

在最开始加载view的时候,两个方法onCreateViewHolder()onBindViewHolder()都执行了,但是当我们上下滑动RecyclerView的时候,我们会发现只执行了onBindViewHolder()方法。所以说,RecyclerView并不是会一直重新创建View,而是会对view进行复用。

复用机制

当我们想去通过看源码去了解缓存复用机制的时候,我们要去想看源码的入口在哪里。上文我们提到是在滑动RecyclerView的时候进行了缓存复用,所以我们会想到去看 onTouchEvent 这个方法:

@Override
public boolean onTouchEvent(MotionEvent e) {
        ...
    case MotionEvent.ACTION_MOVE: {
        final int index = e.findPointerIndex(mScrollPointerId);
        if (index < 0) {
            Log.e(TAG, "Error processing scroll; pointer index for id "
                    + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
            return false;
        }

        final int x = (int) (e.getX(index) + 0.5f);
        final int y = (int) (e.getY(index) + 0.5f);
        int dx = mLastTouchX - x;
        int dy = mLastTouchY - y;

        if (mScrollState != SCROLL_STATE_DRAGGING) {
            boolean startScroll = false;
            if (canScrollHorizontally) {
                if (dx > 0) {
                    dx = Math.max(0, dx - mTouchSlop);
                } else {
                    dx = Math.min(0, dx + mTouchSlop);
                }
                if (dx != 0) {
                    startScroll = true;
                }
            }
            if (canScrollVertically) {
                if (dy > 0) {
                    dy = Math.max(0, dy - mTouchSlop);
                } else {
                    dy = Math.min(0, dy + mTouchSlop);
                }
                if (dy != 0) {
                    startScroll = true;
                }
            }
            if (startScroll) {
                setScrollState(SCROLL_STATE_DRAGGING);
            }
        }

        if (mScrollState == SCROLL_STATE_DRAGGING) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            if (dispatchNestedPreScroll(
                    canScrollHorizontally ? dx : 0,
                    canScrollVertically ? dy : 0,
                    mReusableIntPair, mScrollOffset, TYPE_TOUCH
            )) {
                dx -= mReusableIntPair[0];
                dy -= mReusableIntPair[1];
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
                // Scroll has initiated, prevent parents from intercepting
                getParent().requestDisallowInterceptTouchEvent(true);
            }

            mLastTouchX = x - mScrollOffset[0];
            mLastTouchY = y - mScrollOffset[1];

            if (scrollByInternal(
                    canScrollHorizontally ? dx : 0,
                    canScrollVertically ? dy : 0,
                    e)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            if (mGapWorker != null && (dx != 0 || dy != 0)) {
                mGapWorker.postFromTraversal(this, dx, dy);
            }
        }
    } break;
				...
    return true;
}

case:MotionEvent.ACTION_MOVE里有 scrollByInternal() 这个方法:

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0;
    int unconsumedY = 0;
    int consumedX = 0;
    int consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        scrollStep(x, y, mReusableIntPair);
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        unconsumedX = x - consumedX;
        unconsumedY = y - consumedY;
    }
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH, mReusableIntPair);
    unconsumedX -= mReusableIntPair[0];
    unconsumedY -= mReusableIntPair[1];
    boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

    // Update the last touch co-ords, taking any scroll offset into account
    mLastTouchX -= mScrollOffset[0];
    mLastTouchY -= mScrollOffset[1];
    mNestedOffsets[0] += mScrollOffset[0];
    mNestedOffsets[1] += mScrollOffset[1];

    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
        if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
            pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
        }
        considerReleasingGlowsOnScroll(x, y);
    }
    if (consumedX != 0 || consumedY != 0) {
        dispatchOnScrolled(consumedX, consumedY);
    }
    if (!awakenScrollBars()) {
        invalidate();
    }
    return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

里面的 scrollStep() 方法:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();

    TraceCompat.beginSection(TRACE_SCROLL_TAG);
    fillRemainingScrollValues(mState);

    int consumedX = 0;
    int consumedY = 0;
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    TraceCompat.endSection();
    repositionShadowingViews();

    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);

    if (consumed != null) {
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

scrollHorizontallyByscrollVerticallyBy

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                                RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        return 0;
    }
    return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                              RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}

两个都执行的 scrollBy

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
        return 0;
    }
    ensureLayoutState();
    mLayoutState.mRecycle = true;
    final int layoutDirection = delta > 0 ? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    updateLayoutState(layoutDirection, absDelta, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    if (consumed < 0) {
        if (DEBUG) {
            Log.d(TAG, "Don't have any more elements to scroll");
        }
        return 0;
    }
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    mOrientationHelper.offsetChildren(-scrolled);
    if (DEBUG) {
        Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
    }
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
}

里面的 fill 方法最为关键:

int fill(RecyclerView.Recycler recycler, LinearLayoutManager.LayoutState layoutState,
         RecyclerView.State state, boolean stopOnFocusable) {
   ...
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.beginSection("LLM LayoutChunk");
        }
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        /**
         * Consume the available space if:
         * * layoutChunk did not request to be ignored
         * * OR we are laying out scrap children
         * * OR we are not doing pre-layout
         */
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }

        if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        if (stopOnFocusable && layoutChunkResult.mFocusable) {
            break;
        }
    }
    if (DEBUG) {
        validateChildOrder();
    }
    return start - layoutState.mAvailable;
}

这个方法功能是填充给定的布局,通过while循环不断进行填充,其中的 layoutChunk() 方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        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;
        }
        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);
            }
        }
    ...
}

依次点击:

image-20241120203800209

image-20241120204024167

image-20241120204140384

最后我们就找到了回收复用的最关键的代码。

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()) {
        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) {
            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;
            }
        }
    }
    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
        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);
                }
            }
        }
        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;
            }
            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");
            }
        }
    }

    // This is very ugly but the only place we can grab this information
    // before the View is rebound and returned to the LayoutManager for post layout ops.
    // We don't need this in pre-layout since the VH is not updated by the LM.
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    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()) {
        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);
    }

    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(ItemView)

我们可以通过上面代码看出来RecyclerView的复用机制

第一层:Changed Scrap

代码:

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

解释

  • 如果当前处于预布局(isPreLayout == true),会优先从变更缓存中获取。
  • getChangedScrapViewForPosition(position)查找变更缓存(mChangedScrap),这里保存的是那些被标记为需要更新的ViewHolder
  • 如果找到,直接返回,不需要从其他层中查找。

RecyclerView 的布局过程中,预布局(Pre-Layout)RecyclerView 为支持动画效果(如插入、删除、移动等操作)而执行的一个特殊布局阶段。

以下操作都会触发 预布局阶段,并可能从变更缓存中获取视图来进行后续处理:

  • 插入、移除、范围更新notifyItemInserted, notifyItemRemoved, notifyItemRangeChanged 等)
  • 视图的布局和数据绑定(例如,setAdapter, setHasStableIds
  • 布局管理器或动画的变化setLayoutManager, setItemAnimator
  • 视图的移动notifyItemMoved
  • 所有与动画相关的操作(包括添加、删除、移动动画)

第二层:Scrap/Hidden/Cache

代码:

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
        if (!validateViewHolderForOffsetPosition(holder)) {
            if (!dryRun) {
                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;
        }
    }
}

解释

  • 如果第一层缓存未命中,尝试从

    普通缓存层中获取,包括:

    1. Scrap:表示那些视图项暂时不可见但还可以复用的视图,它们是最常见的一种缓存方式。被标记为废弃的视图,在没有被回收之前可以复用。
    2. Hidden:隐藏视图与 Scrap 类似,但它们的存在通常是为了支持更复杂的布局切换、动画等。它们在显示区域外,但仍然保留在缓存中,直到需要重新显示。
    3. CacheRecyclerView 使用缓存来存储那些根据视图 ID 或类型等条件频繁访问的视图。它们通常在视图池中存储较长时间,直到达到缓存容量限制。
  • 调用

    validateViewHolderForOffsetPosition(holder)
    

    检查缓存的有效性:

    • 如果无效(比如位置错位),将其回收。
    • 如果有效,标记fromScrapOrHiddenOrCache = true

第三层:Stable ID Cache

代码:

if (holder == null && mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
    if (holder != null) {
        holder.mPosition = offsetPosition;
        fromScrapOrHiddenOrCache = true;
    }
}

解释

  • 如果Adapter支持稳定ID(hasStableIds == true),尝试通过稳定ID查找缓存中的ViewHolder
  • 调用getScrapOrCachedViewForId,通过ID获取匹配的ViewHolder
  • 如果找到,将其位置更新为offsetPosition,并标记为来自缓存。

如何启用 Stable ID?

为了使 RecyclerView 使用 Stable ID Cache,必须确保以下两点:

  1. 实现 hasStableIds() 方法: 你需要在你的 RecyclerView.Adapter 中重写 hasStableIds() 方法并返回 true。这是启用 Stable ID Cache 的前提。
  2. 返回稳定的 ID: 在适配器的 getItemId() 方法中为每个项返回唯一的 ID。这个 ID 通常是数据中的唯一标识符(比如数据库中的主键)。

示例代码:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    @Override
    public boolean hasStableIds() {
        return true; // 启用 Stable ID
    }

    @Override
    public long getItemId(int position) {
        // 假设你的数据项有一个唯一的 id 字段
        return myDataList.get(position).getId();
    }

    // 其他适配器方法...
}

或者在构造方法中进行设置:

public MyRecyclerViewAdapter(List<String> strings, Context context) {
	this.strings = strings;
	this.context = context;
	setHasStableIds(true); // 启用稳定 ID
}

第四层:Recycled Pool

代码:

if (holder == null) {
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
    }
}

解释

  • Recycled PoolRecyclerView 内部维护的一个缓存池,用于存储已经被回收并不再使用的视图(View)。这些视图通常是已经滑出屏幕或者暂时不可见的视图。通过回收池,RecyclerView 可以避免每次滚动时都重新创建视图,而是将已回收的视图重新利用,从而提升滚动性能。

    回收池的工作机制是,RecyclerView 会在视图不再需要时将它们放入回收池(即已回收的视图池)。当需要新的视图时,RecyclerView 会从回收池中获取一个合适的视图进行重用。

  • 回收池存储的是所有超出缓存数量限制的ViewHolder,按type分类。

  • 如果找到,调用resetInternal()重置其状态。

最后一层:创建新 ViewHolder

代码中涉及的部分:

if (holder == null) {
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
}

说明

  • 如果所有缓存机制都未找到匹配的 ViewHolder,最终会调用 Adapter.createViewHolder 来创建新的实例。
  • 这是性能代价最高的一步。

我们通过点击

image-20241120215023059

image-20241120215058358

image-20241120215142001

就可以找到我们每次写Adapter都用重写的 onCreateViewHolder方法了。

在后面的代码中,我们依次点击:

image-20241120215850814

image-20241120215912483

image-20241120215934540

image-20241120215952052

image-20241120220013888

就可以找到我们每次写Adapter都用重写的 onBindViewHolder方法了。


已经到底啦!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/923052.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

websocket是什么?

一、定义 Websocket是一种在单个TCP连接上进行全双工通信的协议&#xff0c;它允许服务器主动向客户端推送数据&#xff0c;而不需要客户端不断的轮询服务器来获取数据 与http协议不同&#xff0c;http是一种无状态的&#xff0c;请求&#xff0c;响应模式的协议(单向通信)&a…

已存大量数据的mysql库实现主从各种报错----解决方案

背景何谓“先死后生”本文使用技术1、实施流程图2、实施2.1、数据库备份2.2、搭建Mysql的Master-Slave2.2.1、准备工作2.2.2、开始部署2.2.3、账号配置2.2.4、slave 同步配置2.2.5、验证 2.3、Master做数据恢复 结语 背景 计划对已有大量数据的mysql库的主从搭建&#xff0c;使…

SAP 零售方案 CAR 系统的介绍与研究

前言 当今时代&#xff0c;零售业务是充满活力和活力的业务领域之一。每天&#xff0c;由于销售运营和客户行为&#xff0c;它都会生成大量数据。因此&#xff0c;公司迫切需要管理数据并从中检索见解。它将帮助公司朝着正确的方向发展他们的业务。 这就是为什么公司用来处理…

模电复习易错题

PN 结&#xff1a;PN 结是由 P 型半导体和 N 型半导体通过特殊工艺结合在一起形成的结构。P 型半导体中多子是空穴&#xff0c;N 型半导体中多子是电子。内建电场&#xff1a;在 PN 结形成时&#xff0c;由于 P 区和 N 区载流子浓度的差异&#xff0c;会在结区形成一个内建电场…

AI安全:从现实关切到未来展望

近年来&#xff0c;人工智能技术飞速发展&#xff0c;从简单的图像识别到生成对话&#xff0c;从自动驾驶到医疗诊断&#xff0c;AI技术正深刻改变着我们的生活。然而&#xff0c;伴随着这些进步&#xff0c;AI的安全性和可控性问题也日益凸显。这不仅涉及技术层面的挑战&#…

nfs网络文件系统

NFS(Network File system&#xff0c;网络文件系统)是由SUN公司研制的UNIX表示层协议&#xff0c;它允许网络中的计算机(不同的计算机、不同的操作系统)之间通过TCP/IP网络共享资源&#xff0c;主要在unix系列操作系统上使用。在NFS的应用中&#xff0c;本地NFS的客户端应用可以…

mac终端配置-支持 git branch

mac 终端一般使用的是 zsh&#xff1b; 由于不想安装三方的软件&#xff0c;可以自行编写脚本实现一些效果&#xff1b; 最终效果如下&#xff0c;支持显示git 分支&#xff1a; git_branch(){branch"git branch 2>/dev/null | grep "^\*" | sed -e "…

tableau练习-制作30个图表

一、导入数据 1、导入数据 -添加-添加连接-到文件-excel格式用第一个excel导入&#xff0c;csv格式用第二个文本格式导入 2、连接数据 -从旁边这里直接拖到中间 标头连接 -日期若不一致需调节日期格式 3、保存数据 点击数据提取-再保存数据&#xff0c;保存为twbx格式 二、设计…

使用八爪鱼爬虫抓取汽车网站数据,分析舆情数据

我是做汽车行业的&#xff0c;可以用八爪鱼爬虫抓取汽车之家和微博上的汽车文章内容&#xff0c;分析各种电动汽车口碑数据。 之前&#xff0c;我写过很多Python网络爬虫的案例&#xff0c;使用requests、selenium等技术采集数据&#xff0c;这次尝试去采集小米SU7在微博、汽车…

【HarmonyOS开发实战】使用animation 和 animateTo来制作按钮动画(实现点击按钮释出更多小按钮)

如果你想在页面中添加按钮来实现页面跳转或者其他操作&#xff0c;又觉得过多的按钮太占地方&#xff0c;造成界面不美观。 那么我们可以将多个按钮“压缩”到一个按钮中&#xff0c;如下 在开始开发前&#xff0c;我们先了解一下animation和animateTo的区别。 animation&am…

国家级资质!同驭汽车获得CNAS实验室认证

近日&#xff0c;同驭汽车科技顺利通过中国合格评定国家认可委员会&#xff08;简称CNAS&#xff09;评审&#xff0c;获得《中国合格评定国家认可委员会实验室认可证书》。这标志着同驭已建立国际标准的实验室管理体系&#xff0c;产品的试验与检测技术能力达到了国际认可的准…

选择使用whisper.cpp进行语音转文字

需要将一些wav格式的语音文件转成文字&#xff08;ASR&#xff0c;STT&#xff09;&#xff0c;接到这个任务后&#xff0c;首先上网搜索有没有现成免费的工具或服务可以使用。常用的关键字如“语音转文字 免费 在线”。 搜到的很多野鸡网站&#xff0c;都可以免注册免费提供短…

消息称三星正与 OpenAI 洽谈,有望令 Galaxy AI 整合ChatGPT,三星都要和chatgpt合作了,你会使用chatgpt了吗?

还不知道怎么订阅chatgpt4.o和国外app服务的同学&#xff0c;可以看这里&#xff1a;WildCard官方平台订阅chatgpt 11 月 25 日消息&#xff0c;金融分析师 Dan Nystedt 在 X 平台透露称 OpenAI 正在与三星电子洽谈合作计划&#xff0c;讨论将其 ChatGPT 引入三星 Galaxy AI 的…

candence: 常用的一些命令: Move / Mirror / Rotate / Spain / Fix / unFix / Flipdesign

常用的一些命令 一、 Move 移动 一个可移动一个&#xff0c;也可多个 移动器件 二、 Mirror 镜像 Mirror 就是top 和 bottom 层的器件进行相互转换 三、 Rotate 旋转 移动过程中旋转 四、旋转 Spain 不能在移动中旋转 可以一次旋转一个&#xff0c;也可多个 一次旋转…

【深度学习】【RKNN】【C++】模型转化、环境搭建以及模型部署的详细教程

【深度学习】【RKNN】【C】模型转化、环境搭建以及模型部署的详细教程 提示:博主取舍了很多大佬的博文并亲测有效,分享笔记邀大家共同学习讨论 文章目录 【深度学习】【RKNN】【C】模型转化、环境搭建以及模型部署的详细教程前言模型转换--pytorch转rknnpytorch转onnxonnx转rkn…

Hadoop3.3.6集群安装

Hadoop3.3.6 三节点集群安装 准备工作 准备三台机器&#xff0c;大小为4c8g&#xff0c;主节点为 8c16g。并需要保证网络连通性&#xff0c;每台机器都相互ping一下 1、关闭网络防火墙 # 查看网络防火墙状态 sudo systemctl status firewalld # 立即停止 firewalld sudo sy…

计算机网络-GRE(通用路由封装协议)简介

昨天我们学习了VPN的基本概念&#xff0c;虚拟专用网络在当前企业总部与分支间广泛使用。常用的划分方法为基于协议层次有GRE VPN、IPSec VPN、L2TP VPN、PPTP VPN、SSL VPN等。其实我有考虑该怎么讲&#xff0c;因为在IP阶段好像虚拟专用网络讲得不深&#xff0c;在IE的阶段会…

Android 应用测试的各种环境问题记录(Instrumentation测试)

报错记录 failed to configure packages targetSdkVersion&#xff08;未解决&#xff09; failed to configure com.demo.test.SettingsActivityTest.testOnCreate_withNullSavedInstanceState: Package targetSdkVersion34 > maxSdkVersion32 java.lang.IllegalArgumentE…

计算机网络复习笔记(湖科大教书匠)

课程链接&#xff1a;【计算机网络微课堂&#xff08;有字幕无背景音乐版&#xff09;】 https://www.bilibili.com/video/BV1c4411d7jb/?p61&share_sourcecopy_web&vd_sourcecd12864239c2976e9f2bce4b307393f0 一、基础概念 信息交换方式 电路交换 电话交换机接通…

探索运维新视界,CMDB的3D机房功能深度解析

在数字化转型的浪潮中&#xff0c;数据中心作为企业信息架构的核心&#xff0c;其高效、智能的管理成为了企业竞争力的关键因素之一。3D机房作为这一趋势下的创新产物&#xff0c;正逐步改变着传统机房运维的面貌。本文将结合乐维CMDB&#xff0c;深入探讨3D机房的功能细节、应…