mmarket进击的巨人英文RecyclerViewnotifyDataSetChanged导致图⽚闪烁的真凶
采⽤ViewPager+TabLayout ,已实现数据懒加载)都会缓存第⼀页数据(⽹络)存
⽬前,在项⽬中⼀些主要页⾯(如图1 ⾸页,采⽤ViewPager+TabLayout ,已实现数据懒加载
never let you goDB,下次进来时会先请求DB数据,然后再请求⽹络数据,这样⽤户体验⽐较好。之前我们主要页⾯都是使⽤RadioPullToRefreshListView(封装的PullToRefreshListView,后⾯简称ListView)⽅式实现(刷新⽅式使⽤的是notifyDataSetChanged),这样不管DB中的数据和⽹路请求数据是否⼀样,都会刷新两次,体验上没有什么问题,⾃从4.0版本陆续改⽤RecyclerView ⽅式实现,发现有图⽚加载的地⽅会闪烁⼀下(录屏效果不怎么好,这⾥就不放图了,脑补⼀下)。
图1 企鹅FM ⾸页
仅仅只是⽤RecyclerView 替换ListView 逻辑主体并未修改,因此⽬光很快锁定在RecyclerView上⾯,⽹上⼀搜索,类似问题有很多,⼤
⼤多说是没有设置tStableIds(true) 和复写getItemId()导致的。
多说是没有设置tStableIds(true) 和复写getItemId()导致的
照葫芦画瓢,我给对应的Adapter 设置了tStableIds(true) 和复写getItemId(),
tStableIds(true) 和复写getItemId(),发现闪络问题依然存在,⾄于为什么当时没解决其实可以解决,后⾯给原因),就放弃尝试这种⽅式。
(其实可以解决,后⾯给原因
我们都知道RecylerView 在ListView 的基础上新增了 很多种刷新⽅式,既然notifyDataSetChanged()不⾏ ,于是乎改⽤
了notifyItemRangeChanged(int positionStart, int itemCount),发现改⽅法竟然解决了上述闪烁问题。
⼀ 、对⽐两种刷新⽅式异同点
(1)notifyDataSetChanged()最后作⽤于onChanged
天然气英语
图 2 onChanged()
唯独可疑的是:RecyclerView.this.tDataSetChangedAfterLayout(),将ViewHolder标记为更新或⽆⽤,暂时记录⼀下,后续会讲。
图3 RecyclerView.this.tDataSetChangedAfterLayout()
(2)RecyclerView 中刷新(如notifyItemRangeChanged())
口语交际练习
notifyItemRangeChanged 等⽅法最后,都会调⽤triggerUpdateProcessor() ,由于主页⾯基本都是多ViewType 情况,⼀般不会设置HasFixedSize为true,因⽽最终只会调⽤el 分⽀去。
图4 triggerUpdateProcessor()最好听的背景音乐
从(1)和(2)中,两种刷新差别,仅仅在于⽅法(1)会更改ViewHolder的标志,共同点都会调⽤requestLayout(),导致RecyclerView 重新测量和布局,到此还是看不出闪烁问题的原因所在。另外还有⼀个重要实验未说:如果单纯的只进⾏⽹络刷新的话,图像闪烁情况基本没有(页⾯跳转导致闪烁不明显)。种种迹象表明,可能是数据刷新过程中View未复⽤,从新inflate耗时亦或是其他原因,导致闪烁。
接下来,我们只能从RecyclerView 的测量和布局来分析,找出蛛丝马迹。
⼆、简要分析R ec yc ler View 的测量、布局及缓存策略
(1) RecyclerView::onMeasure()
RecyclerView 的onMeasure 相对⽐较简单 ,具体如下:⽬前官⽅提供的LayoutManager的mAutoMeasure为true ,项⽬中RecyclerView 的宽⾼是精确的,因⽽skipMeasure 为true ,直接return 了。从 RecyclerView::onMeasure() 看不出两种刷新的不同点,接下来看看 onLayout 。
图5 RecyclerView::onMeasure()
(2)RecyclerView::onLayout ()
怎么培养表达能力这⾥不打算细讲,跳过1000字 ,RecyclerView 之所以能够灵活布局,是因为它将onLayout()委托给了LayoutManager ,这⾥以LinearLayoutManager 为例说明:
LinearLayoutManager::onLayoutChildren()(调⽤链 RecyclerView::dispatchLayout()->dispatchLayoutStep2() -LinearLayoutManager::onLayoutChildren()
>onLayoutChildren()
onLayoutChildren()) ,在onLayoutChildren() 讲解两个⽐较重要的函数:detachAndScrapAttachedViews(recycler) ⽤于处理ViewHolder复⽤(View复⽤),fill()执⾏布局。
图 6:LinearLayoutManager::onLayoutChildren()
1)detachAndScrapAttachedViews()
detachAndScrapAttachedViews() 会循环遍历所有的View ,根据ViewHolder 的状态 及Adapter.hasStableIds() 来进⾏不同操作。分⽀1:
ViewHolder ⽆⽤&&没有Remove&&hasStableIds为fal ,会删除对应View,同时根据条件选择 保存在mCachedViews
和 addViewHolderToRecycledViewPool ;
分⽀2:
先detachView ,同时根据条件保存在 mChangedScrap 和 mAttachedScrap 中;
图7:detachAndScrapAttachedViews()
看到上⾯的条件,是不是很熟,在notifyDataSetChanged 时,会先将ViewHolder 设置成⽆效。在此打断点,发现 单独notifyDataSetChanged() 情况,确实会⾛进if 分⽀(notifyItemRangeChanged()会进⼊el 分⽀),那设置hasStableIds(true)应该不会吧,发现竟然还是⾛进去了 ,纳尼
仔细⼀看,对应的Adapter 不对,竟然是封装的RecyclerView组件,内部的WrapAdapter (⽅便实现加载更多等操作),对应修改组件内的WrapAdapter ,设置hasStableIds(true) 和 复写getItemId(),仍然使⽤notifyDataSetChanged ,闪烁问题果然没有了 。
根据detachAndScrapAttachedViews()发现,两种刷新⽅式不同在于对ViewHoler的缓存⽅式不⼀样,那先看看布局时是如何复⽤的,fill() 函数是真正进⾏布局的:
图8 fill()调⽤链
从上可以看到 View 是通过next()⽅式获取 :
next()-&ViewForPosition()->tryGetViewHolderForPositionByDeadline()
图9 tryGetViewHolderForPositionByDeadLine()
从上可以看到ViewHolder创建或复⽤的过程,为了弄懂⾥⾯的流程,我们接着简要介绍⼀下RecyclerView的四级缓存是如何实现的。(3)RecyclerView 的四级缓存
1)屏幕内缓存
mChangedScrap: 表⽰数据已经改变的ViewHolder
mAttachedScrap : 表⽰未与RecyclerView分离的ViewHolder
littlebear>英音美音
mAttachedScrap : 表⽰未与RecyclerView分离的ViewHolder
四年级英语上册教案
2)屏幕外缓存
mCachedViews:屏幕划出屏幕时,ViewHolder会被缓存在mCachedViews (默认 2)
3)⾃⼰实现ViewCacheExtension类实现⾃定义缓存
4)缓存池
tryGetViewHolderForPositionByDeadLine()中操作,可细化为如下流程图(⽹络盗图):