美文网首页
RecyclerView.Adapter根据数据变化更新UI原理

RecyclerView.Adapter根据数据变化更新UI原理

作者: 就叫汉堡吧 | 来源:发表于2022-02-27 08:03 被阅读0次
    • 概述

      当列表数据发生变化时,需要调用Adapter中的一些API来通知RecyclerView改变UI,比如列表刷新、添加、移除、部分数据项改变等,在这些数据更改时需要告诉RecyclerView从而做出改变,本文会根据常用数据更新场景来分析一下是怎么做到的。

      在开始之前,我们要确定一个规范,那就是一定要是数据驱动UI,在onBindViewHolder中进行数据绑定的时候把UI的所有组件的状态和对应数据段对应在一起,然后当数据改变时,我们再调用相关API通知数据变化,这时会重新走onBindViewHolder方法给对应组件重新赋值,这就保证了数据源一致性,切不可在onCreateViewHolder或者onBindViewHolder中根据不同情况动态的设置组件状态,不然就会导致多个地方可以修改组建,造成难以捕捉的混乱错误。

      1. notifyDataSetChanged方法,在所有数据项替换时需要调用,比如刷新操作,当然,所有情况的数据改变你都可以通过这个方法更新UI,但是为了某几项的数据改变去更新所有项显然会影响性能,所以这才有了下面的这些方法,用于更新一部分项;
      2. notifyItemChanged方法,在某一项数据改变时调用;
      3. notifyItemRangeChanged方法,在连续的一部分项数据改变时调用;
      4. notifyItemInserted方法,在插入某一项数据时调用;
      5. notifyItemMoved方法,在移动某一项数据的位置时调用,传入一个当前位置参数和一个目标位置参数;
      6. notifyItemRangeInserted方法,在插入连续的一部分项的数据时调用;
      7. notifyItemRemoved方法,在移除某一项数据时调用;
      8. notifyItemRangeRemoved方法,在移除连续的一部分项数据时调用。

      上述这些方法内部都是调用了mObservable的同名方法,mObservable是什么呢?它在创建Adapter实例时自动创建:

      private final AdapterDataObservable mObservable = new AdapterDataObservable();
      

      AdapterDataObservable继承自Observable,mObservers中存放了多个AdapterDataObserver,上述的每个方法都会调用下面的循环代码去调用每一个AdapterDataObserver的同名方法:

      for (int i = mObservers.size() - 1; i >= 0; i--) {
          mObservers.get(i).onXxxx(...);
      }
      

      mObservers是通过Observable的registerObserver方法注册的,这个方法在Adapter的registerAdapterDataObserver中调用,在setAdapter流程中会调用这个方法注册:

      adapter.registerAdapterDataObserver(mObserver);
      

      mObserver是在RecyclerView实例化的时候自动创建的:

      private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
      

      在RecyclerViewDataObserver中,和AdapterDataObservable类似,每个方法都是调用了mAdapterHelper的同名方法:

      assertNotInLayoutOrScroll(null);
      if (mAdapterHelper.onXxx(...)) {
          triggerUpdateProcessor();
      }
      

      triggerUpdateProcessor方法就是通知View树重新layout:

      void triggerUpdateProcessor() {
          if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
              ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
          } else {
              mAdapterUpdateDuringMeasure = true;
              requestLayout();
          }
      }
      

      特殊的notifyDataSetChanged调用的是onChanged方法:

      @Override
      public void onChanged() {
          assertNotInLayoutOrScroll(null);
          mState.mStructureChanged = true;
      
          processDataSetCompletelyChanged(true);
          if (!mAdapterHelper.hasPendingUpdates()) {
              requestLayout();
          }
      }
      

      下面逐一分析mAdapterHelper的这些方法。

    • 全部更新 — processDataSetCompletelyChanged

      根据上面的梳理,我们知道notifyDataSetChanged方法调用最后会调用RecyclerViewDataObserver的onChange方法,这里面会调用processDataSetCompletelyChanged方法:

      void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
          mDispatchItemsChangedEvent |= dispatchItemsChanged;
          mDataSetHasChangedAfterLayout = true;
          markKnownViewsInvalid();
      }
      

      这里调用了markKnownViewsInvalid方法:

      void markKnownViewsInvalid() {
          final int childCount = mChildHelper.getUnfilteredChildCount();
          for (int i = 0; i < childCount; i++) {
              final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
              if (holder != null && !holder.shouldIgnore()) {
                  holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
              }
          }
          markItemDecorInsetsDirty();
          mRecycler.markKnownViewsInvalid();
      }
      

      mChildHelper.getUnfilteredChildCount得到的是所有已经attach到RecyclerView的ViewHolder,包括隐藏的,这里会给所有attach的ViewHolder设置FLAG_UPDATE和FLAG_INVALID属性,markItemDecorInsetsDirty方法会把这些ViewHolder的itemView的decorInset(即padding这些)设置为dirty,markKnownViewsInvalid会清除mCacheViews并且把它里面的ViewHolder放到mScrapHeap中去。

      待到requestLayout触发新数据布局的时候走到tryGetViewHolderForPositionByDeadline方法中,因为mCacheViews中清空了,所以此时只有getRecycledViewPool().getRecycledView(type)取到的mScrapHeap中有之前缓存的ViewHolder,还记得它的默认大小是5吗,所以滚动显示前5个表项的时候是不会走onCreateViewHolder的,注意这里的5个会包括可以在屏幕中显示的,因为重新设置新的数据内容,相当于所有的表项都会销毁,所以当前在屏幕中的也会被缓存,而滚动的时候则只有在超出屏幕某个范围之后才会缓存。

      不管是不是重新构造的ViewHolder,都会走onBindViewHolder方法,因为之前markKnownViewsInvalid中把当时所有attach的View都会添加FLAG_UPDATE和FLAG_INVALID标志,而在tryGetViewHolderForPositionByDeadline中会根据这些标志调用onBindViewHolder:

      if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
          final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
      }
      
    • 部分更新

      boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
          if (itemCount < 1) {
              return false;
          }
          mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
          mExistingUpdateTypes |= UpdateOp.UPDATE;
          return mPendingUpdates.size() == 1;
      }
      

      Adapter中的notifyItemChanged、notifyItemRangeChanged其实都是走的这个方法,只不过notifyItemChanged传入的itemCount固定为1。

      obtainUpdateOp方法如下:

      @Override
      public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
          UpdateOp op = mUpdateOpPool.acquire();
          if (op == null) {
              op = new UpdateOp(cmd, positionStart, itemCount, payload);
          } else {
              op.cmd = cmd;
              op.positionStart = positionStart;
              op.itemCount = itemCount;
              op.payload = payload;
          }
          return op;
      }
      

      现在我们知道了onItemRangeChanged其实就是在mPendingUpdates中添加一个UpdateOp。

      其他的onItemRangeInserted、onItemRangeRemoved和onItemRangeMoved都是调用obtainUpdateOp在mPendingUpdates中添加一个UpdateOp,但是UpdateOp的cmd属性分别设置为UpdateOp.ADD、UpdateOp.REMOVE、UpdateOp.MOVE。

      然后调用requestLayout之后就会调用onLayout方法,onLayout中调用了dispatchLayout方法,部分代码如下:

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

      在dispatchLayoutStep3中一开始会把dispatchLayoutStep3置为State.STEP_START,它的默认值也是State.STEP_START,所以在开始一次全新的layout的时候一定会是State.STEP_START的,那么就会调用dispatchLayoutStep1方法,这个方法中会调用processAdapterUpdatesAndSetAnimationFlags方法,里面有一段:

      if (predictiveItemAnimationsEnabled()) {
          mAdapterHelper.preProcess();
      } else {
          mAdapterHelper.consumeUpdatesInOnePass();
      }
      

      predictiveItemAnimationsEnabled会决定布局是否支持动画更新:

      private boolean predictiveItemAnimationsEnabled() {
          return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
      }
      

      mItemAnimator会自动初始化为DefaultItemAnimator,所以一定不为null,所以这里取决于LayoutManager的supportsPredictiveItemAnimations方法返回,默认返回false,LinearLayoutManager中重写了它:

      @Override
      public boolean supportsPredictiveItemAnimations() {
          return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd;
      }
      

      mPendingSavedState在onRestoreInstanceState恢复中才会赋值,在onLayoutCompleted布局完成中会置为null,那这里的意思就是在界面恢复过程中不需要展示动画,可以理解,因为界面恢复工作是为了默默地恢复之前的界面,最好不要让用户感知到,加上动画是此地无银了。还有一个条件就是布局方向和上一次相比没有发生变化,比如上次是从上而下的顺序,插入一个表项之后改成从下往上填充了,那这种情况也不会加上动画。

      界面恢复和临时更改布局方向都是很少出现的情况,所以默认都是会走preProcess逻辑,但是这里consumeUpdatesInOnePass的逻辑我也会列出来,我们先看preProcess。

    • mAdapterHelper.preProcess()

      void preProcess() {
          mOpReorderer.reorderOps(mPendingUpdates);
          final int count = mPendingUpdates.size();
          for (int i = 0; i < count; i++) {
              UpdateOp op = mPendingUpdates.get(i);
              switch (op.cmd) {
                  case UpdateOp.ADD:
                      applyAdd(op);
                      break;
                  case UpdateOp.REMOVE:
                      applyRemove(op);
                      break;
                  case UpdateOp.UPDATE:
                      applyUpdate(op);
                      break;
                  case UpdateOp.MOVE:
                      applyMove(op);
                      break;
              }
              if (mOnItemProcessedCallback != null) {
                  mOnItemProcessedCallback.run();
              }
          }
          mPendingUpdates.clear();
      }
      

      这个方法中会根据不同操作调用不同方法。

      • 添加操作 — applyAdd方法

        private void applyAdd(UpdateOp op) {
            postponeAndUpdateViewHolders(op);
        }
        

        这里直接调用了postponeAndUpdateViewHolders方法:

        private void postponeAndUpdateViewHolders(UpdateOp op) {
            if (DEBUG) {
                Log.d(TAG, "postponing " + op);
            }
            mPostponedList.add(op);
            switch (op.cmd) {
                case UpdateOp.ADD:
                    mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                    break;
                case UpdateOp.MOVE:
                    mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                    break;
                case UpdateOp.REMOVE:
                    mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                            op.itemCount);
                    break;
                case UpdateOp.UPDATE:
                    mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown update op type for " + op);
            }
        }
        

        当UpdateOp的cmd属性是UpdateOp.ADD的时候会调用Callback.offsetPositionsForAdd(op.positionStart, op.itemCount)方法,这个方法中会调用offsetPositionRecordsForInsert方法:

      void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
      final int childCount = mChildHelper.getUnfilteredChildCount();
      for (int i = 0; i < childCount; i++) {
      final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
      if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
      if (DEBUG) {
      Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder "
      + holder + " now at position " + (holder.mPosition + itemCount));
      }
      holder.offsetPosition(itemCount, false);
      mState.mStructureChanged = true;
      }
      }
      mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
      requestLayout();
      }

      
      这个方法做了两件事,一件是通过getUnfilteredChildAt方法循环获取已经attach的ViewHolder,凡是holder.mPosition大于positionStart的都是在插入元素之后的表项,offsetPosition把这些ViewHolder的mPosition属性都在原来基础上加上itemCount,从而达到在插入元素之后的表项同步位移的效果;另一件事就是调用Recycler的offsetPositionRecordsForInsert方法修改mCacheViews中的ViewHolder的mPosition属性,也就是同步操作,**注意,这里并没有同步其他的缓存集合,所以那些缓存集合中的mPosition和修改后的位置就不匹配了,这就会导致那些缓存失效**。
      
      offsetPosition里修改mPosition代码:**mPosition += offset**。
      
      其他的移动、删除、更新操作的原理都和这里的添加操作同理,下面就不多说了,只是由于它们的操作的目的不同,所以偏移的算法会不一样,下面只会分析它们自己的偏移算法。
      
      + ### 移动操作 — applyMove方法
      
      applyMove方法也是直接调用了postponeAndUpdateViewHolders方法,同理move操作会调用RecyclerView的offsetPositionRecordsForMove方法:
      
      ```java
      void offsetPositionRecordsForMove(int from, int to) {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        final int start, end, inBetweenOffset;
          //start永远是position更小的那个,而end会是更大的那个
          //inBetweenOffset标志着偏移方向,上移就是减1,下移就是加1
        if (from < to) {
            start = from;
            end = to;
            inBetweenOffset = -1;
        } else {
            start = to;
            end = from;
          inBetweenOffset = 1;
        }
      
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
              //根据start和end锁定偏移范围,mPosition小于start和大于end的表项不移动
            if (holder == null || holder.mPosition < start || holder.mPosition > end) {
                continue;
            }
            if (DEBUG) {
                Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder "
                        + holder);
            }
            //找到当前需要移动的目标ViewHolder了,to - from直接算出带方向的偏移距离,mPosition加上这个值就是偏移后的下标
            if (holder.mPosition == from) {
                holder.offsetPosition(to - from, false);
            } else {
                  //start和end之间的表项位移
              holder.offsetPosition(inBetweenOffset, false);
            }
      
            mState.mStructureChanged = true;
        }
      mRecycler.offsetPositionRecordsForMove(from, to);
        requestLayout();
      }
      

      Recycler的offsetPositionRecordsForMove方法里面和这里的算法一样,只不过它的操作对象是mCacheView。

      • 更新操作 — applyUpdate方法

        applyUpdate方法里会计算出attach的ViewHolder中可见的那些的偏移位置,然后调用postponeAndUpdateViewHolders方法,最后会调用到RecyclerView的viewRangeUpdate方法:

        void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
          final int childCount = mChildHelper.getUnfilteredChildCount();
          final int positionEnd = positionStart + itemCount;
        
          for (int i = 0; i < childCount; i++) {
              final View child = mChildHelper.getUnfilteredChildAt(i);
              final ViewHolder holder = getChildViewHolderInt(child);
              if (holder == null || holder.shouldIgnore()) {
                  continue;
              }
              if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
                  // We re-bind these view holders after pre-processing is complete so that
                  // ViewHolders have their final positions assigned.
                  holder.addFlags(ViewHolder.FLAG_UPDATE);
                  holder.addChangePayload(payload);
                  // lp cannot be null since we get ViewHolder from it.
                  ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
              }
          }
          mRecycler.viewRangeUpdate(positionStart, itemCount);
        

      }

      
      这个方法很简单,就是给attach的ViewHolder添加FLAG_UPDATE标签。
      
      + ### 移除操作 — applyRemove方法
      
      这个方法里同样会计算出在要移除的范围内,attach的ViewHolder中可见的那些的偏移位置。然后调用postponeAndUpdateViewHolders方法,最后会调用到RecyclerView的offsetPositionRecordsForRemove方法:
      
      ```java
      void offsetPositionRecordsForRemove(int positionStart, int itemCount,
              boolean applyToPreLayout) {
          final int positionEnd = positionStart + itemCount;
          final int childCount = mChildHelper.getUnfilteredChildCount();
          for (int i = 0; i < childCount; i++) {
              final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
              if (holder != null && !holder.shouldIgnore()) {
                  if (holder.mPosition >= positionEnd) {
                      if (DEBUG) {
                          Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
                                  + " holder " + holder + " now at position "
                                  + (holder.mPosition - itemCount));
                      }
                      holder.offsetPosition(-itemCount, applyToPreLayout);
                      mState.mStructureChanged = true;
                  } else if (holder.mPosition >= positionStart) {
                      if (DEBUG) {
                          Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
                                  + " holder " + holder + " now REMOVED");
                      }
                      holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
                              applyToPreLayout);
                      mState.mStructureChanged = true;
                  }
              }
          }
          mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
          requestLayout();
      }
      

      flagRemovedAndOffsetPosition方法如下:

      void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
          addFlags(ViewHolder.FLAG_REMOVED);
          offsetPosition(offset, applyToPreLayout);
          mPosition = mNewPosition;
      }
      

      注意,这里添加了FLAG_REMOVED标签,这里调用的offsetPosition方法中:

      if (applyToPreLayout) {
          mPreLayoutPosition += offset;
      }
      mPosition += offset;
      

      因为这里的applyToPreLayout传进来的是false,所以mPreLayoutPosition不会同步到最新位置,还是保留之前表项的存在位置,这很关键。mPosition会被置为新的移除后的位置,也就是移除的第一个表项的前一个位置。

    • mAdapterHelper.consumeUpdatesInOnePass方法

      void consumeUpdatesInOnePass() {
          // we still consume postponed updates (if there is) in case there was a pre-process call
          // w/o a matching consumePostponedUpdates.
          consumePostponedUpdates();
          final int count = mPendingUpdates.size();
          for (int i = 0; i < count; i++) {
              UpdateOp op = mPendingUpdates.get(i);
              switch (op.cmd) {
                  case UpdateOp.ADD:
                      mCallback.onDispatchSecondPass(op);
                      mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                      break;
                  case UpdateOp.REMOVE:
                      mCallback.onDispatchSecondPass(op);
                      mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                      break;
                  case UpdateOp.UPDATE:
                      mCallback.onDispatchSecondPass(op);
                      mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                      break;
                  case UpdateOp.MOVE:
                      mCallback.onDispatchSecondPass(op);
                      mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                      break;
              }
              if (mOnItemProcessedCallback != null) {
                  mOnItemProcessedCallback.run();
              }
          }
          recycleUpdateOpsAndClearList(mPendingUpdates);
          mExistingUpdateTypes = 0;
      }
      

      如果在上述调用dispatchLayoutStep2流程中调用的这个方法,因为mPendingUpdates在preProcess中clear了,所以只会走consumePostponedUpdates方法,里面会循环mPostponedList逐个调用mCallback.onDispatchSecondPass方法,mCallback在构造AdapterHelper的时候构造,mCallback.onDispatchSecondPass方法最终调用的是它的dispatchUpdate方法:

      void dispatchUpdate(AdapterHelper.UpdateOp op) {
          switch (op.cmd) {
              case AdapterHelper.UpdateOp.ADD:
                  mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
                  break;
              case AdapterHelper.UpdateOp.REMOVE:
                  mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
                  break;
              case AdapterHelper.UpdateOp.UPDATE:
                  mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
                          op.payload);
                  break;
              case AdapterHelper.UpdateOp.MOVE:
                  mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
                  break;
          }
      }
      

      发现LinearLayoutManager中并没有实现父类的这些方法,所以这里其实就是给自定义回调处理提供入口而已。

      如果是在dispatchLayoutStep1中调用的这个方法,会根据UpdateOp的cmd调用不同的方法,这和上面的preProcess流程调用的mCallback的方法大部分是完全相同的,只不过remove的时候有所不同,这里remove操作最后调用的也是offsetPositionsForRemovingInvisible方法,只不过这里传入的applyToPreLayout方法是true,所以这里的mPreLayoutPosition也会同步到最新位置,不过暂时mPreLayoutPosition这个值还没发现哪里用到了。

    • 刷新UI

      以上在dispatchLayoutStep1中,preProcess一个主要的工作就是把我们调用Adapter.notifyDataXxx流程中保存的mPendingUpdates给保存到了mPostponedList中,mPendingUpdates的作用主要是用于在preProcess中设置FLAG,但是对我们这里的原理其实帮助不大,可以把它理解成一个摆渡者,把更新信息摆渡给mPostponedList。

      在dispatchLayoutStep2流程中,我们最终会调用tryGetViewHolderForPositionByDeadline方法,在这个方法中有这么一段:

      if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
          final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
      }
      

      这里会通过mAdapterHelper的findPositionOffset方法计算出最新数据的位置:

      int findPositionOffset(int position, int firstPostponedItem) {
          int count = mPostponedList.size();
          for (int i = firstPostponedItem; i < count; ++i) {
              UpdateOp op = mPostponedList.get(i);
              if (op.cmd == UpdateOp.MOVE) {
                  if (op.positionStart == position) {
                      position = op.itemCount;
                  } else {
                      if (op.positionStart < position) {
                          position--; // like a remove
                      }
                      if (op.itemCount <= position) {
                          position++; // like an add
                      }
                  }
              } else if (op.positionStart <= position) {
                  if (op.cmd == UpdateOp.REMOVE) {
                      if (position < op.positionStart + op.itemCount) {
                          return -1;
                      }
                      position -= op.itemCount;
                  } else if (op.cmd == UpdateOp.ADD) {
                      position += op.itemCount;
                  }
              }
          }
          return position;
      }
      

      然后调用tryBindViewHolderByDeadline方法回调到bindViewHolder方法完成新位置的数据更新,之后呢,就交给fill方法进行布局和偏移了。

    • 总结

      纵上可知,数据变化触发RecyclerView更新UI的原理如下:

      1. Adapter中维护了一个AdapterDataObservable实例,可以通过它添加多个响应数据变化的监听器,那么我们这里在setAdapter时(setAdapterInternal中)会设置一个监听器,叫做RecyclerViewDataObserver,调用Adapter的更新数据的方法后,AdapterDataObservable会通知所有监听器数据改变,这里就会调用到RecyclerViewDataObserver的对应方法。

      2. RecyclerViewDataObserver的相关方法中,按照逻辑途径来说有两类操作,一类是全部数据发生改变时调用的notifyDataSetChanged方法的逻辑;另一个是添加、移除、部分change或者移动这些部分更新的逻辑。前者会直接遍历所有目前已经attach的ViewHolder,调用addFlags方法添加ViewHolder.FLAG_UPDATE和ViewHolder.FLAG_INVALI标签;后者会调用AdapterHelper的相关方法来进行处理。但其实最终的最终都是修改ViewHolder的flags。

      3. AdapterHelper中维护了一个ArrayList<UpdateOp>,叫做mPendingUpdates,这里的对应方法中会构造一个新的UpdateOp或者从pool中取出一个复用,设置UpdateOp的相关属性,其中cmd表示操作类型,有ADD、REMOVE、UPDATE、MOVE四种,positionStart表示要改动的起始位置,itemCount表示从起始位置开始需要修改的表项个数,最后会把UpdateOp对象添加到mPendingUpdates中,mExistingUpdateTypes会通过或操作把下次布局时需要执行的操作类型记录,在合适的时机会调用hasAnyUpdateTypes查看是否含有需要执行的操作类型:

        boolean hasAnyUpdateTypes(int updateTypes) {
            return (mExistingUpdateTypes & updateTypes) != 0;
        }
        

        其实可以通过遍历mPendingUpdates达到一样的效果,这里主要是为了性能考虑。

      4. mPendingUpdates中保存的都是需要更新的ViewHolder,接着会调用requestLayout方法。

      5. dispatchLayoutStep1方法中会调用processAdapterUpdatesAndSetAnimationFlags方法,这里会调用preProcess方法,这个方法首先会把这些UpdateOp保存到mPostponedList中,然后会根据mPendingUpdates中的UpdateOp修改对应的attach的ViewHolder。

      6. notifyDataSetChanged逻辑因为直接修改的FLAG,没有操作mPendingUpdates,所以调用了preProcess也不会发生任何操作。

      7. 然后调用dispatchLayoutStep2方法进行fill布局,在tryGetViewHolderForPositionByDeadline中会从回收区中尝试拿回尚未丢弃的holder,这里其实和ViewHolder是什么样的没有关系,关键的是bindViewHolder方法的position参数是什么,这里会通过mAdapterHelper.findPositionOffset(position)方法遍历mPostponedList,找到对应的UpdateOp,根据它修改当前的position,然后传给bindViewHolder,从而达到数据更新的目的。

      总之,其实中心思想就是给bindViewHolder方法的position找最新的值,最核心的关键就是先保存要修改的position信息,然后在布局的时候根据它去数据集里获取针对这个位置的数据,从而达到更新的效果。ViewHolder拿到了能用就复用,不能用就创建新的,其实preProcess里面那么繁琐的修改,主要是为了之后根据FLAG复用ViewHolder。

    相关文章

      网友评论

          本文标题:RecyclerView.Adapter根据数据变化更新UI原理

          本文链接:https://www.haomeiwen.com/subject/qtdqrrtx.html