源码:RecyclerView核心知识点

news2024/12/23 9:33:17

一、简单使用

1.导包

implementation 'androidx.recyclerview:recyclerview:1.1.0'

2.使用

    mAdapter = new MyAdapter(getActivity());
    LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
    //设置布局方向
    // layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
    mRecyclerView.setLayoutManager(layoutManager);
    mRecyclerView.setAdapter(mAdapter);

二、RecyclerView家族图谱

LayoutManager:

布局管理器,用来管理RecyclerView的布局,负责measure、layout的功能,都会被LayoutManager代理,不同的LayoutManager所展示出来的样式是不一样的

默认提供LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager三大类,分别可以实现ListView,GridView以及流式布局的列表效果。

Android Sdk默认提供了三种样式:

  • LinearLayoutManager:线性
  • GridLayoutManager:网格
  • StaggeredGridLayoutManager:流式布局

还可以自定义一些特殊的布局样式

Recyler:

相当于item的缓存池,为RecyclerView提供高效、复用的回收机制

里面存在四种缓存机制:

  • mAttachedScrap
  • mCacheViews
  • mViewCacheExetensions
  • mRecylerPool

SmoothScroller

用来控制RecyclerView滑动时的速度,也可以自定义

SnapHelper:

可以控制RecyclerView松手之后惯性滑动的,可以控制来决定惯性停止后的位置

ItemAnimator:

处理列表上的item add、remove、change 的动画效果,自带默认动画,同时也可以自定义item的动画效果

ItemDecoration:

提供列表上item的装饰的能力,比如DeviderItemDecoration为列表上的item提供分割线、item间距的配置

ItemTouchHelper:

用来控制item的手势行为,比如可以实现列表的侧滑删除、拖动排序的功能,还可以扩展ItemDecoration实现吸顶悬浮的效果

OnItemTouchListener:

可以用来处理RecyclerView的手势分发,可以用来处理item的点击效果

DiffUtil:

用来对比两组数据前后的差异,为RecyclerView提供定向更新的能力

三、RecyclerView和ListView的区别

  • 从效果上看:RecyclerView不仅能实现ListView、GridView的效果,还可以完成瀑布流的效果,还能设置列表的滚动方向(垂直、水平);
  • 从实现上看: RecyclerView中view的复用不需要开发者自己写代码,系统内部已经封装完成了,可以进行局部刷新,提供API来实现item的动画效果
  • 从性能上:频繁的刷新数据,需要添加动画,RecyclerView优势很大,如果只是展示效果,区别并不大

四、RecyclerView 源码分析

1.从setLayoutManager出发

mRecyclerView.setLayoutManager(layoutManager);

在RecyclerView 的setLayoutManager 方法中,首先会判断RecyclerView以前是否已经关联了layoutManager

mLayout != null;

如果之前已经关联了,会对之前关联的layoutManager做一些清理和解除关联的操作,比如结束列表上的动画,移除列表上的items,清理缓存池的缓存

清理之后会把新设置的layoutManager 保存起来  

mLayout = layout;

紧接着判断新设置的layoutManager是否已经和其他的RecyclerView已经关联了,关联了就会抛出异常,

"LayoutManager " + layout + " is already attached to a RecyclerView:"

一个LayoutManager 只能和一个RecyclerView相关联


在方法的最后调用了requestLayout 

整个源码:

    public void setLayoutManager(@Nullable LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();
        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
        // chance that LayoutManagers will re-use views.
        if (mLayout != null) {
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            mLayout.removeAndRecycleAllViews(mRecycler);
            mLayout.removeAndRecycleScrapInt(mRecycler);
            mRecycler.clear();

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        // this is just a defensive measure for faulty item animators.
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout
                        + " is already attached to a RecyclerView:"
                        + layout.mRecyclerView.exceptionLabel());
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        mRecycler.updateViewCacheSize();
        requestLayout();
    }

2.onMeasure

RecyclerView也是一个ViewGroup,在它的onMeasure 方法中,首先也会判断RecyclerView 是否跟layoutManager相关联了,如果没有关联列表上的item 就无法测量,RecyclerView的宽高也就无法得到测量,就会调用defaultOnMeasure()来给RecyclerView设置一个尽量合理的宽高值


这个方法根据父容器的宽高值及RecyclerView宽高测量模式和padding、minimumWidth minmumHeight 来计算,将计算结果设置给RecyclerView

    void defaultOnMeasure(int widthSpec, int heightSpec) {
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }

接下来在onMeasure中判断layoutManager 是否开启了自动测量模式,也就是layoutManager 是否接管了对列表的自动测量工作,开启之后能保证RecyclerView 的宽高在不确定的情况下,同时列表的宽高也在不确定的情况下,能测量出一个正确的值

默认三种layoutManager 都是开启了

    @Override
    public boolean isAutoMeasureEnabled() {
        return true;
    }

对于RecyclerView来说并没有遍历它的子View去测量,而是把子View的测量工作交给了layoutManager 

就是因为RecyclerView 把子View 布局的测量和布局的工作暴露了出去,才得以自定义各种layoutManager的样式,但是这里只是把RecyclerView的子View的宽高值给暴露了出去,RecyclerView 本身的宽高值如果在布局中没有指定确定的值还是需要在这里测量得到的

接下来调用layout 的onMeasure 方法

mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

在这个方法里面,调用了recyclerView的defaultMeasure 方法,并没有开启列表item 的测量工作

       public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
                int heightSpec) {
            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        }

列表子item的测量工作是在onLayout中开始的

接下来判断RecyclerView宽高值是不是都有一个确切的值,比如设置了具体的宽高值,或者都设置了match_parent

measureSpecModeIsExactly才会为true,就不需要执行测量工作

         final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }

State:

RecyclerView的内部类,保存RecyclerView 在当前滑动状态下的所有信息,比如:

  • mTargetPosition:保存了调用scrollToPosition传递的值
  • mItemCount:当前滑动状态下列表上可以显示item的数量
  • mLayoutStep:布局的阶段。layoutmanager 把一次新的布局工作分成了三个阶段
    • STEP_START:第一阶段,预布局阶段,对应的方法是dispatchLayoutStep1,作用:在开启新的布局之前先收集需要做动画的item对应的动画信息
    • STEP_LAYOUT:第二阶段,真正开始布局的阶段,对应的方法是dispatchLayoutStep2,作用:让layoutmanager 测量并布局列表上的item,从而期望计算出RecyclerView的宽高,并不一定能准确的测出,如果RecyclerView没有一个确切的宽高并且列表上至少存在一个item也没有一个确切的宽高,在onMeasure阶段通过两次测量从而能计算出一个确切的宽高
    • STEP_ANIMATIONS:第三阶段,动画阶段,对应的方法是dispatchLayoutStep3,作用:主要是在布局完成之后通过阶段1收集的需要做动画的view信息,开启动画的执行

可以得出RecyclerView 优化的方向:

        尽可能的在布局中指定RecyclerView的宽高为固定的值或者设置为Match_parent,在列表中item的宽高也尽可能的设置固定值

如果没有设置确切的宽高值,就会继续执行以下代码

            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();

            // now we can get the width and height from the children.
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

3.onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

在onLayout中进而调用了dispatchLayout()

    void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

注意:如果在onMeasure()中调用了dispatchLayoutStep1,state就会被改变,否则才会执行到这里

如果state等于STEP_START 就执行dispatchLayoutStep1() ,然后执行dispatchLayoutStep2

dispatchStep2可能会被多次调用,比如没有给RecyclerView 和item设置确切的宽和高,在onMeasure 中就会调用两次

dispatchLayoutStep2 中

    private void dispatchLayoutStep2() {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }

mLayout.onLayoutChildren

layoutManager 去负责item的布局工作,item的布局怎么摆放交由layoutManager负责,可以是线性,网格,瀑布流,也可以是自定义

注意:

        任何自定义的layoutManager 都必须复写这个方法,摆放列表上view的位置

4.LinearLayoutManager

在onLayoutChildren()中

  1. 首先判断savedState是否不为空,说明RecyclerView所在的页面不可见了,此时需要把之前缓存的viewHolder全部移除,把view回收掉

     2. 接着会调用ensureLayoutState来创建layoutState 对象,存储了layoutManager在当前滑动下一些关联信息,

    void ensureLayoutState() {
        if (mLayoutState == null) {
            mLayoutState = createLayoutState();
        }
    }

比如:

  • available:还有多少可用的空间用来摆放列表上的子view
  • currentPosition:当前摆放到了第几个item
  • ScrollingOffset:当前滑动的偏移量

        在滑动的时候判断列表还有多少空间来继续向屏幕上填充item,在列表滑动时也会去更新layoutState 中字段的信息

     3. 接下来会调用resolveShouldLayoutReverse()判断要不要反转布局,从屏幕下方到屏幕上方开始布局

  • 如果调用了setReverseLayout 就会从下往上进行布局
  • 如果调用了setStackFromEnd 列表上item还是从上往下开始布局的,但是布局完成之后会自动滚动到最后一个item上,比如加载历史聊天记录

     4. 接下来判断AnchorInfo是否处于无效状态        

mAnchorInfo

        作用:用来存储锚点view的position位置信息和锚点view的位置坐标

这个对象的mValid在创建时默认是false,会进入if 分支

     5. 调用updateAnchorInfoForLayout() 收集锚点view的坐标和position信息

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }

        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from existing children");
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "deciding anchor info for fresh state");
        }
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }

在更新时提供了三种策略

  • 如果之前页面被回收了,本次页面恢复了以后会从pendingData对象中去恢复上一次的布局状态,恢复AnchorInfo 的数据
  • 列表上找这个焦点的view来更新AnchorInfo的数据
  • 否则根据布局的排列方式来取列表中第一个后者最后一个item来更新AnchorInfo 的数据

问题:

        列表在加载完成之后会自动滚动到某一个item的位置上,就是这个原因,列表上某一个item获取了焦点

解决办法:去除item的焦点,或者在这个item 前放一个能够过去焦点的view

问题:

        LinearLayoutManager在开始布局之前为什么要先去获取AnchorInfo?

原因:

        因为LinearLayout 在往列表上填充数据时并没有从上往下逐一填充,它是从锚点的位置也就是具备焦点item的位置,先是从上往下排列,再是从锚点位置从下往上继续填充

        这就是它往屏幕填充item的一个策略

     6. 接着要计算开始布局的偏移量,也就是刚开始设置layoutManager 时就调用了scrollToPosition,这时需要在布局开始之前就要去计算下开始布局位置的偏移量

     7. 接着就调用detachAndScrapAttachedViews(),在新的布局开始之前,需要对列表上已经存在的item对应的viewHolder 把它们分门别类的回收,只是暂时的回收到对应的缓存集合里,目的是为了接下来在布局时能够从recycle 回收池中复用viewHolder 

onLayoutChildren源码:

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // resolve layout direction
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
            // This case relates to when the anchor child is the focused view and due to layout
            // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
            // up after tapping an EditText which shrinks RV causing the focused view (The tapped
            // EditText which is the anchor child) to get kicked out of the screen. Will update the
            // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
            // the available space in layoutState will be calculated as negative preventing the
            // focused view from being laid out in fill.
            // Note that we won't update the anchor position between layout passes (refer to
            // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
            // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
            // child which can change between layout passes).
            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        }
        if (DEBUG) {
            Log.d(TAG, "Anchor info:" + mAnchorInfo);
        }

        // LLM may decide to layout items for "extra" pixels to account for scrolling target,
        // caching or predictive animations.

        mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
                ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        calculateExtraLayoutSpace(state, mReusableIntPair);
        int extraForStart = Math.max(0, mReusableIntPair[0])
                + mOrientationHelper.getStartAfterPadding();
        int extraForEnd = Math.max(0, mReusableIntPair[1])
                + mOrientationHelper.getEndPadding();
        if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
                && mPendingScrollPositionOffset != INVALID_OFFSET) {
            // if the child is visible and we are going to move it around, we should layout
            // extra items in the opposite direction to make sure new items animate nicely
            // instead of just fading in
            final View existing = findViewByPosition(mPendingScrollPosition);
            if (existing != null) {
                final int current;
                final int upcomingOffset;
                if (mShouldReverseLayout) {
                    current = mOrientationHelper.getEndAfterPadding()
                            - mOrientationHelper.getDecoratedEnd(existing);
                    upcomingOffset = current - mPendingScrollPositionOffset;
                } else {
                    current = mOrientationHelper.getDecoratedStart(existing)
                            - mOrientationHelper.getStartAfterPadding();
                    upcomingOffset = mPendingScrollPositionOffset - current;
                }
                if (upcomingOffset > 0) {
                    extraForStart += upcomingOffset;
                } else {
                    extraForEnd -= upcomingOffset;
                }
            }
        }
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;
        if (mAnchorInfo.mLayoutFromEnd) {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
        } else {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        }

        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        // noRecycleSpace not needed: recycling doesn't happen in below's fill
        // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtraFillSpace = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }

        // changes may cause gaps on the UI, try to fix them.
        // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
        // changed
        if (getChildCount() > 0) {
            // because layout from end may be changed by scroll to position
            // we re-calculate it.
            // find which side we should check for gaps.
            if (mShouldReverseLayout ^ mStackFromEnd) {
                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            } else {
                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            }
        }
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
        if (DEBUG) {
            validateChildOrder();
        }
    }

5.Recycle

Recycle是RecycleView的内部类,记录了viewHolder 回收的四级缓存

  • mAttachedScrap、mChangedScrap:同属于第一级缓存,存储的viewHolder 有一个特性,就是在复用的时候不需要重新调用bindviewHolder 去重新绑定数据,RecyclerView 认为这种情况下的viewHolder 还会再次出现在屏幕上,这两个缓存起来的ViewHolder 数据和状态是不会被重置的
  • mChcheViews:属于第二级缓存,存放随着列表的上下滑动,被滑出去的item对应的viewHolder ,默认容量大小是2,可以通过setItenlmDataSize来适当的扩容,即便是滑出屏幕了也可以多缓存几个,再次滑进屏幕时也是不需要重新绑定数据的
  • mViewCacheExtension:属于第三级缓存,是RecyclerView 缓存能力的一种拓展,允许开发者自定义ViewHolder 的缓存位置和实现方式
  • mRecyclerPool:属于第四级缓存,当且仅当mCacheViews放不下的时候,才会把ViewHolder放进mRecyclerPool 中,它在存放ViewHolder 时是会根据viewHolder对应的viewType 来分门别类的存储,在默认情况下每个viewType 对应的集合容量为5,也可以通过setRecyclerViewPool 来调整每个viewType 中viewHolder 的容量

举例:

1.当调用notifyItemChanged item_02刷新RecycleView时

item_02对应的ViewHolder就会被放到mChangedScrap里面,因为RecycleView认为这个item发生了变化,其余item是没有发生变化的,存放在mAttachedScrap集合里

总结:

  • mAttachedScrap是存放原封不动的ViewHolder
  • mChangedScrap是存放发生变化了的ViewHolder
  • mAttachedScrap、mChangedScrap存储的ViewHolder在复用的时候是不需要重新绑定数据的

2.当RecycleView向下滑动时,item_01被滑出屏幕

        刚被滑出屏幕的item对应的ViewHolder是会被缓存到mChcheViews这个集合中的,RecycleView认为很有可能会再次反向滑动列表使得item_01又会进入屏幕,被缓存到mChcheViews这个集合中的ViewHolder在复用的时候如果和原来的位置一致也是不需要重新调用bindViewHolder来重新绑定数据的,而mChcheViews集合是有容量限制的,默认是2,当列表一直往下滑动时,就会有很多item被滑出屏幕了,在添加mChcheViews集合之前会校验容量是否已满,满的话就会把最先添加到集合中的ViewHolder移除,并添加到mRecyclerPool集合中

总结:

        mRecyclerPool这个缓存所缓存ViewHolder都是需要重新绑定数据的,也就是说当调用了notifyDataSetChanged时,屏幕上几乎所有的item都需要重新绑定数据,所以在更新列表时,不到万不得已不要直接使用notifyDataSetChanged

回收机制源码分析

在调用notifyItemChanged、notifyDataSetChanged都会触发LayoutManager的onLayoutChildren

在这个方法中,LayoutManager向列表填充item时,直接向Recycler中去获取ViewHolder,如果获取不到,就说明缓存中没有可复用的ViewHolder,此时把item填充到列表上时就会去创建一个ViewHolder,再来重新绑定数据,这样是非常浪费的,所以在复用之前要先回收,从缓存中得到的itemView,在调用addView时,会判断这个view是否已经被添加过了,不用担心重复添加View的情况

        public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

在这个方法中对屏幕上可见的view进行遍历,然后逐个调用scrapOrRecycleView

       private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

这个方法在回收之前,会做一些判断 如果数据无效了且没有移除,就是调用了notifyDataSetChanged的时候,RecyclerView认为列表上的数据集已经全面发生了变化,列表上所有的item都需要重新绑定数据,就会给列表上的ViewHolder标记上一个无效的flag,进入if分支

  • 先判断viewHolder.isInvalid(),item的ViewHolder里面所存储的数据无效了,发生变化了,就说明这个viewHolder无效了
  • 然后判断!viewHolder.isRemoved(),这个ViewHolder的数据有没有被删除
  • 判断!mRecyclerView.mAdapter.hasStableIds() 列表上每一个item是否都有一个long类型身份id的标志,默认都是false,除非手动去指定

1.if分支

在if分支中,会调用

recycler.recycleViewHolderInternal(viewHolder);

在这个方法中会去回收ViewHolder

在回收的时候首先去判断是否有存储到mCacheViews里

如果没有就会判断mCacheViews缓存的数量是否已经溢出了,溢出的话就会把CacheViews中最先添加进来的,也就是最老的ViewHolder进行移除,移除后放到RecyclerPool里面去

然后把本次需要缓存的ViewHolder添加到mCacheViews集合中

mCachedViews.add(targetCacheIndex, holder);

1.else分支

如果不是notifyDataSetChanged导致的列表item无效,就会进入else分支,调用:

recycler.scrapView(view);

        void scrapView(View view) {
            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);
            }
        }

判断ViewHolder有没有被移除,有没有被置为无效,有没有被更新,如果没有的话RecycleView会认为在本次布局阶段依旧想留在屏幕上面的,就把它存储到mAttachedScrap集合中

mAttachedScrap.add(holder);

如果对应的ViewHolder被更新了,就会进入到else分支,也就是调用了notifyItemChanged触发了重新布局,就会把这个ViewHolder存放到mChangedScrap集合中

mChangedScrap.add(holder);

列表上填充Item

在onLayoutChildren中执行了detachAndScrapAttachedViews(recycler)后

判断是否需要倒序布局,也就是从屏幕的下方往屏幕的上方开始布局

只有调用setStackFromEnd、setReverseLayout显性的要求它倒序布局,都是false

else中会先从锚点的位置从上往下往列表上面填充item,紧接着会从从锚点的位置从下往上往列表上面填充item,都是调用fill方法进行填充

在网列表上填充item的时候开启一个while循环,while循环中有一个判断remainingSpace,在当前的方向上是否有多余的空间可用于填充item,在while循环中调用了layoutChunk,一个个的把item填充到列表上

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);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

1.从view对象的缓冲池中得到一个view对象

View view = layoutState.next(recycler);

进而调用layoutManager的addView方法

在这个addView方法中,会判断这个view对应的ViewHolder是否是一个即将被删除的View,如果是就把它添加到即将被删除的集合中

mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);

如果这个View之前已经被添加过了,就不需要再次添加了

child.getParent() == mRecyclerView

否则的话就把这个view添加到RecyclerView这个ViewGroup上了

mChildHelper.addView(child, index, false);

2.调用measureChildWithMargins,在测量时考虑到了item的上下左右的padding、margin所需要的空间

measureChildWithMargins(view, 0, 0);

3.紧接着会调用layoutDecoratedWithMargins,把view摆放到界面适当的位置上去,摆放子view位置时会考虑到decoration的存在,不至于把decoration遮盖住了

layoutDecoratedWithMargins(view, left, top, right, bottom);

复用机制流程

复用流程的结构

LayoutManager每次向列表上填充item时都会向Recycle去索取一个ViewHolder,

  • 在索取ViewHolder时Recycle会按照优先级先到mAttachedScrap、mChangeScrap这两个一级缓存中去查找是否有可复用的ViewHolder,这两个存储的ViewHolder不需要重新绑定数据,存储的都是屏幕内的ViewHolder
  • 如果一级缓存中找不到,就会向二级缓存mCacheViews去查找,如果找到,就会来一个位置一致性校验,因为mCacheViews里存储的ViewHolder是被滑出屏幕的,相同位置的才可以直接复用,如果是被下面滑上来的被复用的就需要重新绑定数据了
  • 如果二级缓存中找不到,就会向三级缓存允许开发者自定义mViewCacheExt中去查找
  • 如果都没找到,就会去四级缓存mRecyclerPool中去查找,回去根据item的viewType去查找,如果有就返回了,如果没有还是会调用adapter的onCreateViewHolder来创建一个并返回

复用流程最终会调用到tryGetViewHolderForPositionByDeadline 去尝试下的获取ViewHolder

   @Nullable
        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;
        }

在这个方法中首先去调用getChangedScrapViewForPosition 从一级缓存ChangedScrap里读取

holder = getChangedScrapViewForPosition(position);

如果ChangedScrap没有任何内容就直接返回了,接下来对mChangedScrap进行遍历,校验之前是否没有被复用过,之前列表上的位置和正要填充的位置是否一致,一致就返回

如果holder等于null,就会调用getScrapOrHiddenOrCachedHolderForPosition,从三个集合中尝试获取holder

holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

            // Try first for an exact, non-invalid match from scrap.
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }

            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    // This View is good to be used. We just need to unhide, detach and move to the
                    // scrap list.
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }

            // Search in our first-level recycled view cache.
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }

首先是到mAttachScrap中获取,判断方式跟mChangedScrap类似,如果添加都满足就返回

这里是第一次向mAttachScrap中去查找,如果在这里没找到,就会向正在做删除动画的item集合中去查找是否有可以满足复用的ViewHolder,虽然这里向mHiddenViews里面去查找有没有存在复用的ViewHolder,但它并不属于四级缓存,这个集合中存储的只是需要做删除动画的ViewHolder,动画执行完成后,这里缓存的ViewHolder就会被移除

View view = mChildHelper.findHiddenNonRemovedView(position);

    View findHiddenNonRemovedView(int position) {
        final int count = mHiddenViews.size();
        for (int i = 0; i < count; i++) {
            final View view = mHiddenViews.get(i);
            RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
            if (holder.getLayoutPosition() == position
                    && !holder.isInvalid()
                    && !holder.isRemoved()) {
                return view;
            }
        }
        return null;
    }

如果这里也没找到,就会去mCachedViews里面去查找

如果满足跳转就直接返回ViewHolder,否则就返回null 

如果 getScrapOrHiddenOrCachedHolderForPosition 返回不为空

就会调用validateViewHolderForOffsetPosition

        boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
            // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
            // if it is not removed, verify the type and id.
            if (holder.isRemoved()) {
                if (DEBUG && !mState.isPreLayout()) {
                    throw new IllegalStateException("should not receive a removed view unless it"
                            + " is pre layout" + exceptionLabel());
                }
                return mState.isPreLayout();
            }
            if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
                        + "adapter position" + holder + exceptionLabel());
            }
            if (!mState.isPreLayout()) {
                // don't check type if it is pre-layout.
                final int type = mAdapter.getItemViewType(holder.mPosition);
                if (type != holder.getItemViewType()) {
                    return false;
                }
            }
            if (mAdapter.hasStableIds()) {
                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
            }
            return true;
        }

在这个方法中会判断这个item的viewType和正在填充的item的viewType是否一致,

type != holder.getItemViewType()

如果给item开启了身份唯一标识,还会判断这个item前后id的一致性

if (mAdapter.hasStableIds()) {
     return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}

都通过了都满足条件了才会复用

如果这个方法返回了null,还会向mViewCacheExtension中去获取

最后,根据viewType到RecyclerPool中去查找

如果RecyclerPool也查找不到就会调用adapter的createViewHolder,去创建一个新的ViewHolder了

holder = mAdapter.createViewHolder(RecyclerView.this, type);

五、总结

  • 在设计一个大型框架的时候,可以使用插拔式的设计模式,增加组件的灵活性
  • 在列表刷新的时候可以使用notifyItemChanged、notifyItemRangeChanged,而不是直接使用notifyDataSetChanged
  • 使用DiffUtil差分异也能提高列表刷新的性能
  • 在使用RecyclerView时尽可能的给它指定固定的宽和高,列表上的item也尽量指定确切的宽和高,这样在RecyclerView的onMeasure方法中,也就是它的测量阶段大大提升测量效率

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十一章 后端编译与优化

文章目录 11.1 概述11.2 即时编译器11.2.1 解释器与编译器11.2.2 编译对象与触发条件11.2.3 编译过程 11.3 提前编译器11.4 编译器优化技术11.4.1 方法内联11.4.2 逃逸分析11.4.3 公共子表达式11.4.4 数组边界检查消除 11.1 概述 如果我们把字节码看作是程序语言的一种中间表示…

哈希应用之位图+布隆过滤器

文章目录 bitset介绍bitset常用函数位图的简单实现布隆过滤器布隆过滤器实现 bitset介绍 在 C 中&#xff0c;std::bitset 是一个标准库提供的类模板&#xff0c;用于表示固定大小的位集合。std::bitset 类模板允许你以一种方便且高效的方式处理位&#xff08;二进制位&#x…

外汇天眼:什么是外汇隔夜利息、滑点和价格跳空?

隔夜利息 所有的外汇交易中只要持仓过夜就一定会有隔夜利息&#xff0c;只不过这个利息有可能是正的&#xff0c;也有可能是负数。 买一个货币涨&#xff0c;相当于我们卖出对应货币&#xff0c;买入基础货币。 买一个货币跌&#xff0c;相当于我们向外汇平台卖出基础货币&a…

定制耐酸碱移液吸头PFA移液枪头可重复使用

移液枪是移液器的一种&#xff0c;常用于实验室少量或微量液体的移取&#xff0c;规格不同&#xff0c;不同规格的移液枪配套使用不同大小的枪头&#xff0c;不同生产厂家生产的形状也略有不同&#xff0c;但工作原理及操作方法基本一致。移液枪属精密仪器&#xff0c;使用及存…

开启鸿蒙开发探索之旅ArkTS基本语法介绍(3)

上一章简单的介绍了鸿蒙HUAWEI DevEco Studio框架的搭建&#xff0c;这一章讲一下鸿蒙的主要开发一眼ArkTS的基本语法结构 1.ArkTS语法解释 ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&…

Redis的主从配置,哨兵模式,集群模式

目录 什么是主从复制&#xff1f; 主从复制的作用&#xff1f; 主从复制的流程&#xff1f; 搭建Redis的主从复制 安装Redis 环境准备 修改内核参数 安装Redis 定义systemd服务管理脚本 修改Redis配置文件&#xff08;Master节点操作&#xff09;192.168.17.25 修改Re…

计算机网络技术-2022期末考试解析

【前言】 这是计算机网络技术这门课&#xff0c;感觉和计网还是有不一样的&#xff0c;但也有能做的&#xff0c;把能做的做了。 一、单项选择题&#xff08;每题2分&#xff0c;共20分&#xff09; 1. 用于测试两台计算机连通状况的命令是 。 ( ) A. cmd B. ping C. ipconf…

(N-137)基于springboot,vue运动会报名管理系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 系统分前后台&#xff0c;项目采用前后端分离 前端技术&#xff1a;vueAvueElementUI 服务端技术&#xff1a;springbootmybatis 本项…

如何利用ChatGPT快速生成月报?

随着每个月的结束&#xff0c;个人和团队经常需要编写月报来回顾和总结。这项任务通常消耗大量时间和精力。幸运的是&#xff0c;借助ChatGPT&#xff0c;这个过程可以变得更加简单和高效。接下来&#xff0c;我将详细介绍如何利用ChatGPT快速生成月报&#xff0c;从而帮助你节…

简易实现 MyBatis 底层机制

MyBatis 大家好呀&#xff01;我是小笙&#xff0c;我中间有1年没有更新文章了&#xff0c;主要忙于毕业和就业相关事情&#xff0c;接下来&#xff0c;我会恢复更新&#xff01;我们一起努力吧&#xff01; 概述 MyBatis 是一个持久层的框架&#xff08;前身是 ibatis&#x…

