RecyclerView 体验优化及入坑总结

     本文所讲RecyclerView 是来自support 库 26 版本,本文主要来源于自身开发及组内同事遇到问题的经验总结,作为知识沉淀记录一下,以备日后查看。

本文主要讲解以下几部分:

(1)RecyclerView 滑动体验篇

       1)横向ViewPager 与内嵌横向RecyclerView 之间的滑动冲突;

       2)纵向RecycleView/ListView 与 横向RecycleView 之间的滑动冲突;

       3)横向RecyclerView  ItemView 滑动不停留在中间态;

       4)记录、恢复RecyclerView 滚动偏移位置;

(2)RecyclerView 入坑篇

       1)RecyclerView 导致的内存泄漏(support 26 + 7.0以下机型);

       2)RecyclerView调用notifyDataSetChanged 会闪烁;

       3)RecycleView /ListView 设置itemView 为View.GONE 效果等同于View.Invisible;

(3)RecyclerView 性能提升篇

一、RecyclerView 滑动体验篇

(1)ViewPager 与 横向RecyclerView 之间的滑动冲突

       目前,企鹅FM项目中,很多页面使用ViewPager+ TabLayout (如首页、详情页、搜索结果页等),而对应页面很多时候会嵌套一个横向RecycleView,用来展现更多的信息,如下,在RecycleView中滑动到最后一个元素时,会同时带动ViewPager滑动,这种体验极差。

图 1  ViewPager 内嵌套RecyclerView 示例

原因分析:

       作为子View 的RecyclerView在滑到最后一个或第一个ItemView到导致ViewPager滑动,这一定是ViewPager在此刻对滑动事件进行了拦截,解决的最简单办法就是不让ViewPager拦截横向RecyclerView的滑动事件(即 ViewPager::onInterceptTouchEvent方法返回false),ViewPager::onInterceptTouchEvent中的Move 事件如下:

                     图 2 ViewPager::onInterceptTouchEvent()                                           

目前,有以下两种方式使ViewPager 不去拦截横向RecyclerView 滑动事件:

1)在RecyclerView 对应滑动事件分发中调用        

      getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewPager对其MOVE或者UP事件进行拦截,但是考虑的因素比较多,而且效果不是太好,故放弃这种方式。

2)修改某些方法,进入到上图if判断中

      在滑动横向RecyclerView 到两端时,dx != 0 && !isGutterDrag(mLastMotionX, dx) 肯定满足条件,那说明canScroll() (用来判断一个View以及它的子View是否可以滑动)一定返回了false, 复写canScroll()方法,打log,发现返回果然为false,验证了自己的判断。

解决办法:复写canScroll,当View 是横向RecyclerView(LinearLayoutManager 包含GridLayoutManager)时,直接返回true即可解决问题,解决代码如下:

图3 复写canScroll()方法

      类似的冲突还有ViewPager 和HorizontalScrollView 等等,解决方式与上面类似。 

(2)纵向RecyclerView/ListView 与 横向RecyclerView 之间的滑动冲突

     在有些时候因为产品需求,需要在纵向的RecyclerView/ListView内嵌套一个横向的RecyclerView,当这个横向RecyclerView的item 比高度较大的时候(企鹅FM书城排行榜模块),在横向滑动时,容易导致整体向上滑,体验效果较差,如下图所示(网络盗图) :

图3  右滑横向RecyclerView

       造成上述现象的原因是:外层纵向滑动的RecyclerView对 横向滑动的RecyclerView 的滑动事件进行了拦截,如下图2 所示,canScrollVertically 此刻为true,因此这里仅仅只判断了Math.abs(dy)>mTouchSlop(可以认为是一个滑动阀值,是一个定值8dp) ,并未判断方向或角度,从而决定是否拦截。

