欢迎访问 生活随笔!

凯发k8官方网

当前位置: 凯发k8官方网 > 编程资源 > 编程问答 >内容正文

编程问答

recyclerview item点击无效-凯发k8官方网

发布时间:2024/10/14 编程问答 13 豆豆
凯发k8官方网 收集整理的这篇文章主要介绍了 recyclerview item点击无效_让你彻底掌握recyclerview的缓存机制 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

点击上方蓝字关注 ??

来源:肖邦kakahttps://www.jianshu.com/p/3e9aa4bdaefd

前言

recyclerview这个控件几乎所有的android开发者都使用过(甚至不用加几乎),它是真的很好用,完美取代了listview和gridview,而recyclerview之所以好用,得益于它优秀的缓存机制。关于recyclerview缓存机制,更是需要我们开发者来掌握的。本文就将先从整体流程看recyclerview的缓存,再带你从源码角度分析,跳过读源码的坑,最后用一个简单的demo的形式展示出来。在开始recyclerview的缓存机制之前我们先学习关于viewholder的知识。

recyclerview为什么强制我们实现viewholder模式?

关于这个问题,我们首先看一下listview。listview是不强制我们实现viewholder的,但是后来google建议我们实现viewholder模式。我们先分别看一下这两种不同的方式。

其实这里我已经用红框标出来了,listview使用viewholder的好处就在于可以避免每次getview都进行findviewbyid()操作,因为findviewbyid()利用的是dfs算法(深度优化搜索),是非常耗性能的。而对于recyclerview来说,强制实现viewholder的其中一个原因就是避免多次进行findviewbyid()的处理,另一个原因就是因为itemview和viewholder的关系是一对一,也就是说一个viewholder对应一个itemview。这个viewholder当中持有对应的itemview的所有信息,比如说:position;view;width等等,拿到了viewholder基本就拿到了itemview的所有信息,而viewholder使用起来相比itemview更加方便。recyclerview缓存机制缓存的就是viewholder(listview缓存的是itemview),这也是为什么recyclerview为什么强制我们实现viewholder的原因。

listview的缓存机制

在正式讲recyclerview的缓存机制之前还需要提一嘴listview的缓存机制,不多bb,先上图:

listview的缓存有两级,在listview里面有一个内部类 recyclebin,recyclebin有两个对象active view和scrap view来管理缓存,active view是第一级,scrap view是第二级。

  • active view:是缓存在屏幕内的itemview,当列表数据发生变化时,屏幕内的数据可以直接拿来复用,无须进行数据绑定。

  • scrap view:缓存屏幕外的itemview,这里所有的缓存的数据都是"脏的",也就是数据需要重新绑定,也就是说屏幕外的所有数据在进入屏幕的时候都要走一遍getview()方法。再来一张图,看看listview的缓存流程

  • 当active view和scrap view中都没有缓存的时候就会直接create view。

小结

listview的缓存机制相对比较好理解,它只有两级缓存,一级缓存active view是负责屏幕内的itemview快速复用,而scrap view是缓存屏幕外的数据,当该数据从屏幕外滑动到屏幕内的时候需要走一遍getview()方法。

recyclerview的缓存机制

先上图:

recyclerview的缓存分为四级

  • scrap

  • cache

  • viewcacheextension

  • recycledviewpool

scrap对应listview 的active view,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。

cache 刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据fifo原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。cache里面的数据是干净的,也就是携带了原来的viewholder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。举个栗子:当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时recyclerview就从cache里面找position=0的数据,如果找到了就直接拿来复用。

viewcacheextension是google留给开发者自己来自定义缓存的,这个viewcacheextension我个人建议还是要慎用,因为我扒拉扒拉网上其他的博客,没有找到对应的使用场景,而且这个类的api设计的也有些奇怪,只有一个public abstract view getviewforpositionandtype(@nonnull recycler recycler, int position, int type);让开发者重写通过position和type拿到viewholder的方法,却没有提供如何产生viewholder或者管理viewholder的方法,给人一种只出不进的赶脚,还是那句话慎用。

recycledviewpool刚才说了cache默认的缓存数量是2个,当cache缓存满了以后会根据fifo(先进先出)的规则把cache先缓存进去的viewholder移出并缓存到recycledviewpool中,recycledviewpool默认的缓存数量是5个。recycledviewpool与cache相比不同的是,从cache里面移出的viewholder再存入recycledviewpool之前viewholder的数据会被全部重置,相当于一个新的viewholder,而且cache是根据position来获取viewholder,而recycledviewpool是根据itemtype获取的,如果没有重写getitemtype()方法,itemtype就是默认的。因为recycledviewpool缓存的viewholder是全新的,所以取出来的时候需要走onbindviewholder()方法。再来张图看看整体流程

