持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
神奇bug
本来是转测版本后开开心心的第二天,突然测试同事就找到我说,你这标题栏有一个bug,会把一个标题的图片加载到上一个标题去。我听了之后,直接三连:不可能,数据配错了,复现给我看。
啪的一下!很快啊,立马复现,直接打脸。我捂着脸去回去翻代码,下面是简化后的代码: 很简单,一个RecyclerView,表项是TextView+ImageView,实体类是标题名+图片
TabBean
data class TabBean(var title: String, var stateDrawable: Drawable?)
TabAdapter
class TabAdapter(val mContext: Context) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
private val mTabBeanList = ArrayList<TabBean>()
……
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tabBean = mTabBeanList[position]
holder.mTvTitle.text = tabBean.title
tabBean.stateDrawable?.let {
holder.mIvLogo.setImageDrawable(it)
}
}
……
}
数据
arrayListOf(
TabBean("犬夜叉", getDrawable(R.drawable.selector_logo_qyc)),
TabBean("圣斗士", null),
TabBean("洗衣机", null),
TabBean("暴龙兽", null),
TabBean("那撸多", getDrawable(R.drawable.selector_logo_naruto)),
TabBean("海贼王", null),
//第二次增加这一条数据
//TabBean("金木研", null)
)
到这里一切都很正常,但是当我将数据增加一条,再重新设置数据调用notifyDataSetChanged
更新后,问题就来了,下面是两次的效果图:
现象
增加了一条数据,刷新后竟然让本没有图片的“暴龙兽”获得了“那撸多”的图片!
啊,这波啊,这波是多重影分身之术!来,换个小李的图片就没问题了
减少列表里的数据,或者刷新时再多增加一两条数据,最后的结果也会变得不同,这是为什么呢?看现象可以推测是复用相关的问题。
解决bug
其实看到代码,可能大家就已经发现问题的所在,那就是我在TabAdapter
的onBindViewHolder
方法里,当图片为null时,并没有将imageView
的drawable
设置为null,导致发生了这样的问题。
所以只要如下修改,就可以恢复:
if (tabBean.stateDrawable == null){
holder.mIvLogo.setImageDrawable(null)
}
不过我还是很奇怪,为什么图片会错位“复用”到上一个表项上?于是我打印了两次加载的表项,惊奇地发现:
刷新后,Recyclerview某些位置的表项,复用了之前后一个位置的表项。
也就是是说,刷新后的“暴龙兽”,其实是之前的“那撸多”换了名字(setText)而已,所以并不是它错误的加载了图片,而是它本来就是如此。
带着这些疑惑,我开始翻看源码。
RecyclerView复用逻辑分析
既然是数据更新时出了问题,那么久从notifyDataSetChanged()
和onBindViewHolder
两个方面来看
1. notifyDataSetChanged()
调用notifyDataSetChanged()
,实质是让被观察者mObservable去通知观察者数据已经发生了变化,从而更新。mObservable(AdapterDataObservable)继承自android.database.Observable,其中有一个观察者数组,通过registerObserver()
方法去注册添加观察者Observer,最终调用的是Observer的onChanged()
方法
public final void notifyDataSetChanged() {
//被观察者通知数据发生改变
mObservable.notifyChanged();
}
//AdapterDataObservable继承了Observable被观察者,然后通过registerObserver方法注册观察者
static class AdapterDataObservable extends Observable<AdapterDataObserver>
//观察者
private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
}
继续看观察者的onChanged()
方法,发现processDataSetCompletelyChanged()
方法
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
//从字面意思上是将已知的view都标记为失效,接下来往里看
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()) {
//这里给viewHolder设置flag,为UPDATE和INVALID,后面有用
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
void markKnownViewsInvalid() {
……
if (mAdapter == null || !mAdapter.hasStableIds()) {
//这里满足条件的话,cachedViews会被全部回收,不过我这里没有离屏缓存,就可以先不往下看了
recycleAndClearCachedViews();
}
}
//中间这些就只列出关键代码和它们的作用
recycleCachedViewAt(i);//倒序遍历mCachedViews,开始回收
addViewHolderToRecycledViewPool(viewHolder, true);//获取到ViewHolder,准备加入RecycledViewPool
getRecycledViewPool().putRecycledView(holder);//放入Pool中
这里主要就是给viewHolder设置标识位,以及回收cachedViews
接下来再看onBindViewHolder
2. onBindViewHolder
调用链
从onBindViewHolder
的调用的地方,一路往上找,方法的调用链如下:
LinearLayoutManager ->
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
∟ fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable)
∟ layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)
∟ next(RecyclerView.Recycler recycler)
RecyclerView ->
∟ getViewForPosition(int position)
∟ getViewForPosition(int position, boolean dryRun)
∟ tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs)
∟ tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, int position, long deadlineNs)
∟ bindViewHolder(@NonNull VH holder, int position)
∟ onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads)
最终找到了开始的地方onLayoutChildren
,那么来看一下它的关键代码
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
……
detachAndScrapAttachedViews(recycler);
……
fill(recycler, mLayoutState, state, false);
}
分离、废弃和回收view
可以看到在填充前,还调用了detachAndScrapAttachedViews
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
//遍历出hideView外所有的view,注意这里是倒序
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
//之前已经给viewHolder设置了invalid的flag标志位,条件满足
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//移除
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
……
}
}
void recycleViewHolderInternal(ViewHolder holder) {
……
boolean cached = false;
//viewholder的flag有update和invalid,不满足条件,所以cached不改变,还是false
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
cached = true;
}
if (!cached) {
//获取到的ViewHolder,准备加入RecycledViewPool
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
从代码来看,detachAndScrapAttachedViews
是先从mChildHelper中倒序
遍历出所有的view,这个ChildHelper是管理LayoutManager和RecyclerView子项的帮助类,然后通过view获取viewholder,接下来判断这个viewHolder是否失效、是否已被移除、是否有设置了id等等条件。上面说过在调用notifyDataSetChange
时给viewholder设置了FLAG_INVALID|FLAG_UPDATE标识位,所以最终viewHolder只被回收到了RecycleredViewPool中。
回收到pool中
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
//放入Pool中
getRecycledViewPool().putRecycledView(holder);
}
//关键点来了,分析在下面
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
从源码来看,putRecycledView
中先获取viewholder的类型,然后根据类型viewType
从mScrap
中获取对应的集合,如果这个集合中已存在的元素数量小于它的最大值mMaxScrap
(默认是5,可以手动设置),那么就将要回收的viewHolder添加到里面去,其实也就是说每个类型最多会回收mMaxScrap
个viewHolder。
别忘了前面在detachAndScrapAttachedViews
里可是倒序遍历view的,所以这里也是倒序添加的。而我的代码中最初有6个view,且没有重设最大值,那么此时只回收了6,5,4,3,2共5个view的viewholder,剩下没有被回收的1,就被无情地丢弃掉了。
填充
看完了废弃和回收,接下来看看填充方法fill()
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)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
}
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
……
addView(view);
}
View next(RecyclerView.Recycler recycler) {
……
final View view = recycler.getViewForPosition(mCurrentPosition);
……
return view;
}
public View getViewForPosition(int position)
View getViewForPosition(int position, boolean dryRun)
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs)
根据上面列出的调用链,可以一路找到tryGetViewHolderForPositionByDeadline
,这里是真正决定复用或者生成ViewHolder
的地方。
方法很长,先看注释(笑)。我从方法的注释中了解到,这是一个试图根据指定位置获取ViewHolder的方法,会尝试从scrap, 或者cache,亦或RecycledViewPool中获取,实在没有就直接创建一个。
Attempts to get the ViewHolder for the given position, either from the Recycler scrap, cache, the RecycledViewPool, or creating it directly.
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 0) 先从changed Scrap中获取viewholder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
// 1) 根据位置position从scrap/hidden list/cache这几个地方获取viewholder
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
……
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
// 2) 根据id查找scrap/cache
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
……
}
if (holder == null && mViewCacheExtension != null) {
//这是从开发人员自定义的缓存中取
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
……
}
if (holder == null) { // fallback to pool
//从缓存池中取
holder = getRecycledViewPool().getRecycledView(type);
……
}
if (holder == null) {
……
//直接创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
……
}
}
……
return holder;
}
从代码来看,获取ViewHolder其实经过了几个步骤:直接从changed scrap中查找;通过position或id查找scrap、hidden list、cache;查找自定义缓存;查找缓冲池;直接创建。任意一个步骤获取到了viewholder,就直接返回,否则继续进行下一步直至创建为止。
对于我的代码来说,既没有设置id或自定义缓存,又因为notifyDataSetChange使一些缓存失效,最后能用的只有RecyclerViewPool。前面也分析过,1个被抛弃,只缓存了5个,因此还要再新创建两个,最终导致了开头的结果。惭愧,惭愧~(掩面)。
第一次写这种分析文章,不当之处,请大家不吝赐教,谢谢