GVKun编程网logo

Android源码解读——RecyclerView回收复用机制(recyclerview的回收复用机制)

16

关于Android源码解读——RecyclerView回收复用机制和recyclerview的回收复用机制的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于AndroidListView与R

关于Android源码解读——RecyclerView回收复用机制recyclerview的回收复用机制的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于Android ListView与RecyclerView对比浅析--缓存机制、Android MotionLayout + Recyclerview,recyclerview中的视图不可单击、Android RecyclerView、Android RecyclerView 复用错乱通用解法详解等相关知识的信息别忘了在本站进行查找喔。

本文目录一览:

Android源码解读——RecyclerView回收复用机制(recyclerview的回收复用机制)

Android源码解读——RecyclerView回收复用机制(recyclerview的回收复用机制)

问题归类:

  • 什么是回收?什么是复用?
  • 回收什么?复用什么?
  • 回收到哪里去?从哪里获得复用?
  • 什么时候回收?什么时候复用?

带着以上几个问题来分析源码,当以上问题都能解释清楚的时候,对RecyclerView回收复用机制的了解也算是完成了。

1、什么是回收?什么是复用?

回收:即缓存,RecyclerView的缓存是将内容存到集合里面。

复用:即取缓存,从集合中去获取。

2、回收什么?复用什么?

回收和复用的对象都是ViewHolder。

什么是ViewHolder?ViewHolder其实就是用来包装view的,我们可以将它看成列表的itemview

3、回收到哪里去?从哪里获得复用?

4、什么时候回收?什么时候复用?

问题3、4结合源码一起分析。

首先对RecyclerView进行普通的使用

 

public class TestRvAdapter extends RecyclerView.Adapter<TestRvAdapter.ViewHolder> {

    private Context context;
    private List<Star> starList;
    private static final String TAG = "TestRvAdapter";

    public TestRvAdapter(Context context, List<Star> starList) {
        this.context = context;
        this.starList = starList;
    }

    @NonNull
    @Override
    public TestRvAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.rv_top_item, null);
        Log.e(TAG, "onCreateViewHolder: " + getItemCount());
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull TestRvAdapter.ViewHolder holder, int position) {
        holder.tv.setText(starList.get(position).getName());
        Log.e(TAG, "onBindViewHolder: " + position);
    }

    @Override
    public int getItemCount() {
        return starList == null ? 0 : starList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        private TextView tv;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            tv = itemView.findViewById(R.id.tv_star);
        }
    }
}

 

public class RvTestActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private List<Star> starList = new ArrayList<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_rv);

        initData();

        recyclerView = findViewById(R.id.test_rv);
        /*注意此处*/
        recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
        recyclerView.addItemdecoration(new DividerItemdecoration(this, LinearLayout.VERTICAL));
        recyclerView.setAdapter(new TestRvAdapter(this, starList));

    }

    private void initData() {
        for (int i = 1; i <= 1000; i++) {
            starList.add(new Star(i + "", "快乐家族" + i));
        }
    }
}

以上代码相信都看得懂吧?我们看下代码细节。

在adapter的onCreateViewHolder和onBindViewHolder两个方法里面进行打印,然后在activity里面我们设置LayoutManager时,用的是GridLayoutManager,数量设置为1,其实跟linearlayoutmanager是一样的效果,但是这里为了方便测试,所以用的是GridLayoutManager,进入到GridLayoutManager源码也可以看到,他是继承自linearlayoutmanager的。 OK现在运行看看打印情况。

可以发现,刚进入界面的时候,onCreateViewHolder和onBindViewHolder都进行了打印,而往下滑动之后只会打印onBindViewHolder,不会再进入onCreateViewHolder

我们把代码 new GridLayoutManager(this, 1) 的1改成8试试会是怎么样的打印情况:

 

可以看到,无论滚动到哪个item,onCreateViewHolder和onBindViewHolder都一直在打印,那么这是为什么呢?

接下来分析源码。