这里大家先记住主要流程,并且记住各级缓存是根据什么拿到viewholder以及viewholder能否直接拿来复用,先有一个整体的认识,下面我会带着大家再简单分析一下recyclerview缓存机制的源码。

阅读recyclerview缓存机制源码

由于篇幅和内容的关系,我不可能带大家一行一行读,这里我只列出关键点,还有哪些需要重点看,哪些可以直接略过,避免大家陷入读源码一个劲儿钻进去出不来的误区。当recyclerview绘制的时候,会走到layoutmanager里面的next()方法,在next()里面是正式开始使用缓存机制,这里以linearlayoutmanager为例子

/**
* gets the view for the next element that we should layout.
* also updates current item index to the next item, based on {@link #mitemdirection}
*
* @return the next element that we should layout.
*/
view next(recyclerview.recycler recycler) {if (mscraplist != null) {return nextviewfromscraplist();
}final view view = recycler.getviewforposition(mcurrentposition);
mcurrentposition = mitemdirection;return view;
}

在next方法里传入了recycler对象,这个对象是recyclerview的内部类。我们先去看一眼这个类

public final class recycler {final arraylist mattachedscrap = new arraylist<>();
arraylist mchangedscrap = null;final arraylist mcachedviews = new arraylist();private final list
munmodifiableattachedscrap = collections.unmodifiablelist(mattachedscrap);private int mrequestedcachemax = default_cache_size;int mviewcachemax = default_cache_size;
recycledviewpool mrecyclerpool;private viewcacheextension mviewcacheextension;static final int default_cache_size = 2;
}

再看一眼recycledviewpool的源码

public static class recycledviewpool {private static final int default_max_scrap = 5;static class scrapdata {final arraylist mscrapheap = new arraylist<>();int mmaxscrap = default_max_scrap;long mcreaterunningaveragens = 0;long mbindrunningaveragens = 0;
}
sparsearray mscrap = new sparsearray<>();

其中mattachedscrap对应scrap;mcachedviews对应cache;mviewcacheextension对应viewcacheextension;mrecyclerpool对应recycledviewpool。注意:mattachedscrap、mcachedviews和recycledviewpool里面的mscrapheap都是arraylist,缓存被加入到这三个对象里面实际上就是调用的arraylist.add()方法,复用缓存呢,这里要注意一下不是调用的arraylist.get()而是arraylist.remove(),其实这里也很好理解,因为当缓存数据被取出来展示到了屏幕内,自然就应该被移除。我们现在回到刚才的next()方法里,recycler.getviewforposition(mcurrentposition); 直接去看getviewforposition这个方法,接着跟到了这里

view getviewforposition(int position, boolean dryrun) {return trygetviewholderforpositionbydeadline(position, dryrun, forever_ns).itemview;
}

接着跟进去

viewholder trygetviewholderforpositionbydeadline(int position,boolean dryrun, long deadlinens){if (position < 0 || position >= mstate.getitemcount()) {throw new indexoutofboundsexception("invalid item position " position
"(" position "). item count:" mstate.getitemcount()
exceptionlabel());
}boolean fromscraporhiddenorcache = false;
viewholder holder = null;// 0) if there is a changed scrap, try to find from thereif (mstate.isprelayout()) {
holder = getchangedscrapviewforposition(position);
fromscraporhiddenorcache = holder != null;
}// 1) find by position from scrap/hidden list/cacheif (holder == null) {
holder = getscraporhiddenorcachedholderforposition(position, dryrun);
}if (holder == null) {final int type = madapter.getitemviewtype(offsetposition);// 2) find from scrap/cache via stable ids, if existsif (madapter.hasstableids()) {
holder = getscraporcachedviewforid(madapter.getitemid(offsetposition),
type, dryrun);if (holder != null) {// update position
holder.mposition = offsetposition;
fromscraporhiddenorcache = true;
}
}if (holder == null && mviewcacheextension != null) {// we are not sending the offsetposition because layoutmanager does not// know it.final view view = mviewcacheextension
.getviewforpositionandtype(this, position, type);if (view != null) {
holder = getchildviewholder(view);
}
}if (holder == null) { // fallback to poolif (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);
}
}
}if (holder == null) {long start = getnanotime();if (deadlinens != forever_ns
&& !mrecyclerpool.willcreateintime(type, start, deadlinens)) {// abort - we have a deadline we can't meetreturn 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);
}
}
}
}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);
}return holder;
}

终于到了缓存机制最核心的地方,为了方便大家阅读,我对这部分源码进行了删减,直接从官方给的注释里面看。

