熟悉RecyclerView
的同学应该都知道,Adapter
作为RecyclerView
四大组成部分(Adapter
,LayoutManager
,ItemAnimator
,ItemDecoration
)之一,其重要性自然是不言而喻。今天,我们来分析一下Adapter
的源码。我打算将Adapter
的源码分析分为两个部分,一是,从普通的角度上来看Adapter
,从源码的角度上来分析我们日常使用的一些操作;二是,分析DiffUtil
,可能会涉及到Adapter
的部分源码。所以Adapter
源码分析分为两篇,本文是第一篇。
1. 概述
在分析Adapter
源码之前,我们先来回顾一下,我们经常使用的几个方法。
方法名 | 作用 |
---|---|
onCreateViewHolder | 创建一个ViewHolder对象,主要作用是将数据保存在ViewHolder,以供后面bind操作使用 |
onBindViewHolder | 数据绑定方法 |
getItemCount | 当前Adapter拥有数据的数量,该方法必须被重写,否则RecyclerView 展示不了任何数据 |
getItemViewType | 该方法带一个Position,主要是返回当前位置的ViewType。这个方法通常用于一个RecyclerView 需要加载不同的布局。 |
getItemId | 该方法表示的意思是返回当前位置Item的id,此方法只在setHasStableIds 设置为true才会生效 |
setHasStableIds | 设置当前RecyclerView 的ItemView 是否拥有固定id,跟getItemId 方法一起使用。如果设置为true,会提高RecyclerView 的缓存效率。 |
上表中所列的方法应该就是我们使用Adapter
经常使用的方法,接下来,我将正式分析Adapter
的相关代码。我打算从如下角度来分析:
重新从
RecyclerView
缓存角度来分析onCreateViewHolder
和onBindViewHolder
。
onBindViewHolder
的一个重载方法--主要是用于局部刷新。结合
Adapter
,分析ViewHolder
的position。
1. onCreateViewHolder和onBindViewHolder
onCreateViewHolder
方法和onBindViewHolder
方法算是我们使用次数最多的方法,很多自定义Adapter
的框架也都是从这两个方法入手的。我们来看看这两个方法到底有什么作用。
(1).onCreateViewHolder
首先,我们来看一下onCreateViewHolder
方法,从它的调用时机入手。
在本文之前,我分析过RecyclerView
的缓存机制,当时我将RecyclerView
的缓存分为4级缓存,其中分别是:
一级缓存:
scrap
数组二级缓存:
CachedView
三级缓存:
ViewCacheExtension
四级缓存:
RecyclerViewPool
LayoutManager
会获取ViewHolder
时,如果4级缓存都没有命中,就会调用Adapter
的onCreateViewHolder
方法来创建一个新的ViewHolder
。我们来看看相关的代码:
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"); } }
上面的代码是RecyclerView
的tryGetViewHolderForPositionByDeadline
方法代码片段。之前,我们在分析缓存机制时,就已经仔细分析这个方法,这里我就不再赘述,有兴趣的同学可以我之前的文章:RecyclerView 源码分析(三) - RecyclerView的缓存机制。
我们回到上面的代码片段中来,从上面的代码上,我们看到这里是调用的是Adapter
的createViewHolder
方法来创建ViewHolder
。我们来看看Adapter
的createViewHolder
方法:
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { try { TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); final VH holder = onCreateViewHolder(parent, viewType); if (holder.itemView.getParent() != null) { throw new IllegalStateException("ViewHolder views must not be attached when" + " created. Ensure that you are not passing 'true' to the attachToRoot" + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)"); } holder.mItemViewType = viewType; return holder; } finally { TraceCompat.endSection(); } }
其实createViewHolder
方法里面也没有做什么的操作,差不多就是调用onCreateViewHolder
方法。简而言之,onCreateViewHolder
有点带兜底的韵味,缓存都没有命中,只能乖乖的创建ViewHolder
。
我们来看看第二方法,也就是onBindViewHolder
方法。
(2). onBindViewHolder
我们都知道,onBindViewHolder
方法的作用是进行数据绑定,所以执行这个方法的条件相对于onCreateViewHolder
有点苛刻。为什么呢?我们这么想一下吧,假设我们change了其中一个ItemView的数据,然后通过notifyItemChanged
来通知数据源已经改变。在这种情况下,正常来说,都是只刷新对应位置的ItemView就行了,没必要刷新其他数据没有改变的ItemView(这里的刷新就是指执行onBindViewHolder
方法)。现在,我们来看看对应的执行代码:
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); }
从上面的代码,我们可以看出来,最后调用了tryBindViewHolderByDeadline
方法。而调用tryBindViewHolderByDeadline
方法条件比较苛刻,不过不管怎么苛刻,只要记住一点,如果对应位置的数据被更新了,该位置会执行一次onBindViewHolder
方法。我们继续看一下tryBindViewHolderByDeadline
方法的代码:
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition, int position, long deadlineNs) { // ······ mAdapter.bindViewHolder(holder, offsetPosition); // ······ }
执行过程跟onCreateViewHolder
方法差不多,都是在依靠Adapter
内部一个对应的final方法来回调。这样所做的好处,可以在onBindViewHolder
方法执行前后做一些其他的操作,比如初始化操作和清理操作,这种模式有点类似于Java中静态代理模式中的继承代理。然后,我们来看看Adapter
的bindViewHolder
方法:
public final void bindViewHolder(@NonNull VH holder, int position) { holder.mPosition = position; if (hasStableIds()) { holder.mItemId = getItemId(position); } holder.setFlags(ViewHolder.FLAG_BOUND, ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); TraceCompat.beginSection(TRACE_BIND_VIEW_TAG); onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); holder.clearPayload(); final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); if (layoutParams instanceof RecyclerView.LayoutParams) { ((LayoutParams) layoutParams).mInsetsDirty = true; } TraceCompat.endSection(); }
从这里,我们可以简单发现,在执行onBindViewHolder
方法前后,各自做了一些不同的操作。比如,在执行onBindViewHolder
方法之前,更新了ViewHolder
的mPosition
属性和给ViewHolder
设置了一些flag;在执行onBindViewHolder
方法之后,清理了ViewHolder
的payload
,并且还是给ItemView
的LayoutParams
的mInsetsDirty
属性设置为true。
这里额外的提出两个点:
payload
主要是用于局部刷新的,待会我会详细解释怎么进行局部刷新。关于
LayoutParams
的mInsetsDirty
属性,这个属性尤为重要的,主要用于ItemView的绘制,后续我在分析ItemDecoration
时会详细的解释这个属性。
3. 局部刷新的基本使用和实现原理
(1). 局部刷新的基本使用
在分析局部刷新之前,我们先来讨论一下怎么进行布局刷新,也就是说怎么通过RecyclerView
实现ItemView
的局部刷新。
假设下面的一个Demo:
点击一下下面灰色的Button
,position
为0的ItemView
会改变显示的文字。如果我们不做局部刷新,出现什么问题呢?我们先来试试:
mDataList.get(0).setString("change data"); mAdapter.notifyItemChanged(0);
正常的实现应该就是上面的代码,非常的简单,也是我们经常书写的代码。这样书写有什么问题吗?有很大的问题!就是整个ItemView会闪烁一下,效果如下:
网上给了一堆的原因分析,我个人觉得,原因非常的简单,就是第一个ItemView执行的change动画。所以介于这两个原因,我们可以找到两种解决方案:
设置
RecyclerView
的change动画时间为0,也就是调用ItemAnimator
的setChangeDuration
方法。直接将
RecyclerView
的ItemAnimator
设置为null。
对于第二种方案,我不置可否。这样来想,我们直接将动画设置为null,那么RecyclerView
就没有任何动画,是不是感觉有点得不偿失?
第一种方案比起第二种方案稍微要好一些,我们将change动画时间设置为0,只影响了change动画(相当于取消了change动画),不会影响其他其他操作的动画。不过,还是感觉美中不足,相当于后面所有的change操作都没有了动画,如果我想有些change操作有动画呢?
此时就需要局部刷新出手了。我们先来看看怎么实现局部刷新:
首先,调用带两个参数的notifyItemChanged
方法,如下:
mAdapter.notifyItemChanged(0, "");
第二参数是一个payload,Object类型,所以我们可以传递任意对象,这里就传递一个空字符串。
然后我们得重写Adapter
的onBindViewHolder
方法(这里重写的是带三个参数的onBindViewHolder
方法,带两个参数的onBindViewHolder
该怎么写就怎么写)。
@Override public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position, @NonNull List<Object> payloads) { if (payloads.contains("")) { holder.mTextView.setText(mDataList.get(position).getString()); } else { super.onBindViewHolder(holder, position, payloads); } }
这里我们判断了一下payloads
里面是否含有之前我们传递的空字符串,如果含有的话,直接更新显示文字即可,如果不含有则走默认逻辑。现在来我们看看效果:
是不是感觉perfect?
(2). 局部刷新的实现原理
局部刷新的使用是非常的简单的,就是重写了Adapter
带三个参数的onBindViewHolder
方法,然后调用的也是带两个参数的notifyItemChanged
方法。
但是,我们不禁好奇,为什么这样做ItemView就不会闪烁呢?我在这里就可以告诉大家答案,是因为没有执行change动画。为了保持求知若饥,虚心若愚的良好传统,大家肯定会进一步的问,为什么在这种情况下不会执行动画呢?其实为了回答这个问题,我早就已经为大家打好铺垫,在理解局部刷新的原理之前,大家最好已经理解了RecyclerView
的动画机制,有兴趣的同学可以看看我之前的文章:RecyclerView 源码分析(四) - RecyclerView的动画机制。
前面频繁的源码追踪我们这里就不进行了,可以参考我的文章:RecyclerView 源码分析(四) - RecyclerView的动画机制,这里直接从根源入手。我们都知道,当我们调用Adapter
的notifyItemChanged
方法,会执行到AdapterHelper$Callback
的markViewHoldersUpdated
方法。
而我们这里不看markViewHoldersUpdated
方法,而是看哪里调用了这个方法。
根据我们辛苦的追踪代码,我们发现主要是有两个地方在调用markViewHoldersUpdated
方法:1. postponeAndUpdateViewHolders
方法;2. consumeUpdatesInOnePass
方法。
这其中,consumeUpdatesInOnePass
方法是我们的老朋友,该方法主要是在dispatchLayoutStep2
方法,其作用也是不言而喻,主要是给消费之前添加的operation
。而postponeAndUpdateViewHolders
方法我们就感觉非常的陌生,这个方法是在哪里被调用呢?
根据我们的追踪,发现它的调用源头是AdapterHelper
的preProcess
方法。而preProcess
方法又是在哪里被调用的呢?是在processAdapterUpdatesAndSetAnimationFlags
方法:
private void processAdapterUpdatesAndSetAnimationFlags() { // ······ if (predictiveItemAnimationsEnabled()) { mAdapterHelper.preProcess(); } else { mAdapterHelper.consumeUpdatesInOnePass(); } // ······ }
而processAdapterUpdatesAndSetAnimationFlags
方法只在dispatchLayoutStep1
方法调用(这里不考虑非自动测量的情况)。这里,我们就彻底明了。dispatchLayoutStep1
方法阶段被预布局阶段,也就是说,change操作在预布局阶段就已经回调markViewHoldersUpdated
方法。
而markViewHoldersUpdated
方法的作用是啥呢?其实在RecyclerView 源码分析(四) - RecyclerView的动画机制,我就已经解释过了,主要作用有两个:
给每个
ViewHolder
打了对应的flag更新每个
ViewHolder
的position。
关于这两个作用的分析,flag我们可以直接跳过,position在后面我会详细的分析。
从而,我们知道,在预布局阶段,每个ViewHolder
的position和flag就已经确定了,这个有什么作用呢?还记得我们之前分析RecyclerView
的动画机制说过,在预布局阶段如果条件允许的话,会进行一次布局,也就是会调用LayoutManager
的onLayouyChildren
方法。
而onLayouyChildren
方法会做啥呢?我主要介绍两点(这里以LinearLayoutManager
为例):
1.调用
LayoutManager
的detachAndScrapAttachedViews
方法,回收所有的ViewHolder,将他们放入四级缓存中。
调用fill方法进行布局。在fill方法调用流程会调用
RecyclerView
的tryGetViewHolderForPositionByDeadline
方法从缓存中获取ViewHolder
。
这里我们先来看回收部分,我们知道detachAndScrapAttachedViews
方法最终会调用到Recycler
的scrapView
方法里面去。我们来看看scrapView
方法(请大家睁大眼睛,这是寻找答案的第一条线索):
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); } }
从这里我们知道,scrapView
方法的作用就是ViewHolder
分别放到mAttachedScrap
和mChanedScrap
数组。这里我们重点关注canReuseUpdatedViewHolder(holder)
这个判断条件,我们来追踪这个方案的代码,最终我们找到了DefaultItemAnimator
的canReuseUpdatedViewHolder
方法:
@Override public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, @NonNull List<Object> payloads) { return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); }
看到没?这里判断了一下payloads
是否为空。这个有什么作用呢?我们回到scrapView
方法来,如果payloads
不为空的话,当前的ViewHolder
会被回收到mAttachedScrap
。这里,我们一定要记得,当ViewHolder
的payloads
不为空,那么在回收时,ViewHolder
会被回收到mAttachedScrap
。这个有什么作用呢?这就需要我们去寻找第二条线索。
第二条线索就藏在tryGetViewHolderForPositionByDeadline
方法里面。我们来瞅瞅:
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; } } // ······ }
结合上面的分析,当在预布局阶段,也就是dispatchLayoutStep1
阶段进行布局,通过带两个参数的notifyItemChanged
方法进行通知,肯定会在上面的代码返回一个ViewHolder
。也就是说,在这种情况下,变化前后该ItemView
的ViewHolder
肯定是同一个ViewHolder
。
如上就是第二条线索,那第二条线索有什么作用呢?就得看第三条线索了。那第三条线索在哪里呢?就在dispatchLayoutStep3
方法里面。
我们都知道,dispatchLayoutStep3
阶段被称为后布局,主要进行动画的执行,我们来看看我们的change操作会执行哪些代码:
private void dispatchLayoutStep3() { // ······ if (mState.mRunSimpleAnimations) { for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { // ······ animateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); // ······ } // ······ }
change操作肯定会执行到如上的代码,我们在分析动画机制时就已经分析过了。我们来看看animateChange
方法:
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, boolean oldHolderDisappearing, boolean newHolderDisappearing) { // ······ if (oldHolder != newHolder) { // ······ addAnimatingView(oldHolder); // ······ } // ······ }
看到没,这就是最终的答案,只有两个ViewHolder
不是同一个对象才会添加一个AnimatingView
。
由于局部刷新的前后,ItemView
的是同一个ViewHolder
对象,才会导致局部刷新不会执行change动画,才会解决ItemView
的闪烁。
有可能有人又有疑问了,为什么会全局刷新不是同一个ViewHolder
呢?我们通过scrapView
方法可以知道,如果全局刷新,那么change的ViewHolder
会被回收到mChangedScrap
数组里面去,而在tryGetViewHolderForPositionByDeadline
方法里面,我们可以知道,只有预布局阶段才会从mChangedScrap
数组里面获取ViewHolder
对象:
if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; }
所以预布局阶段和正式布局阶段同一个ItemView
肯定是不同的ViewHolder
,从而会执行change动画。
由于这个问题的答案寻找起来比较麻烦,这里我就针对这个问题做一个简单的总结:
布局刷新之所以能解决
ItemView
的闪烁问题,是因为在局部刷新的情况下,不会执行change动画。而不执行的chang动画的原因是因为在刷新前后都是同一个ViewHolder
,并且都是从mAttachedScrap
数组里面获得,所以在动画执行阶段,不会执行局部刷新导致的change动画,进而解决闪烁问题;而全局刷新由于刷新前后不是同一个ViewHolder
,所以会执行change动画。
作者:琼珶和予
链接:https://www.jianshu.com/p/bdd9f4bdd90a