首先就 RecyclerView 的源码就有一万多行,这还没包括layoutmanager的,分析到底从何入手?那么我们知道,在触发onCreateViewHolder和onBindViewHolder的时候,我们都对屏幕进行了滑动,所以我们直接先进入 RecyclerView 的onTouchEvent看看RecyclerView 是如何进行滑动处理的。由于是滑动,因此我们直接进入action_move里面进行查看。(由于源码太多,就不贴上来了,源码流程只需要关系入口以及我们需要分析的部分就行了。)

入口:滑动 Move 事件 --> scrollByInternal --> scrollStep --> mLayout.scrollVerticallyBy(mLayout就是LayoutManager,所以我们要看他的实现类linearlayoutmanager.scrollVerticallyBy)  --> scrollBy --> fill --> layoutChunk --> layoutState.next获取view --> addView(view);

view就是从这里加载进RecyclerView 的,那么在addView之前有一个获取view的动作layoutState.next,我们具体分析一下到底是如何获取的。

layoutState.next --> getViewForPosition --> tryGetViewHolderForPositionByDeadline

tryGetViewHolderForPositionByDeadline就是我们最终需要找到的一个方法,在这个方法里面,RecyclerView 通过缓存取出viewHolder,我们可以看到里面有各种 if (holder == null) 的判断,那么到底是如何去取的呢?

分以下几种情况去获取ViewHolder

  1. getChangedScrapViewForPosition -- mChangeScrap 与动画相关
  2. getScrapOrHiddenorCachedHolderForPosition -- mAttachedScrap 、mCachedViews
  3. getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)
  4. mViewCacheExtension.getViewForPositionAndType -- 自定义缓存
  5. getRecycledViewPool().getRecycledView -- 从缓冲池里面获取

 

归纳一下我们可以得出,RecyclerView 大致分为四级缓存

  • mChangeScrap与 mAttachedScrap,用来缓存还在屏幕内的 ViewHolder
  • mCachedViews,用来缓存移除屏幕之外的 ViewHolder
  • mViewCacheExtension,开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存(通常用不到,至少目前为止我没用到过)
  • RecycledViewPool,ViewHolder 缓存池

多级缓存的最终目的就是为了提升性能

看到源码最后一个if语句,也就是当所有的缓存都没有viewHolder的时候,这个时候我们就需要创建,

holder = mAdapter.createViewHolder(RecyclerView.this, type);

createViewHolder方法里面自然的就调用到了adapter的onCreateViewHolder方法

也就是:当没有缓存的时候: mAdapter.createViewHolder --> onCreateViewHolder

    创建ViewHolder 后 绑定: tryBindViewHolderByDeadline--> mAdapter.bindViewHolder--> onBindViewHolder

以上为复用(取缓存)的过程,那么回收(存缓存)是个什么样的机制呢?

首先也是一样要找到入口

当我们刷新布局的时候,RecyclerView 会调用到 linearlayoutmanager 的 onLayoutChildren 方法。

linearlayoutmanager.onLayoutChildren --> detachAndScrapAttachedViews --> scrapOrRecycleView

    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasstableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

 

那么在 scrapOrRecycleView 方法里面分为了两种情况:

  • recycler.recycleViewHolderInternal(viewHolder);
  • recycler.scrapView(view);

分析recycler.recycleViewHolderInternal(viewHolder);

进入到recycleViewHolderInternal方法,可以看到有如下的一个判断:

if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNowN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }

 

这段代码表示:如果ViewHolder不改变(有时候一个列表可能用到了多个ViewHolder),那么先判断mCachedViews的大小

         mCachedViews.size 大于默认可缓存的大小(默认为2),执行recycleCachedViewAt,实际上就是将cacheView里面的数据拿到RecycledViewPool缓存池里面,然后再把新的缓存存入到cacheView里面,采取的是先进先出的原则。

       缓存池里面保存的只是 ViewHolder 类型,没有数据,而cacheView是包含数据的(经过binder了的ViewHolder)。这也是为什么分级的原因,最终还是为了执行效率。