// (0) if there is a changed scrap, try to find from thereif (mstate.isprelayout()) {
holder = getchangedscrapviewforposition(position);
fromscraporhiddenorcache = holder != null;
}

这里面只有设置动画以后才会为true,跟咱们讲的缓存也没有多大关系,直接略过。

// 1) find by position from scrap/hidden list/cacheif (holder == null) {
holder = getscraporhiddenorcachedholderforposition(position, dryrun);
}

这里就开始拿第一级和第二级缓存了getscraporhiddenorcachedholderforposition()这个方法可以深入去看以下,注意这里传的参数是position(dryrun这个参数不用管),就跟我之前说的,scrap和cache是根据position拿到缓存。

if (holder == null && mviewcacheextension != null) {// we are not sending the offsetposition because layoutmanager does not// know it.
final view view = mviewcacheextension
.getviewforpositionandtype(this, position, type);if (view != null) {
holder = getchildviewholder(view);
}
}


这里开始拿第三级缓存了,这里我们不自定义viewcacheextension就不会进入判断条件,还是那句话慎用。

if (holder == null) { // fallback to poolif (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);
}
}
}

这里到了第四级缓存recycledviewpool,getrecycledviewpool().getrecycledview(type);通过type拿到viewholder,接着holder.resetinternal();重置viewholder,让其变成一个全新的viewholder

if (holder == null) {long start = getnanotime();if (deadlinens != forever_ns
&& !mrecyclerpool.willcreateintime(type, start, deadlinens)) {// abort - we have a deadline we can't meetreturn 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);
}
}
}

到这里如果viewholder还为null的话,就会create view了,创建一个新的viewholder

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

这里else if (!holder.isbound() || holder.needsupdate() || holder.isinvalid())是判断这个viewholder是不是有效的,也就是可不可以复用,如果不可以复用就会进入trybindviewholderbydeadline(holder, offsetposition, position, deadlinens);这个方法,在这里面调用了bindviewholder()方法。点进去看一眼

private boolean trybindviewholderbydeadline(@nonnull viewholder holder, int offsetposition,
int position, long deadlinens) {
....................madapter.bindviewholder(holder, offsetposition);
....................return true;
}

在点进去就到了我们熟悉的onbindviewholder()

public final void bindviewholder(@nonnull vh holder, int position) {
.......................onbindviewholder(holder, position, holder.getunmodifiedpayloads());
.........................
}

至此,缓存机制的整体流程就全部分析完毕了。

小结

listview有两级缓存,分别是active view和scrap view,缓存的对象是itemview;而recyclerview有四级缓存,分别是scrap、cache、viewcacheextension和recycledviewpool,缓存的对象是viewholder。scrap和cache分别是通过position去找viewholder可以直接复用;viewcacheextension自定义缓存,目前来说应用场景比较少却需慎用;recycledviewpool通过type来获取viewholder,获取的viewholder是个全新,需要重新绑定数据。当你看到这里的时候,面试官再问recyclerview的性能比listview优化在哪里,我想你已经有答案。

通过demo理解

担心你看完上面的内容,倒头就忘,我们写个简单的demo通过打印log的方式来巩固一下学到的知识。简单说一下demo里面需要注意的代码,下面是对recyclerview的一个包装

public class recyclerviewwrapper extends recyclerview {private layoutlistener layoutlistener;public recyclerviewwrapper(@nonnull context context) {super(context);
}public recyclerviewwrapper(@nonnull context context, @nullable attributeset attrs) {super(context, attrs);
}public recyclerviewwrapper(@nonnull context context, @nullable attributeset attrs, int defstyle) {super(context, attrs, defstyle);
}public void setlayoutlistener(layoutlistener layoutlistener) {this.layoutlistener = layoutlistener;
}@overrideprotected void onlayout(boolean changed, int l, int t, int r, int b) {if (layoutlistener != null) {
layoutlistener.onbeforelayout();
}super.onlayout(changed, l, t, r, b);if (layoutlistener != null) {
layoutlistener.onafterlayout();
}
}public interface layoutlistener {void onbeforelayout();void onafterlayout();
}
}

其实很简单,在recyclerview执行onlayout()方法前后执行一下咱们打印缓存变化的方法

再看一眼打印缓存变化的方法,利用反射的技术

