熟悉RecyclerView
的同窗应该都知道,Adapter
做为RecyclerView
四大组成部分(Adapter
,LayoutManager
,ItemAnimator
,ItemDecoration
)之一,其重要性天然是不言而喻。今天,咱们来分析一下Adapter
的源码。我打算将Adapter
的源码分析分为两个部分,一是,从普通的角度上来看Adapter
,从源码的角度上来分析咱们平常使用的一些操做;二是,分析DiffUtil
,可能会涉及到Adapter
的部分源码。因此Adapter
源码分析分为两篇,本文是第一篇。数组
在分析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
的相关代码。我打算从以下角度来分析:bash
- 从新从
RecyclerView
缓存角度来分析onCreateViewHolder
和onBindViewHolder
。onBindViewHolder
的一个重载方法--主要是用于局部刷新。- 结合
Adapter
,分析ViewHolder
的position。
onCreateViewHolder
方法和onBindViewHolder
方法算是咱们使用次数最多的方法,不少自定义Adapter
的框架也都是从这两个方法入手的。咱们来看看这两个方法到底有什么做用。app
首先,咱们来看一下onCreateViewHolder
方法,从它的调用时机入手。框架
在本文以前,我分析过RecyclerView
的缓存机制,当时我将RecyclerView
的缓存分为4级缓存,其中分别是:less
- 一级缓存:
scrap
数组- 二级缓存:
CachedView
- 三级缓存:
ViewCacheExtension
- 四级缓存:
RecyclerViewPool
LayoutManager
会获取ViewHolder
时,若是4级缓存都没有命中,就会调用Adapter
的onCreateViewHolder
方法来建立一个新的ViewHolder
。咱们来看看相关的代码:ide
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
。post
咱们来看看第二方法,也就是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
时会详细的解释这个属性。
在分析局部刷新以前,咱们先来讨论一下怎么进行布局刷新,也就是说怎么经过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
里面是否含有以前咱们传递的空字符串,若是含有的话,直接更新显示文字便可,若是不含有则走默认逻辑。如今来咱们看看效果:
局部刷新的使用是很是的简单的,就是重写了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,将他们放入四级缓存中。 2. 调用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动画。
在ViewHolder的内部有几个让人难以理解的问题,一个是flag,众多的flag让人很是的懵逼,这个我在缓存机制那一篇文章,我已经作了详细的总结,有兴趣的同窗能够看看个人文章:RecyclerView 源码分析(三) - RecyclerView的缓存机制;另外一个是position,本文来重点分析一下。
这里主要分析两个方法,分别是getAdapterPosition
和getLayoutPosition
,对应着ViewHolder
内部两个成员变量mPosition
和mPreLayoutPosition
两个属性。
你们在使用这两个方法时,应该都对这两个方法有必定的疑问,这里我简单的解释一下这两个方法的区别(其实咱们从这两个方法的注释就能看出区别)。
咱们先来看看这两个方法的代码,首先来看一下getAdapterPosition
方法:
public final int getAdapterPosition() {
if (mOwnerRecyclerView == null) {
return NO_POSITION;
}
return mOwnerRecyclerView.getAdapterPositionFor(this);
}
复制代码
别看getAdapterPosition
方法比较麻烦,还调用了RecyclerView
的getAdapterPositionFor
方法进行位置的计算。可是它表达的意思是很是简单的,就是获取当前ViewHolder所绑定ItemView的真实位置。这里的真实位置说的比较笼统,这样来解释吧,当咱们remove掉为position为0的item,正常来讲,后面ViewHolder
的position应该都减1。可是RecyclerView
处理Adapter
的更新采用的延迟处理策略,因此在正式处理以前获取ViewHolder
的位置可能会出现偏差,介于这个缘由,getAdapterPosition
方法就出现了。
getAdapterPosition
方法是怎样保证每次计算都是正确的呢?包括在正式处理以前呢?咱们知道,在RecyclerView
中,延迟处理的实现是在notify阶段往一个叫mPendingUpdates
数组里面添加Operation
,分别在dispatchLayoutStep1
阶段或者dispatchLayoutStep2
阶段进行处理。经过追踪getAdapterPositionFor
方法,咱们知道getAdapterPosition
方法在计算位置时,考虑到mPendingUpdates
数组的存在,因此在notify阶段和dispatchLayoutStep1
阶段之间(这里假设dispatchLayoutStep1
就会处理),getAdapterPosition
方法返回正确的位置。
而getLayoutPosition
方法呢?getLayoutPosition
方法就不能保证在notify阶段和dispatchLayoutStep1
阶段之间获取的位置是正确的。为何这么说呢?咱们来看看getLayoutPosition
方法的代码:
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
复制代码
getLayoutPosition
方法返回的是mPosition
或者mPreLayoutPosition
,可是在dispatchLayoutStep1
阶段以前,还未更新每一个ViewHolder
的position,因此得到不必定的是正确(只有在处理mPendingUpdates
的操做时,position才会被更新,对应着的代码就是执行AdapterHelper$Callback
接口的方法)。
可是getLayoutPosition
方法为何还有存在的必要呢?咱们发现getLayoutPosition
方法不会每次都计算,也就是说,getLayoutPosition
方法的效率比getAdapterPosition
方法高。当咱们在Adapter这种调用方法来获取ViewHolder
的位置时,能够优先考虑getLayoutPosition
方法,由于Adapter
的方法回调阶段不在mPendingUpdates
处理以前,因此此时getLayoutPosition
方法跟getAdapterPosition
方法没有任何区别了。
可是须要注意,若是咱们在其余地方获取ViewHolder
的position
,要特别注意这种状况,由于其余地方不能保证与RecyclerView
状态同步,这种状况为了保证结果的正确性,咱们应该优先考虑getAdapterPosition
方法。
本文到这里差很少就结束了,在这里咱们作一个简单的总结
- 之因此局部刷新能解决
ItemView
闪烁的问题,是由于局部刷新进行change操做时没有执行change动画。而没有执行change动画的缘由是由于在预布局阶段和后布局阶段,ItemView
的ViewHolder
是同一个对象。getAdapterPosition
方法在任什么时候候获取的都是ViewHolder
真实的位置,而getLayoutPosition
方法只在mPendingUpdates
数组处理以后才能获取真实的位置。这是两个方法区别。
下一篇文章是分析DiffUtil
的实现,来看看DiffUtil
来怎么实现差量计算的。