而缓冲池是与Map类似。他的缓存形式就如:ArrayList<ArrayList<viewholder>>, RecycledViewPool 会根据ViewType来进行分类,ArrayList<viewholder>对应的就是一个ViewType

我们进入recycleCachedViewAt--> addViewHolderToRecycledViewPool(缓存到缓存池里面)

如果上面的条件不满足,那么会执行到下面的if语句块,直接缓存到缓存池:

if (!cached) {
    addViewHolderToRecycledViewPool(holder, true);
    recycled = true;
}

 

直接执行addViewHolderToRecycledViewPool,可以看到与上面调用的方法一样,在这个方法里面会执行getRecycledViewPool().putRecycledView(holder)

 

 

    public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
        //多余的直接丢弃 mMaxScrap = 5 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); }

 

 

 

通过上面源码可以得出,每个ArrayList<viewholder>最多存储5个ViewHolder,多余的会直接丢弃不保存。

为什么数据满了之后,会直接丢弃呢?接着分析:

假如没有满的话,也就是没有多余的话,那么就会执行 scrap.resetInternal(); 

        void resetInternal() {
            mFlags = 0;
            mPosition = NO_POSITION;
            mOldPosition = NO_POSITION;
            mItemId = NO_ID;
            mPreLayoutPosition = NO_POSITION;
            mIsRecyclableCount = 0;
            mShadowedHolder = null;
            mShadowingHolder = null;
            clearPayload();
            mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
            clearnestedRecyclerViewIfNotnested(this);
        }        

 

这里面是将ViewHolder进行清空,然后再通过 scrapHeap.add(scrap); 进行保存,这也是为什么缓冲池里面只是 ViewHolder类型,而没有数据的原因,没有数据的话,我们缓存太多没有意义。

这也是为什么前面我们运行的时候,当值=8的时候,会不停的刷新onCreateViewHolder和onBindViewHolder,而为1的时候只刷新onBindViewHolder,因为其默认上限值为2+5=7,超过这个数,多余的就没进入缓存了。

以上分析完毕之后我们知道了,下面两种情况是如何缓存的了

而第三种是系统不管的,也就是我们自定义的缓存

分析代码:recycler.scrapView(view),mAttachedScrap 和 mChangedScrap 这种情况

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

 

我们发现,该方法一进入的时候就直接在处理了 

mAttachedScrap.add(holder);
mChangedScrap.add(holder);

只是需要清楚上面的判断是如何判断的,其意思就是 if 的情况 ,当我们的标记没移除、或者失效、更新、动画不复用或者没动画的时候,就会利用 mAttachedScrap 进行保存,否则就用 mChangedScrap 进行保存。

完。

Android ListView与RecyclerView对比浅析--缓存机制

Android ListView与RecyclerView对比浅析--缓存机制

一,背景

RecyclerView是谷歌官方出的一个用于大量数据展示的新控件,可以用来代替传统的ListView,更加强大和灵活。

最近,自己负责的业务,也遇到这样的一个问题,关于是否要将ListView替换为RecyclerView?

秉承着实事求是的作风,弄清楚RecyclerView是否有足够的吸引力替换掉ListView,我从性能这一角度出发,研究RecyclerView和ListView二者的缓存机制,并得到了一些较有益的"结论",待我慢慢道来。

同时也希望能通过本文,让大家快速了解RecyclerView与ListView在缓存机制上的一些区别,在使用上也更加得心应手吧。

PS:相关知识: ListView与RecyclerView缓存机制原理大致相似,如下图所示:

过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)

PPS:本文不贴出详细代码,结合源码食用更佳!

二. 正文

2.1 缓存机制对比

1. 层级不同:

RecyclerView比ListView多两级缓存,支持多个离ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。

具体来说: ListView(两级缓存):

RecyclerView(四级缓存):

ListView和RecyclerView缓存机制基本一致:

1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;

2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.

3). RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

2. 缓存不同:

1). RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为: View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态); 2). ListView缓存View。

缓存不同,二者在缓存的使用上也略有差别,具体来说: ListView获取缓存的流程:

RecyclerView获取缓存的流程:

1). RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView:

而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView),相关代码如下:

//AbsListView源码:line2345
//通过匹配pos从mScrapView中获取缓存
final View scrapView = mRecycler.getScrapView(position);
//无论是否成功都直接调用getView,导致必定会调用createView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
    if (child != scrapView) {
        mRecycler.addScrapView(scrapView, position);
    } else {
        ...
    }
}

2). ListView中通过pos获取的是view,即pos–>view; RecyclerView中通过pos获取的是viewholder,即pos --> (view,viewHolder,flag); 从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心.

2.2 局部刷新

由上文可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView.

(RecyclerView和ListView添加,移除Item效果对比)

结合RecyclerView的缓存机制,看看局部刷新是如何实现的: 以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为: onMeasure()–>onLayout()–>onDraw()

其中,onLayout()为重点,分为三步:

  1. dispathLayoutStep1():记录RecyclerView刷新前列表项ItemView的各种信息,如Top,Left,Bottom,Right,用于动画的相关计算;
  2. dispathLayoutStep2():真正测量布局大小,位置,核心函数为layoutChildren();
  3. dispathLayoutStep3():计算布局前后各个ItemView的状态,如Remove,Add,Move,Update等,如有必要执行相应的动画.

其中,layoutChildren()流程图:

当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):

当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.

需要指出,ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是"一锅端",将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

三.结论

  1. 在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异:

文章的开头便抛出了这样一个问题,微信Android客户端卡券模块,大部分UI都是以列表页的形式展示,实现方式为ListView,是否有必要将其替换成RecyclerView呢?

答案是否定的,从性能上看,RecyclerView并没有带来显著的提升,不需要频繁更新,暂不支持用动画,意味着RecyclerView优势也不太明显,没有太大的吸引力,ListView已经能很好地满足业务需求。

  1. 数据源频繁更新的场景,如弹幕:http://www.jianshu.com/p/2232a63442d6等RecyclerView的优势会非常明显;

进一步来讲,结论是: 列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其它情况(如微信卡包列表页)两者都OK,但ListView在使用上会更加方便,快捷。

Ps:仅从一个角度做了对比,盲人摸象,有误跪求指正。

四.参考资料

1. ListView

a. Android-23源码 b. Android ListView工作原理解析,带你从源码的角度彻底理解:http://blog.csdn.net/guolin_blog/article/details/44996879 c. Android自己动手写ListView学习其原理:http://blog.csdn.net/androiddevelop/article/details/8734255

2. RecyclerView

a. RecyclerView-v7-23.4.0源码 b. RecyclerView剖析:http://blog.csdn.net/qq_23012315/article/details/50807224 c. RecyclerView剖析:http://blog.csdn.net/qq_23012315/article/details/51096696

Android MotionLayout + Recyclerview,recyclerview中的视图不可单击

Android MotionLayout + Recyclerview,recyclerview中的视图不可单击

好的,我已经解决了这个错误。该错误来自recyclerview和motionlayot之间的兼容性。只需将您的recyclerview版本设置为最新版本即可。

Android RecyclerView

Android RecyclerView

http://blog.csdn.net/skykingf/article/details/50827141

http://www.2cto.com/kf/201511/450814.html

http://blog.csdn.net/lmj623565791/article/details/45059587

Android RecyclerView 复用错乱通用解法详解

Android RecyclerView 复用错乱通用解法详解

写在前面:

在上篇文章中说过对于像 RecyclerView 或者 ListView 等等此类在有限屏幕中展示大量内容的控件,复用的逻辑就是其核心的逻辑,而关于复用导致最常见的 bug 就是复用错乱。在大上周我就遇到了一个很奇怪的问题,这也是我下决心研究 RecyclerView 的原因。

RecyclerView 源码分析