/**
* 利用java反射机制拿到recyclerview内的缓存并打印出来
* */private void showmessage(recyclerviewwrapper rv) {try {
field mrecycler =
class.forname("androidx.recyclerview.widget.recyclerview").getdeclaredfield("mrecycler");
mrecycler.setaccessible(true);
recyclerview.recycler recyclerinstance = (recyclerview.recycler) mrecycler.get(rv);
class> recyclerclass = class.forname(mrecycler.gettype().getname());
field mviewcachemax = recyclerclass.getdeclaredfield("mviewcachemax");
field mattachedscrap = recyclerclass.getdeclaredfield("mattachedscrap");
field mchangedscrap = recyclerclass.getdeclaredfield("mchangedscrap");
field mcachedviews = recyclerclass.getdeclaredfield("mcachedviews");
field mrecyclerpool = recyclerclass.getdeclaredfield("mrecyclerpool");
mviewcachemax.setaccessible(true);
mattachedscrap.setaccessible(true);
mchangedscrap.setaccessible(true);
mcachedviews.setaccessible(true);
mrecyclerpool.setaccessible(true);int mviewcachesize = (int) mviewcachemax.get(recyclerinstance);
arraylistwrapper mattached =
(arraylistwrapper) mattachedscrap.get(recyclerinstance);
arraylist mchanged =
(arraylist) mchangedscrap.get(recyclerinstance);
arraylist mcached =
(arraylist) mcachedviews.get(recyclerinstance);
recyclerview.recycledviewpool recycledviewpool =
(recyclerview.recycledviewpool) mrecyclerpool.get(recyclerinstance);
class> recyclerpoolclass = class.forname(mrecyclerpool.gettype().getname());
log.e(tag, "mattachedscrap(一缓) size is:" mattached.maxsize ", \n" "mcachedviews(二缓) max size is:" mviewcachesize ","
getmcachedviewsinfo(mcached) getrvpoolinfo(recyclerpoolclass, recycledviewpool));
} catch (exception e) {
e.printstacktrace();
}
}

核心的代码呢就这两块,文章的最后我会把我的demo上传到github上。注意:本文使用的recyclerview的版本是androidx,在调onattachedtowindow()方法的时候会进行版本判断,如果是5.0以及以上的系统(即大于等于21),gapworker会把recyclerview自己加入到gapworker。在renderthread线程执行预取操作的时候会mprefetchmaxcountobserved = 1,这就会导致你使用5.0以及以上系统的手机打印缓存数量的时候会比你预想的多一个。这里为了不造成这种问题,本文使用4.4系统的android模拟器来演示demo。

demo演示效果截图

  • 启动app,第一次加载的情况

初始化加载只有屏幕内的一级缓存7个

  • 把position = 0 和position=1 两个item移除屏幕

看蓝色框出来的,position = 0 和position = 1的item被加入到了cache缓存中,cache的缓存数量我没有修改,默认2个,也就说现在已经满了

  • 再把position = 2的item也移除屏幕

因为上一步cache里面的缓存已经慢了,此时position = 2又被加入缓存,根据fifo的原则,cache里面position = 0 被remove掉并加入到了四级缓存recycledview里面,此时recycledview也有了缓存并且该缓存没有任何有效数据信息。

  • 再上一步的基础上下拉一下,把position = 2的item显示出来

此时position = 2的item将要被显示出来,会先从cache里面找,发现cache正好有position = 2的缓存就直接拿出来复用了,并且原来在屏幕里的position= 9 的item被移除了,就会加入到cache的缓存里。现在看一下oncreateviewholder()和onbindviewholder()的情况

  • 还是启动app,第一次加载后,再把position = 0和position =1的item移除屏幕再移回来

onbindviewholder()方法没有被重复执行(静态图显示的效果不是很好,gif录制的质量太差了,还是建议下载demo自己尝试一下)

  • 最后留一个问题给大家


为什么在第10次oncreateviewholder()执行以后就再也没有执行过oncreateviewholder()方法了?

总结

关于recyclerview的缓存分为四级,scrap、cache、viewcacheextension和recycledviewpool。scrap是屏幕内的缓存一般我们不怎么需要特别注意;cache可直接拿来复用的缓存,性能高效;viewcacheextension需要开发者自定义的缓存,api设计比较奇怪,慎用;recycledviewpool四级缓存,可以避免用户调用oncreateviewholder()方法,提高性能,在viewpager recyclerview的应用场景下可以大有作为。以上就是关于recyclerview缓存的所有内容,另外要备注一下,就是文章的图片上有些单词打错了,实在是懒得重画了,以文本的内容为准,请大家见谅。

最后是github的地址:

https://github.com/kaka10xiaobang/recyclerviewcachedemo

—————end—————

总结

以上是凯发k8官方网为你收集整理的recyclerview item点击无效_让你彻底掌握recyclerview的缓存机制的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得凯发k8官方网网站内容还不错,欢迎将凯发k8官方网推荐给好友。

网站地图