图4  RecyclerView::onInterceptTouchEvent()

       解决办法 :既然RecyclerView::onInterceptTouchEvent 内部没有判断滑动角度或方向,那我们就人为去判断,在上面判读的基础上继续判断 Math.abs(dy) 和Math.abs(dx) 的大小,从而决定是否拦截:具体分析细节可参照 , 修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题

        使用上述方法,可以很快解决上述滑动体验问题,那是不是只有上述一种解决方式了,答案是否定的,作为一名Android 开发者我们知道,除了上述方式拦截滑动事件外,我们还可以通过getParent().requestDisallowInterceptTouchEvent(true); 让父RecyclerView不去拦截横向滑动,如下是RecyclerView::onTouchEvent() ,内部已经实现了requestDisallowInterceptTouchEvent(true) 。

       我们需要考虑的是,当我们横向上或横向下滑动时,需要 进入上图中1的判断 ,2的判断还未满足,此时内部横向RecyclerView 会拦截内部itemView的滑动事件,进而执行自己的onTouchEvent事件,从而调用requestDisallowInterceptTouchEvent(true) ,让外层RecyclerView不去拦截内部RecyclerView的横向滑动事件,至此需要解决如何保证先进入1判断而不进入2判断。

图5 RecyclerView::onTouchEvent()

解决办法:通过调整TouchSlop值的大小 

      在开始我们已介绍RecyclerView 的默认TouchSlop 值是8dp,如果要先保证进入1判断条件,必须调大TouchSlop值(反射获取),经过调整TouchSlop (按倍数调整比较简单,可以先知道一个大致范围)验证,当TouchSlop扩大1倍时就能满足条件。

总结:上述两种方式各有优缺点,方法1,对原生RecyclerView 侵入性较强(特别是对RecyclerView 进行多层封装的情况下,影响比较大),优点是TouchSlop 值保持与系统一致,不会带来其他未知问题;方法 2 ,修改方式简单,入侵性小,缺点,需要调整TouchSlop 值,可能还会带来其他问题。

(3)横向RecyclerView  ItemView 滑动不停留在中间态

           如下图所示,正在滑动的模块是书城——排行榜模块,排行榜模块主要由横向RecyclerView 构成,内部包含两个榜单形式,列举前top3的内容,在(2)的基础上解决了纵向RecyclerView 嵌套横向RecyclerView 滑动问题外,还有有个小问题那就是,RecyclerView  ItemView 滑动多少就停在那里,这种效果不是我们想要的,我们想要的是滑到左边就显示第一个榜单,滑到右边就显示第二个榜单。

图6 书城——排行榜模块

那有没有好的办法做到这一点了,官方考虑到这一点,针对RecyclerView 滑动情况,专门提供了SnapHelper类(PagerSnapHelper 和LinearSnapHelper  ,详细介绍介绍可参看Android中使用RecyclerView + SnapHelper实现类似ViewPager效果), 使用其他相当简单,针对上述问题解决方式如下:

图 7 LinearSnapHelper 使用

(4)记录、恢复RecyclerView 滚动偏移位置

        熟悉RecyclerView 缓存的同学应该知道(后面在也会介绍RecyclerView缓存机制),当RecyclerView中的itemView 滑出屏幕后会缓存在mCacheView 中(默认缓存最大数是2),因此当滑出屏幕超过2后,再滑回来,原来的位置信息都会被重置,对于一般的RecyclerView 没有什么影响,但是如果内嵌了一个横向RecyclerView (如下图中分类模块位置) ,起初”悬疑推理“ 在一排第一个位置,向左滑动到其他位置后,再纵向滑动外层RecyclerView ,发现分类模块第一个又变成了”悬疑推理“ ,这个是产品不能接受的。

图 8 未记录RecyclerView 滚动偏移位置

     那如何修正上述问题了,RecyclerView 布局 及位置相关信息都是由对应LayoutManager决定,因此查看对应LayoutManager::onSaveInstanceState() 如下所示,内部确实记录了position及offset 值。

图 9  LinearLayoutManger::onSaveInstanceState

解决办法步骤:

(1)在Adapter::onViewRecycled 中保存对应LayoutManager的onSaveInstanceState ,同时记录保存下来

图 9 Adapter::onViewRecycled()

(2)在setData()数据给Adapter 时,恢复对应LayoutManager 之前保存在数据信息

图 10 setData()

(3)保存记录RecyclerView 后的效果

图 11 已记录RecyclerView 滚动偏移位置

二、RecyclerView 入坑篇

(1)RecyclerView 导致的内存泄漏(support 26 + 7.0以下机型)

         在进行4.0 版本迭代时,发现在之前的广播聚合页存在RecyclerView导致的内存泄漏,下图为内存泄漏的引用链,引用对象可以追到GapWorker。这里的RecyclerView是一个横向的RecyclerView ,作为广播聚合页(ListView)的HeaderView。

图12 RecyclerView 内存泄漏 引用链

       由于广播页面是比较老的页面,最近几个版本也未发现此类泄漏,细细想一下,可能与RecyclerView 版本有关(4.0版本直接将support 库由23.1升级到26.1版本),刚好这几个版本,support 库 修复了修复很多RecyclerView 的bug 及添加了许多新功能。通过AndroidXRef 查询知(查询结果如下),GapWorker 果然是在support 26 新增的。

图13 GapWorker 出现版本

        查看GapWorker ,里面sGapWorker 是一个ThreadLocal 带GapWorker 的对象,同时维持了一个RecyclerView 的List对象(通过add  和remove 方法进行)。

图14 GapWorker 内重要对象

      而GapWorker的add 和remove 方法分别在RecyclerView::onAttachedToWindow 和RecyclerView::onDetachedFromWindow 中调用,如下图所示:

图15 RecyclerView::onAttachedToWindow 和 RecyclerView::onDetachedFromWindow

   根据上面的引用链知,RecyclerView::onDetachedFromWindow 方法 没有被主动调用,断点验证,在退出广播页面的时候也没有调用(导致泄漏),按理说在滑动离屏的时候就应该调用的,难道和RecylerView 做为ListView 的HeaderView 有关,顺着这条思路发现果然和上述使用方式有关。

       之前组内同事chunyu遇到过:ListView 嵌套GridView时,GridView数据错乱问题(7.0及其以上有问题),里面刚好说明了7.0及其以上版本,官方修正了RecylerView 做为ListView 的HeaderView 情况,滑出屏幕,不调用onDetachedFromWindow()的原因,具体如下:

图16  ListView::scrollListItemsBy()
图17  整个调用流程

       从分析中,可以获取到两个重要的信息:1)GapWorker 是在support 26 以上才有的,且SDK_INT>=21,才会进行对应add 和remove 操作 ;2)在SDK_INT< 24(7.0) 时,不会主动调用View::dispatchDetachedFromWindow()。

        因此,上述问题的解决办是:在对应Fragment 的onDetach() 或 其他场景主要去调用上图中的ViewGroup::removeDetachedView()  (这里需要使用反射),具体如下:

图18 在定制ListView中利用反射实现removeDetachedView()

(2)RecyclerView调用notifyDataSetChanged 会闪烁

详见我的另一篇文章:RecyclerView notifyDataSetChanged 导致图片闪烁的真凶

(3)RecycleView /ListView 设置itemView 为View.GONE 效果等同于View.Invisible

  解决办法:将itemView 的宽高设置成 0 ,重新设置一下LayoutParams


三、RecyclerView 性能提升篇

   说是RecyclerView性能提升篇有点夸大 ,这里主要讲讲RecyclerView 使用小技巧

(1)setHasFixedSize(true)优化思想

(2)DiffUtil ()

(3)......

    限于篇幅内容有点多,后续再补充.....  ,有分析不对的地方欢迎指出 ,谢谢 ^_^!


相关引用资料

(1)修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题

(2)Android中使用RecyclerView + SnapHelper实现类似ViewPager效果

  (3)  关于RecyclerView的缓存机制的理解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345