而这篇文章的目的首先是讨论在 RecyclerView 复用错乱时,一些通用的解决思路,其次就是探究我遇到的那个奇怪的问题,帮助未来同样遇到的朋友们。

复用错乱的解决办法

本文的前半部分很简单的,以为关于复用错乱,RecyclerView 已经有他的前辈 ListView 替它踩了很多坑了。虽然他们的复用逻辑是有差异的,例如 ListView 只有两层缓存,但是 RecyclerView 可以理解为有四层;ListView 缓存的单位是 view,而 RecyclerView 缓存的单位是 ViewHolder。但是不管他们复用逻辑的差异如何,终归都是把那个缓存起来的 view 拿过来接着用,所以解决复用错乱的方法是一样的。

RecyclerView 复用导致错乱的原因其实就是拿出来之前的 View 来添加到新 item 上,之前 View 的状态一直保留着,所以也就错乱了。不过解决起来很简单:

首先我们以 adapter 数据的来源分为两大类:

1.当数据来源是同步的

这种情况是最简单的,你就保证当 onBindViewHolder 方法调用的时候,你的 itemview 中每个 view 的状态都有一个默认值。这是什么意思呢?

    if ("<unkNown>".equals(artists)) {
      holder.cbMusicState.setChecked(true);
    } else {
      holder.cbMusicState.setChecked(false);
    }

假设我们的 holder 里面有个 CheckBox 控件,当歌手名为 unkNown 时,CheckBox 勾选。注意个时候你一定要加上这个 else 条件,才能保证复用这个 ViewHolder 的时候,CheckBox 的状态不出错。任何控件都一样,总结起来就是你要给每个控件的状态赋一个新的值,替换掉之前的,这样自然不会出现什么复用错乱的问题。

2.当数据的来源是异步的

这种情况也很常见,我们举个栗子,比如你的 ItemView 里面有个 ImageView,每次 onBindViewHolder 的时候,你传入一个 url,等待服务器返回的结果,然后展示在 ImageView 上。这种情况会怎样导致错乱呢?

是这样的,假设我进入了页面,开始为第一个 ImageView 请求图片,但是此刻我下划屏幕,划到了第四个 item,此时第一个 item 已经不可见了,第四个 item 复用了第一个 item 的 imageview,恰好此刻第一个 imageview 的图片结果返回了,就正好展示在了第四个 itemview 上。 这样就发生了图片的错乱。

出现这个问题的原因就是这个 ImageView 和请求的 url 没一一绑定,所以按照这个思路来解决吧:

  holder.ivCameraimages.setBackground(R.drawable.place_holder);
  
  holder.ivCameraimages.setTag(imageURL);

    @Override
    public void handleMessage(Message msg) {
      super.handleMessage(msg);
      if (msg.what == MSG_IMAGE) {
        Bitmap bm = (Bitmap) msg.obj;
        if (bm != null) {
          if (TextUtils.equals((String) imageView.getTag(),imageURL)) {
            imageView.setBackground(new BitmapDrawable(bm));
          }
        }
      }
    }

首先在没加载图片之前,给 ImageView 设置一个默认图片,然后通过 setTag 方法,将 ImageView 和 图片的 url 一一对应起来,设置的时候再判断一下,这个 imageview 的 tag 和当时请求的 url,是不是一致的,如果是一致的,再保存。

以上就是复用错乱时两种比较通用的解法,基本上可以覆盖大部分情况。

一个奇怪的问题

这个问题的现象是这样子的:

当 RecyclerView 的条目很少的时候,比如只有六个,将 RecyclerView 从上滑动到下,这个时候是正常的,onBindViewHolder 会调用,不过此时从底部上划的时候,上方的 item 从不可见到可见的这个过程中,onBindViewHolder 并没有调用,这个时候我也就没办法进行一些刷新 item 的操作了。

这个问题的原因是 onBindViewHolder 方法不调用导致的,我在 StackOverflow 上搜索了很多答案,终于找到了一个可以解决我的问题的:

recyclerview-not-recycling-views-if-the-view-count-is-small

(中文资料压根就没有,所以掌握英文搜索是多么的重要)

你可以调用

recyclerView.setItemViewCacheSize(int);

这个 api,去调整 RecyclerView 的复用逻辑和方式来解决 onBindViewHolder 没有调用的这个问题。

但是原理是怎样的呢?作为一名好奇心颇重的程序员,一步步 debug RecyclerView 的源代码,发现了导致这个问题的原因,一起来看看吧。

在上一篇文章中,我们分析了 RecyclerView 的源码,其中复用逻辑的模块,有一个非常重要的核心方法 tryBindViewHolderByDeadline,这个方法目的就是在 RecyclerView 的层层缓存结构中,取出 ViewHolder。

这里就不再次研究它了,想了解的去看之前的文章,我来描述一下对于这个场景,简化之后的逻辑:

当 RecyclerView 从底部向上滑动的时候,会先后从 mCachedViews 和 mRecyclerPool 中寻找缓存的 ViewHolder。

mCachedViews 和 mRecyclerPool 之间又有什么关系呢?

    public void setViewCacheSize(int viewCount) {
      mRequestedCacheMax = viewCount;
      updateViewCacheSize();
    }

    void updateViewCacheSize() {
      int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
      mViewCacheMax = mRequestedCacheMax + extraCache;

      // first,try the views that can be recycled
      for (int i = mCachedViews.size() - 1;
          i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
        recycleCachedViewAt(i);
      }
    }

当调用 setViewCacheSize 这个方法时,相当于是给 mViewCacheMax 这个变量赋值了, for 循环调用 recycleCachedViewAt 的作用是将 mCachedViews 中缓存的 ViewHolder 放进 RecyclerPool 中。可以看到 for 循环的周期是从 mCachedViews 的最后一个对象直到 mCachedViews.size == mViewCacheMax 这个值时。

也就是可以这么理解, setViewCacheSize 这个方法其实就是为 mCachedViews 集合设置所能持有 ViewHolder 的最大数量。

当  setViewCacheSize(0)时,RecyclerView 想去复用 ViewHolder 时,只能去 RecyclerPool 中去取了,这里就有问题来了,从 RecyclerPool 中取和从 mCachedViews 中取 ViewHolder 中又有什么区别呢?

        if (holder == null) { // fallback to pool
          if (DEBUG) {
            Log.d(TAG,"tryGetViewHolderForPositionByDeadline("
                + position + ") fetching from shared pool");
          }
          holder = getRecycledViewPool().getRecycledView(type);
          if (holder != null) {
            holder.resetInternal();
            if (FORCE_INVALIDATE_disPLAY_LIST) {
              invalidatedisplayListInt(holder);
            }
          }
        }

当从 RecyclerPool 取出 ViewHolder 时,调用了 resetInternal 这个函数的作用是清空一些记录的参数,包括之前记录 ViewHolder 状态的 mFlags。

 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);
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder,offsetPosition,position,deadlineNs);
      }

代码再往下走的时候,刚刚清空的 flag 参数这个时候就用到了,holder.isBound() 返回 flase,进入 if 判断,调用 tryBindViewHolderByDeadline 进而调用了 onBindViewHolder

到这里这个逻辑就描述清楚了,所以设置 setViewCacheSize 来调整 mCachedViews 保存 ViewHolder 的大小,就能解决问题。

当然有些特殊的情况,某些位置就不能调用 onBindViewHolder,没关系,可以监听 RecyclerView 的滑动,当滑动停止的时候,再调用 notify 刷新下列表也是可以的。

好了本文到这里就结束了~希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

关于Android源码解读——RecyclerView回收复用机制recyclerview的回收复用机制的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于Android ListView与RecyclerView对比浅析--缓存机制、Android MotionLayout + Recyclerview,recyclerview中的视图不可单击、Android RecyclerView、Android RecyclerView 复用错乱通用解法详解的相关信息,请在本站寻找。

本文标签: