RecyclerView$Adapter 的:
setHasStableIds(boolean)
- 在什么场景下我们会考虑设置为true?
- 设置为true会带来什么好处?
本问题来自于:@风风风筝 的提问,提问入口:每日一问 问答征集,欢迎大家踊跃提问。
更多问答 >>
-
每日一问 | 属性动画与硬件加速的相遇,不是你想的那么简单?
2020-10-26 23:45 -
每日一问 | 当Unsafe遇上final,超神奇的事情发生了?
2020-11-02 00:16 -
每日一问 | Call requires API level 23 (current min is 14) 扫描出来的原理是?
2020-12-27 22:39 -
每日一问 | View invalidate() 相关的一些细节探究~
2020-12-27 22:38 -
每日一问 | RxJava中Observable、Flowable、Single、Maybe 有何区别?
2021-01-03 20:34 -
每日一问 | 玩转 Gradle,可不能不熟悉 Transform,那么,我要开始问了。
2020-10-26 23:45 -
每日一问 | 启动了Activity 的 app 至少有几个线程?
2020-10-12 00:47 -
2020-10-03 11:43
-
2020-09-09 23:54
-
2020-08-26 21:11
先来了解下这个方法是用来做什么的:
官方文档的解释是,如果set为true则表示每个Item都是唯一的。好叭,这个描述好像没什么帮助,还是来看代码吧:它这里只是把参数赋值给mHasStableIds而已,与之对应的是
hasStableIds
方法:在RecyclerView源码中搜
.hasStableIds()
会看到一共有14处用到了这个方法,我们重点看两个地方:这个方法是用来回收ViewHolder的,通常会在RecyclerView重新布局子View时使用。
在之前的问题中【RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?】我们分析过,RecyclerView的缓存分两种:一种是数据还有效的,另一种则相反,是需要重新绑定数据的。那么刚好就对应了上面这个scrapOrRecycleView
方法的逻辑:如果这个ViewHolder已经被标记无效而且还没有从RecyclerView中移除的话,就会先移除,然后尝试把它put到RecycledViewPool里面去(当然了,里面还会进一步判断能不能放在mCachedViews
中)。否则呢,就先detach,然后放到mAttachedScrap
或mChangedScrap
中。注意!!!那个if的条件里,还有个!mRecyclerView.mAdapter.hasStableIds()
,也就是说,只要mHasStableIds
为true,就算这个ViewHolder被标记无效,也不会扔到mCachedViews
/RecycledViewPool中,而是放在mAttachedScrap
/mChangedScrap
里面!有什么区别吗?在上面提到的那个问题中也有解释:mAttachedScrap
/mChangedScrap
只在RecyclerView布局子View时使用,也就是它们的 “有效期” 是在RecyclerView的onLayout
方法结束前,当子View布局完成之后,这两个集合都会被清空(进一步放到mCachedViews
/RecycledViewPool中)。所以可以断定,hasStableIds
是跟重新布局子View有关。好,我们接着来看下一个调用到
hasStableIds
方法的地方:如果Adapter没有设置
hasStableIds
为true,就会调用recycleAndClearCachedViews
方法:emmmm,清空
也就是说,当mCachedViews
(把mCachedViews
中所有ViewHolder都放到RecycledViewPool中)。markKnownViewsInvalid
方法被调用时,如果Adapter的hasStableIds
不为true,那么当前RecyclerView的所有 "有效" 缓存都会被清空。这个方法只在
processDataSetCompletelyChanged
中有使用:继续看
processDataSetCompletelyChanged
在哪里使用:嗯,在重新设置Adapter时调用,完全合理。
等等,还有一个地方:!!!!!
有同学可能已经知道了!这个onChanged
会在AdapterDataObservable的notifyChanged
方法中调用:而这个
notifyChanged
:
捋一捋:在默认情况下,调用Adapter的notifyDataSetChanged
!!!notifyDataSetChanged
方法,对应RecyclerView的所有 "有效" 缓存都会变成 "无效" 缓存,也就是全部都要经过onBindViewHolder
重新绑定一次数据。但如果Adapter的hasStableIds
为true的话,则可以继续保留这些有效缓存。
来写个Demo看看叭:我们在RecyclerView每次布局和滚动的前后,都打印一次各个缓存集合的元素,以及在Adapter中打印ViewHolder的Create、Bind、Recycle等情况。最后给Item设置点击和长按事件:notifyDataSetChanged
时,有效缓存的保留与不保留,分别会有怎么样的效果呢?点击Item后,会在下面新增一个Item,长按Item则删除。注意现在用的都是
篇幅原因,其他代码先不贴了,有同学想要我再发出来。好,现在运行看下效果:先是没有notifyDataSetChanged
方法。setHasStableIds
为true的新增Item:看看log:稍微有点长,我们把它分为5部分:
先看第1个:After scrollVerticallyBy,就是在RecyclerView处理完垂直滚动后打印的,可以看到这时候只有
mCachedViews
里面有3个Holder的缓存,其他三个都是空的;这里才是点击Item之后的逻辑,可以看到它连续回收了3个Holder,其实就是把
mCachedViews
的所有缓存,都移动到了mRecyclerPool
中(跟我们上面分析的一样);开始布局Item,这里同样会先回收一些Holder,与上面不同的是,这里是回收已经Attach到RecyclerView中的Holder,也就是正在显示的那些Item; 当所有Item都被回收之后,会看到
mRecyclerPool
已经满载了(默认情况下最多缓存数量是5个);回收了Holder之后,接着就是拿出来重新绑定数据了,但是,此时
mRecyclerPool
中只有5个,实际上要Attach 7个到RecyclerView中,所以只能Create 2个Holder了;布局完成,会看到此时的4个缓存集合都是空的;
嗯,现在来看下长按删除:
log:其实也跟刚刚的新增Item差不多,同样是先将所有ViewHolder扔到
mRecyclerPool
中,然后重新拿出来绑定数据,但是mRecyclerPool
默认最大容量是5,一共需要7个Item,所以只能Create2个了。布局完成之后,RecyclerView的缓存依然是空的。好,这次加上
setHasStableIds(true)
并重写getItemId
方法:来看看效果:
!!!居然有动画了!看看log:emmmm,很明显,这次在布局前一个Item都没有被回收。
仔细看,在布局时还有个细节:setHasStableIds为true之后,虽然notifyDataSetChanged
同样会导致当前Item全部重新绑定一次数据,但除了新增的【Appended Item 27】,其他Item的数据在Bind前后并没有变化,说明这些Holder并不是从mRecyclerPool
中取出来的。还有,【Appended Item 27】是复用了原来的【Item 6】,往上看,【Item 6】其实是存放在mRecyclerPool
中的,在布局完成之后可以看到,mRecyclerPool
已经被掏空了。mAttachedScrap
中存了个【Item 30】,它其实就是刚刚被新增的【Appended Item 27】挤出屏幕的那个。在RecyclerView布局完成之后,它就被回收了,可以看到最后它会移到mRecyclerPool
里面去。最后我们来看看删除的:
同样也是会播放动画。log:onBindViewHolder
时,所有Item的数据都没有乱,被删除的那个【Item 27】同样也是被扔到mAttachedScrap
里面,布局完成后,移到mRecyclerPool
中,mCachedViews
的缓存无变化。总结
setHasStableIds
这个方法主要跟notifyDataSetChanged
有关;当HasStableIds被set为true时,通过
scrapOrRecycleView
方法回收的Holder则永远不会放到mRecyclerPool
中;重新设置Adapter和调用
notifyDataSetChanged
方法时,RecyclerView的有效缓存依然会保留;由
notifyDataSetChanged
发起的重新布局,如果HasStableIds为true,则会最大程度保证不Create Holder。虽然当前所有Attached Item都会重新绑定一次数据,但mCachedViews
中的缓存还在,所以后面如果要显示的Item刚好在mCachedViews
中,就不用重新绑定数据了,理论上会提高流畅度(当然了,这么小的差距肉眼根本无法感知的)。睡觉。
请问打印信息中,楼主是如何获得mRecyclerPoor中的值的呀?
能否发一下代码看看
用反射获取
代码在这里: https://wanandroid.com/blogimgs/066b77d1-fea8-4878-8d69-cd8c08fb6df6.kt
好的,看到了,其它变量好反射获取到,就是那个不好获取,现在已经知道了,非常感谢,另外还有个问题是:我看源码默认这个mCachedViews大小是2,我也没有修改这个大小,为什么我看你的打印日志中,也有 ...查看更多
好的,看到了,其它变量好反射获取到,就是那个不好获取,现在已经知道了,非常感谢,另外还有个问题是:我看源码默认这个mCachedViews大小是2,我也没有修改这个大小,为什么我看你的打印日志中,也有存在存3个元素的情况,我的日志是最少2个最多能存5个,这是怎么回事?我用的RecyclerView 是v7:30.0.0这个版本的。
我知道了,因为RecyclerView 的预取机制,提前缓存到 mCachedViews 中 了。
该方法为获取recyclerView ViewHolder缓存
判断当前viewHolder和屏幕上的是否是同一个viewholder
##简单的说两个作用
1.RecyclerView的缓存规则相关了,其中第一重缓存
getChangedScrapViewForPosition
change 丢弃的viewholder,第三重缓存getScrapOrCachedViewForId
规则就是根据这个ID去获取的。所以从理论上来说,设置了true的情况下,其在recyclerview 内的缓存机制会得到更充分的使用。
正式用例,真实场景。TV开发,原本焦点定位在RecycleVIew的第5个位置。结果notifyDataChange之后,焦点定位到第1个位置。
这个时候设置setHasStableIds(true) 可以让焦点继续保存在第5个的位置。小缘大佬的结论:
“在默认情况下,调用Adapter的notifyDataSetChanged方法,对应RecyclerView的所有 "有效" 缓存都会变成 "无效" 缓存,也就是全部都要经过onBindViewHolder重新绑定一次数据。但如果Adapter的hasStableIds为true的话,则可以继续保留这些有效缓存”
确实之前存在这个,也是设置了这个属性,刷新的时候,就好啦.
测试发现个问题,我给item加了个EditText, 当setHasStableIds为true的时候,删除或者修改数据,然后notifyDataSetChanged,界面没有任何变化,除非滑动页面。 ...查看更多
测试发现个问题,我给item加了个EditText, 当setHasStableIds为true的时候,删除或者修改数据,然后notifyDataSetChanged,界面没有任何变化,除非滑动页面。 setHasStableIds为false的时候,界面上的数据会立马变化的,当然了,焦点回到第一个item上了。 你那真实场景可以刷新数据?
把editText设置成不可强占焦点试试
在看公司tv项目代码,发现很多地方有这个api,然后找到了这个问题的回答,原来还有这样的因素在,解决焦点问题,但是带来的就是缓存多了要吃掉更多的内存?
估计都在放假 -,- 鄙人小白 抛砖引玉下。
第一次回答,还望多多指正 (继续看你们秀朋友圈了 云旅游)
节日快乐呀,fix了一下网络不好,多次点击,重复提交的问题。
同乐 受宠若惊。 我的 出租屋网络不好 哈哈 周末别加班了 好好陪陪孩子
当设置了以上的 开关后,detachAndScrapAttachedViews -> scrapOrRecycleView 的 recycler.scrapView(view);
// 将缓存到 mAttachedScrap,最后直接使用,不需要创建,绑定数据,优化喔!!使用场景:位置固定,比如,广告位。不会改变数量合理,保存在内存中没啥关系。