jmeter--常用插件及服务器监控(14)

一.jmeter插件管理器 下载jmeter插件管理器&#xff1a;plugins-manager.jar 下载plugins-manager.jar并将其放入lib/ext目录&#xff0c;然后重启JMeter。 插件管理界面 打开选项->Plugins Manager&#xff08;界面见下图&#xff09;&#xff0c;“Installed Plugns”…

Github全球第一的免费waf防火墙雷池社区版的语义分析检测算法

传统规则防护&#xff0c;在当下为什么失灵&#xff1f; 当下&#xff0c;Web 应用防火墙大多采用规则匹配方式来识别和阻断攻击流量&#xff0c;但由于 Web 攻击成本低、方式复杂多样、高危漏洞不定期爆发等原因&#xff0c;管理者们在安全运维工作中不得不持续调整防护规则&a…

回顾2023,立2024flag

文章目录 回顾2023与CSDN相识专栏整理数据回顾 立2024flag 回顾2023 在过去的一年里&#xff0c;前端技术不断演进和创新。新技术、新框架层出不穷&#xff0c;给前端工程师提供了更多选择和挑战。2023年已经成为过去&#xff0c;回首这一年&#xff0c;我们也经历了许多挑战和…

@RequestParam

在我们写接口的时候&#xff0c;经常会用到这个注解来标记参数&#xff0c;通过这个注解我们可以把请求的url中的参数名和值映射到被标记的参数上。 比如下方&#xff0c;这个接口是通过传入的参数来查询相关信息的 我们定义这样一个接口&#xff0c;设置了8个参数&#xff0c;…

ChatGPT本地部署,学习记录

一、GPT4ALL模型 官网地址&#xff1a; Github&#xff1a;https://github.com/nomic-ai/gpt4all GPT4ALL项目部署简易&#xff0c;但是在运行体验上一般&#xff0c;并且是只调用CPU来进行运算。 看官方文档介绍在嵌入式上有比较大的优势&#xff0c;但是目前个人对嵌入式…

云贝教育 |【技术文章】存储对象的LIBRARY CACHE LOCK/PIN实验(一)

注: 本文为云贝教育 刘峰 原创&#xff0c;请尊重知识产权&#xff0c;转发请注明出处&#xff0c;不接受任何抄袭、演绎和未经注明出处的转载。 实验环境 操作系统&#xff1a;Red Hat Enterprise Linux release 8.8 (Ootpa) 数据库&#xff1a;oracle Version 19.3.0.0.0 …

Nginx介绍与安装

目录 nginx服务 1、Nginx 介绍 2、为什么选择 nginx 3、IO多路复用 1、I/O multiplexing【多并发】 2、一个请求到来了&#xff0c;nginx使用epoll接收请求的过程是怎样的? 3、异步&#xff0c;非阻塞 4、nginx 的内部技术架构 5、yum安装部署nginx和配置管理 1.获取…

Vue学习笔记六--Vue3学习

1、Vue3的优势 2、创建Vue3工程 前提&#xff1a;node -v 查看node版本&#xff0c;需要在16.0及以上 创建命令 npm init vuelatest,先安装create-vue然后创建项目 然后执行npm run dev 提示 sh: vite: command not found,需要执行npm i重新安装依赖&#xff0c;之后再执行np…

13.若依代码自动生成功能详解

文章目录 1.代码自动生成功能2.功能的使用3. 代码的导出和使用 1.代码自动生成功能 基于若依的目录结构&#xff0c;若依本身提供了代码生成功能&#xff0c;可以根据数据库表的内容&#xff0c;生成一些基本的CRUD的前后端的功能。本文将生成过程中的一些注意事项&#xff0c…

Windows系统设置会议闹钟铃声提醒怎么操作的

在这个快节奏的时代&#xff0c;我们每个人都像是被时间推着走。过去&#xff0c;我经常会因为埋头于工作而忘记了即将召开的会议&#xff0c;直到同事打来电话催促&#xff0c;才匆匆忙忙地赶去会议室。这种尴尬的经历让我意识到&#xff0c;我需要一个能够准时提醒我参加会议…