IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> RecyclerView(四)—— RecyclerView回收和复用机制分析 -> 正文阅读

[移动开发]RecyclerView(四)—— RecyclerView回收和复用机制分析

RecyclerView回收和复用机制分析

1 RecyclerView的刷新回收复用机制

RecyclerViewlayoutView时,都通过回收复用机制来管理。RecyclerView的回收复用机制确实很完善,覆盖到各种场景中,但并不是每种场景的回收复用时都会将机制的所有流程走一遍的。举个例子说,在setLayoutManagersetAdapternotifyDataSetChanged或者滑动时等等这些场景都会触发回收复用机制的工作。但是如果只是 RecyclerView滑动的场景触发的回收复用机制工作时,其实并不需要四级缓存都参与的。

问:假设有一个20itemRecyclerView,每5个占满一个屏幕,在从头滑到尾的过程中,onCreatViewHolder会调用多少次?

RecyclerView的回收复用机制的内部实现都是由Recycler内部类实现,下面就都以这样一种页面的滑动场景来讲解RecyclerView的回收复用机制。

这个页面每行可显示5个卡位,每个卡位的item布局type一致。开始分析回收复用机制之前,先提几个问题:

Q1:如果向下滑动,新一行的5个卡位的显示会去复用缓存的ViewHolder,第1行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?也就是说,新一行的5个卡位复用的ViewHolder有可能是第1行被回收的5个卡位吗?

RecyclerView

黑框表示屏幕,RecyclerView先向下滑动,第3行卡位显示出来,再向上滑动,第3行移出屏幕,第1行显示出来。我们分别在 AdapteronCreateViewHolder()onBindViewHolder()里打日志,下面是这个过程的日志:

日志

红框1RecyclerView向下滑动操作的日志,第35个卡位的显示都是重新创建的ViewHolder;红框2是再次向上滑动时的日志,第15个卡位的重新显示用的ViewHolder都是复用的,因为没有create viewHolder的日志,然后只有后面3个卡位重新绑定数据,调用了onBindViewHolder();那么问题来了:

Q2:在这个过程中,为什么当RecyclerView再次向上滑动重新显示第1行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder()方法,重新绑定数据呢?明明5个卡位都是复用的。

在上面的操作基础上,我们继续往下操作:先向下再向下

RecyclerView

在第二个问题操作的基础上,目前已经创建了15ViewHolder,此时显示的是第12行的卡位,那么继续向下滑动两次,这个过程的日志如下:

日志

红框1是第二个问题操作的日志,在这里截出来只是为了显示接下去的日志是在上面的基础上继续操作的;红框2就是第一次向下滑时的日志,对比问题2的日志,这次第3行的5个卡位用的ViewHolder也都是复用的,而且也只有后面3个卡位触发了onBindViewHolder() 重新绑定数据;红框3是第二次向下滑动时的日志,这次第4行的5个卡位,前3个的卡位用的ViewHolder是复用的,后面2个卡位的ViewHolder则是重新创建的,而且5个卡位都调用了onBindViewHolder()重新绑定数据;

Q3:接下去不管是向上滑动还是向下滑动,滑动几次,都不会再有onCreateViewHolder()的日志了,也就是说RecyclerView总共创建了17ViewHolder,但有时一行的5个卡位只有3个卡位需要重新绑定数据,有时却又5个卡位都需要重新绑定数据,这是为什么呢?

如果明白RecyclerView的回收复用机制,那么这三个问题也就都知道原因了;反过来,如果知道这三个问题的原因,那么理解 RecyclerView的回收复用机制也就更简单了;所以,带着问题,在特定的场景下去分析源码的话,应该会比较容易。

RecyclerView滑动场景下的回收复用涉及到的结构体两个:mCachedViewsRecyclerViewPool

mCachedViews优先级高于RecyclerViewPool,回收时,最新的ViewHolder都是往mCachedViews里放,如果它满了,那就移出一个扔到RecyclerViewPool里好空出位置来缓存最新的ViewHolder

复用时,也是先到mCachedViews里找ViewHolder,但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews里的ViewHolder,如果mCachedViews里没有,那么才去RecyclerViewPool里找。在RecyclerViewPool里的ViewHolder都是跟全新的ViewHolder一样,只要type一样,有找到,就可以拿出来复用,重新绑定下数据即可。

整体的流程图如下:

滑动复用回收

最后,解释一下开头的问题

Q1:如果向下滑动,新一行的5个卡位的显示会去复用缓存的ViewHolder,第1行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?也就是说,新一行的5个卡位复用的ViewHolder有可能是第1行被回收的5个卡位吗?

答:先复用再回收,新一行的5个卡位先去目前的mCachedViewsRecyclerViewPool的缓存中寻找复用,没有就重新创建,然后移出屏幕的那行的5个卡位再回收缓存到mCachedViewsRecyclerViewPool里面,所以新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位。

Q2:在这个过程中,为什么当RecyclerView再次向上滑动重新显示第1行的5个卡位时,只有后面3个卡位触发了onBindViewHolder()方法,重新绑定数据呢?明明5个卡位都是复用的。

答:滑动场景下涉及到的回收和复用的结构体是mCachedViewsRecyclerViewPool,前者默认大小为2,后者为5。所以,当第3行显示出来后,第1行的5个卡位被回收,回收时先缓存在mCachedViews,满了再移出旧的到RecyclerViewPool里,所有5个卡位有2个缓存在mCachedViews里,3个缓存在RecyclerViewPool,至于是哪2个缓存在mCachedViews,这是由LayoutManager控制。上面讲解的例子使用的是GridLayoutManager,滑动时的回收逻辑则是在父类LinearLayoutManager里实现,回收第1行卡位时是从后往前回收,所以最新的两个卡位是01,会放在mCachedViews里,而234的卡位则放在RecyclerViewPool里。

所以,当再次向上滑动时,第15个卡位会去两个结构体里找复用,之前说过,mCachedViews里存放的ViewHolder只有原本位置的卡位才能复用,所以01两个卡位都可以直接去mCachedViews里拿ViewHolder复用,而且这里的ViewHolder是不用重新绑定数据的,至于234卡位则去RecyclerViewPool里找,刚好RecyclerViewPool里缓存着3ViewHolder,所以第1行的5个卡位都是用的复用的,而从RecyclerViewPool里拿的复用需要重新绑定数据,才会这样只有3个卡位需要重新绑定数据。

Q3:接下去不管是向上滑动还是向下滑动,滑动几次,都不会再有onCreateViewHolder()的日志了,也就是说RecyclerView总共创建了17ViewHolder,但有时一行的5个卡位只有3个卡位需要重新绑定数据,有时却又5个卡位都需要重新绑定数据,这是为什么呢?

答:有时一行只有3个卡位需要重新绑定的原因跟Q2一样,因为mCachedView里正好缓存着当前位置的ViewHolder,本来就是它的 ViewHolder当然可以直接拿来用。而至于为什么会创建了17ViewHolder,那是因为再第4行的卡位要显示出来时,RecyclerViewPool里只有3个缓存,而第4行的卡位又用不了mCachedViews里的2个缓存,因为这两个缓存的是67卡位的 ViewHolder,所以就需要再重新创建2ViewHolder来给第4行最后的两个卡位使用。

2 RecyclerView的刷新回收复用机制

notifyXxx后会RecyclerView会进行两次布局,一次预布局,一次实际布局,然后执行动画操作

dispatchLayoutStep1

查找改变holder,并保存在mChangedScrap中;其他未改变的保存到mAttachedScrap中(mChangedScrap保存的holder信息只有预布局时才会被复用)

dispatchLayoutStep2

此步骤会创建一个新的holder并执行绑定数据,充当改变位置的holder,其他位置holdermAttachedScrap中获取

3 RecyclerView为什么要预布局(pre-layout

预布局

列表中有两个表项(12),删除2,此时3会从屏幕底部平滑地移入并占据原来2的位置。

这是怎么做到的?RecyclerView如何知道表项3的动画轨迹?虽然动画的终点已经有了(表项2的顶部),那起点呢?LayoutManager只加载所有可见表项,在删除表项2之前,表项3处于不可见状态,它并不会被layout

对于这种情况RecyclerView的策略是——执行两次layout:为动画前的表项先执行一次pre-layout,将不可见的表项3也加载到布局中,形成一张布局快照(123)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(13)。比对两张快照中表项3的位置,就知道它该如何做动画了。

3.1 预布局生命周期

RecyclerView.onLayout()开始:

public class RecyclerView extends ViewGroup implements ScrollingView,
				NestedScrollingChild2, NestedScrollingChild3 {

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

}

RecyclerView.onLayout()很短,一眼就可以找到其中的关键dispatchLayout():

public class RecyclerView extends ViewGroup implements ScrollingView,
			NestedScrollingChild2, NestedScrollingChild3 {

  void dispatchLayout() {
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
      // 分发布局1
      dispatchLayoutStep1();
      mLayout.setExactMeasureSpecsFrom(this);
      // 分发布局2
      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);
    }
    // 分发布局3
    dispatchLayoutStep3();
  }

}

布局分了三个步骤,从第一步骤开始看:

public class RecyclerView extends ViewGroup implements ScrollingView,
					NestedScrollingChild2, NestedScrollingChild3 {

  private void dispatchLayoutStep1() {
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
  }
            
  public static class State {
    boolean mInPreLayout = false;
  }

}

在分发布局第一步中发现了一个布尔变量mInPreLayout,字面意思是:是否在pre-layout过程中。

找到一点和pre-layout沾边的信息,映入脑壳的问题是:mInPreLayout什么时候被置为true,什么时候又被置为false?,回答这个问题就能知道pre-layout的生命周期了。

全局搜索mInPreLayout被赋值的地方,除了mState.mInPreLayout = mState.mRunPredictiveAnimations;其余都被置为 false。想必mState.mRunPredictiveAnimations一定为true,怎么验证?看看它在哪里被赋值:

public class RecyclerView extends ViewGroup implements ScrollingView,
				NestedScrollingChild2, NestedScrollingChild3 {

  private void processAdapterUpdatesAndSetAnimationFlags() {
    mState.mRunSimpleAnimations = mFirstLayoutComplete
      && mItemAnimator != null
      && (mDataSetHasChangedAfterLayout
          || animationTypeSupported
          || mLayout.mRequestedSimpleAnimations)
      && (!mDataSetHasChangedAfterLayout
          || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
      && animationTypeSupported
      && !mDataSetHasChangedAfterLayout
      && predictiveItemAnimationsEnabled();
  }

}

mRunPredictiveAnimations的值由另外N个布尔变量共同决定,难道我得挨个搜索其他变量才能确定它的值吗?(其实有一个更简单的方法可以验证,下面会提到)

就此打住,mRunPredictiveAnimations的值一定为true,否则mInPreLayout就永远为false了。

继续走查dispatchLayoutStep1()剩余的代码:

public class RecyclerView extends ViewGroup implements ScrollingView,
					NestedScrollingChild2, NestedScrollingChild3 {

  private void dispatchLayoutStep1() {
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    if (mState.mRunSimpleAnimations) {
      mLayout.onLayoutChildren(mRecycler, mState);
    }
  }

}

发现了一个很关键的方法LayoutManager.onLayoutChildren(),它有很长的注释,大意是“该方法用于布局Adapter中所有的表项。若支持表项动画,则onLayoutChildren()会被调用2次,第一次称为pre-layout,它是真正布局表项之前的一次预布局。”

搜索LayoutManager.onLayoutChildren()被调用的地方,只有两处,一次在RecyclerView.dispatchLayoutStep1()中,另一次在RecyclerView.dispatchLayoutStep2()

public class RecyclerView extends ViewGroup implements ScrollingView,
				NestedScrollingChild2, NestedScrollingChild3 {

  private void dispatchLayoutStep2() {
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);
  }

}

布局的第二步中,调用onLayoutChildren()前,把mInPreLayout置为了 false,pre-layout就此结束。

而且mState作为参数被传入onLayoutChildren(),在onLayoutChildren()中一定会读取mInPreLayout

看到这里,结合注释和代码走查,可以下一些结论:

  • RecyclerView为了实现表项动画,进行了2次布局,第一次预布局,第二次正真的布局,在源码上表现为LayoutManager.onLayoutChildren()被调用2
  • mState.mInPreLayout的值标记了预布局的生命周期。预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()。两次调用LayoutManager.onLayoutChildren()会因为这个标记位的不同而执行不同的逻辑分支。

3.2 预布局填充额外表项

知道了预布局的起点和终点,就为走查代码缩小了范围。只需要定位在LinearLayoutManager.onLayoutChildren()中,就可以了解预布局做了些什么。

预布局一定做了很多事情,但现在最关心的是“预布局过程中,如何将额外的不可见表项填充进来?”

RecyclerView缓存机制(咋复用?)中讲述了怎么在源码中一步步找到 “填充表项” 的逻辑,这段逻辑正好就在onLayoutChildren()中,引用如下:

public class LinearLayoutManager {
    // 布局表项
    public void onLayoutChildren() {
        // 填充表项
        fill() {
            while(列表有剩余空间){
                // 填充单个表项
                layoutChunk(){
                    // 让表项成为子视图
                    addView(view)
                }
            }
        }
    }
}

RecyclerView将布局表项的任务委托给LinearLayoutManagerLinearLayoutManager布局表项时,在fill()方法中循环不断地调用layoutChunk()逐个将表项填入,直到列表没有空间。

对于填充表项,fill()layoutChunk()是两个关键方法,添加额外表项的逻辑肯定藏在其中:

public class LinearLayoutManager {
    // 根据剩余空间填充表项
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 计算剩余空间 = 可用空间 + 额外空间
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 当剩余空间 > 0 时,继续填充更多表项
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            layoutChunk()
            ...
        }
    }
}

LinearLayoutManager在循环填充表项前会计算剩余空间,计算公式中的mExtraFillSpace引起了我的注意,它和我关心的问题“额外表项”很匹配,心想 “在 pre-layout 过程中可能是mExtraFillSpace增大,放宽了循环条件,使得额外表项被填充。” 于是乎,我开始搜索它被赋值的地方,结果显示有 11 处(有点多,好慌):

LinearLayoutManager

大部分的赋值都发生在onLayoutChildren()中:

if (mAnchorInfo.mLayoutFromEnd) {// 从尾部开始布局
    mLayoutState.mExtraFillSpace = extraForStart;
} else {// 从头部开始布局
    mLayoutState.mExtraFillSpace = extraForEnd;
}

而且它们分别处于不同的方向分支中,即对于一种方向的列表只有一个赋值语句被执行,随便找了一个mLayoutState.mExtraFillSpace = extraForEnd;,继续搜索extraForEnd被赋值的地方:

public class LinearLayoutManager {
    private int[] mReusableIntPair = new int[2];
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        calculateExtraLayoutSpace(state, mReusableIntPair); // 计算值
        int extraForEnd = Math.max(0, mReusableIntPair[1]) // 赋值
    }
}

extraForEnd的值和mReusableIntPair[1]有关,而它在calculateExtraLayoutSpace()中被计算,继续跳转:

public class LinearLayoutManager {
    // 计算额外空间
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,@NonNull int[] extraLayoutSpace) {
        int extraLayoutSpaceStart = 0;
        int extraLayoutSpaceEnd = 0;

        int extraScrollSpace = getExtraLayoutSpace(state);// 计算值
        if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            extraLayoutSpaceStart = extraScrollSpace;
        } else {
            extraLayoutSpaceEnd = extraScrollSpace;
        }

        extraLayoutSpace[0] = extraLayoutSpaceStart;
        extraLayoutSpace[1] = extraLayoutSpaceEnd;// 赋值
    }
}

calculateExtraLayoutSpace()这个方法名让我更加坚信这条路没错(额外表项对应着额外空间)。

在这个方法中又调用了getExtraLayoutSpace()并将结果赋值给extraLayoutSpace[1],继续跳:

public class LinearLayoutManager {
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        if (state.hasTargetScrollPosition()) {
            return mOrientationHelper.getTotalSpace();
        } else {
            return 0;
        }
    }
}

方法要么返回 0 要么返回mOrientationHelper.getTotalSpace(),我更愿意相信后者,因为只有返回非0值才能证实猜想。为了验证,我还得跳一次:

public class RecyclerView {
    public static class State {
        public boolean hasTargetScrollPosition() {
            return mTargetPosition != RecyclerView.NO_POSITION;
        }
    }
}

看到这,我陷入了迷茫,因为删除表项操作并不会发生列表滚动,即hasTargetScrollPosition()应该返回 false,也就说返回额外空间的方法getExtraLayoutSpace()应该返回0。我无法接受这个事实。。。

难道列表发生滚动了?

怎么证明滚动了?

继续搜索 mTargetPosition 被赋值的地方?

不。。。我已经跳不动了。。

硬生生地看了一下午源码,也没有看到想要的结果,更致命的是硬看很容易钻牛角尖,有限的生命就耗费在这无穷的细节中。

想知道某个变量的值,最快的办法是断点调试,它也可以用到阅读源码上。写了一个简单的 Demo 模拟删除表项的场景,将断点打在计算剩余空间那一行:

public class LinearLayoutManager {
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 计算剩余空间 = 现有空间 + 额外空间(断点)
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            layoutChunk()
            ...
        }
    }
}

断点告诉我layoutState.mExtraFillSpace的确为0!

layoutState.mAvailable的值是否在pre-layout过程中变大?断点告诉我没有!

循环条件没有放宽!那额外的表项是如何被填充的?

我将断点打在了循环条件while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state))上,惊喜地发现了一个新的线索:在正常布局表项时,当第二个表项被填充后remainingSpace就等于0了,但同样的情况在 pre-layout 阶段,remainingSpace就不为0,这导致循环可以多走一次,即可以将表项 3 填充进来。

每次循环填充表项后remainingSpace的值应该变小,难道填充被删除的表项时跳过了这个步骤?

又到了硬看源码发挥作用的时刻:

public class LinearLayoutManager {
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 填充表项结果
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            // 填充单个表项(将layoutChunkResult传入)
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null|| !state.isPreLayout()) {
                // 在剩余空间中扣除刚填充表项消耗的空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }
        }
    }
}

循环中唯一一处扣除剩余空间的代码被一个条件表达式包裹着,表达式中有三个条件做或运算,其中一个条件!state.isPreLayout()对于非pre-layout阶段来说肯定为 true,即无论其他条件如何,非pre-layout阶段一定会扣除所有表项消耗的空间,而对于pre-layout来说,填充某些表项时,可能会跳过扣除。哪些表项会跳过?

***条件表达式中有一个变量 layoutChunkResult.mIgnoreConsumed,字面意思是忽略这次消耗,而且layoutChunkResult被作为参数传入layoutChunk()***:

public class LinearLayoutManager {
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 获取下一个该被填充的表项视图
        View view = layoutState.next(recycler);
        ...
        // 获取表项布局参数
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        // 如果表项被移除 则 mIgnoreConsumed 置为 true
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        ...
    }
}

看到这里感觉八九不离十了,用断点调试验证了,的确和猜想的一样:在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。

why

这种负责执行动画的View在原布局或新布局中不存在的动画,就是预测动画。

因为RecyclerView 要执行预测动画。比如有A,B,C三个itemView,其中A和B被加载到屏幕上,这时候删除B后, 按照最终效果我们会看到C移动到B的位置;因为我们只知道 C 最终的位置,但是不知道 C 的起始位置在哪里(即C还 未被加载)。

用户有 A、B、C 三个 item,A,B 刚好显示在屏幕中,这个时候,用户把 B 删除了,那么最终 C 会显示在 B 原 来的位置

因为我们只知道 C 最终的位置,但是不知道 C 的起始位置在哪里,无法确定 C 应该从哪里滑动过来。 在其他 LayoutManager 中,它可能是从侧面或者是其他地方滑动过来的。

what

当 Adapter 发生变化的时候,RecyclerView 会让 LayoutManager 进行两次布局。

第一次,预布局,为动画前的表项先执行一次pre-layout,根据 Adapter 的 notify 信息,我们知道哪些 item 即将 变化了,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。

第二次,实际布局,也就是变化完成之后的布局同样形成一张布局快照(1、3)。 这样只要比较前后布局的变化,就能得出应该执行什么动画了,就称为预测动画。

4 ListViewRecyclerView区别

1.布局效果

`ListView 的布局比较单一,只有一个纵向效果; RecyclerView 的布局效果丰富, 可以在 LayoutMananger 中 设置:线性布局(纵向,横向),表格布局,瀑布流布局

2.局部刷新

RecyclerView中可以实现局部刷新,例如:notifyItemChanged();

如果要在ListView实现局部刷新,依然是可以实现的,当一个item数据刷新时,我们可以在Adapter中,实现一 个notifyItemChanged()方法,在方法里面通过这个 item 的 position,刷新这个item的数据

3.缓存区别
ListView有两级缓存,在屏幕与非屏幕内。 RecyclerView比ListView多两级缓存 ListView缓存View。

RecyclerView缓存RecyclerView.ViewHolder

5 RecyclerView性能优化

1.数据处理与视图加载分离

简单来说就是在onBindViewHolder()只设置UI显示,不做任何逻辑判断,需要的业务逻辑在得到javabean之前 处理好,

2.布局优化
减少过渡绘制 减少布局层级 3.设置RecyclerView.addOnScrollListener()来在滑动过程中停止加载的操作。

参考

https://www.jianshu.com/p/467ae8a7ca6e

https://www.pianshen.com/article/73691937375/

https://juejin.cn/post/6890288761783975950#heading-0

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-26 08:57:51  更:2021-11-26 08:59:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:33:00-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码