现象描述
卡顿现象视频:进入筛选页,连续下拉刷新页面,刷了很多页之后页面就很卡,初步分析,可以将卡顿现象的原因分解成两个方面:
(1)连续刷新多页之后,上拉刷新时候,等页面回弹时候页面UI发生抖动,有时候甚至卡的动不了;
(2)在不停的下拉刷新的过程中去观察内存占用的情况,发现内存占用出现线性增长的情况,从512M开始增长到1.5G,往后上拉刷新基本上卡的动不了;
问题解决过程
分析卡顿问题,首先要去大概看下,发生卡顿的时候,程序在执行哪些操作,因此我们首先去抓下方法执行的Trace文件去大概的定位下问题出在哪。
在发生卡顿的时候抓取一段trace文件,分析下卡顿的时候程序主要在做些什么。

Android Studio自带的可视化工具展示的方法调用图非常的详细,放大缩小拖拽起来不是很方便,难以观察全局情况。我们可以使用网页工具Perfetto来进行可视化,它能比较方便的放大缩小。
Perfetto UI 地址:
arduino
https://ui.perfetto.dev/
在AndroidStudio抓取到 Call Stack 的 trace之后,导出 .perfotto-trace文件的命令行为:
bash
adb pull /data/local/traces
实时查看内存的命令行为:
adb shell dumpsys meminfo com.heytap.yoli
分析页面卡顿,首先要去抓取CPU的方法执行时序图,抓取之后发现代码一直在执行绘制操作performTraversals()(时间达到秒级),暂时也不知道是啥原因造成的过度绘制,继续往下看。

进一步回到AndroidStudio的trace图去仔细的看,然后找到了一个可疑之处,RecycleView与SmartRefreshLayout在同一时间内一起发生UI的绘制。然后我们分别考虑这两个控件,首先从图中可以看出,RecycleView发生绘制是由于执行了onCreateView(),生成itemView的过程有UI绘制的操作。

然后我们再看SmartRefreshlayout,结合卡顿的现象去看,SmartRefreshlayout需要改变UI的地方就是执行刷新动作了。因此我们可以想到,有很大可能是因为在列表新增数据没有完成UI绘制的时候,SmartRefreshlayout正在执行下拉回弹的动作。


简单修改代码之后再次检查下卡顿情况,刷新时候的卡顿解决了!考虑到下拉刷新,页面回弹这种效果对于几乎满屏展示内容的页面并不好,最好的方式是直接去掉这个回弹的动画,既能节省性能,还能解决冲突,同时能优化用户体验,可谓一举三得!因此,需要在页面回弹完成之后再更新RecycleView的数据。
修改刷新控件SmartRefreshLayout与RecycleView更新UI的冲突之后,再去看看卡顿现象有没有被修复。不断地上拉刷新加载数据之后,发现每次上拉刷新然后页面回弹的这个过程不再出现卡着暂时不动的情况。但是上拉很多次数之后(大概十五六次的样子),页面基本上划不动了(手指滑动,整个页面卡着不动一段时间,然后才出现上拉刷新动画然后回弹),期间观察内存占用情况,一直走高。
于是我们开始分析一下内存占用的情况,抓取 Heap Dump 文件之后,我们找到FilmFilterFragment所对应的类对象。可以看到,在Recycelview的对象里面,mChildren高达几百个!刻意还抓了首页列表的情况,其Recycelview的对象mChildren只有12个,如下图所示:
正常列表内存详情情况:

筛选页列表内存占用情况:

于是,可以想到应该是ViewHolder没有被回收成功,再回去看下代码,结合分析,作出两点修改(1)覆写onViewRecyceled()方法;(2)在Recycelview滚动的时候停止图片加载的任务,在页面停止的时候重启图片加载任务;(3)扩大RecycleView的第四级缓存大小,因为筛选列表是三列,至少需要六个缓存位置,才能完成最上和最下位置的缓存;
在ViewHolderBuilder中,覆写onViewRecyceled()方法:

在Recycelview中,覆写onViewRecyceled()方法:

在FilmFilterFragment中,修改图片加载的逻辑和扩充第四级缓存:


重新编译之后,重复卡顿出现的操作步骤,发现内存依旧直线上涨!抓取 Heap Dump 文件,然后去查看Recycelview的子view情况,依旧是100多个!通过断点和过滤日志发现,RecycleView的onViewRecyceled()方法没有被执行!
onViewRecycled() 方法在 RecyclerView 的视图被回收时被调用。详细来说,当 RecyclerView 确定某个视图不再需要显示,并且可以被重用时,它会调用这个方法。视图的回收主要发生在以下几种情况下:
(1)视图滚出屏幕:当用户滚动 RecyclerView 时,部分视图滚出屏幕,这些不再可见的视图将被回收到缓存池中。
(2)数据发生变化:当数据源发生变化并通知适配器(通过调用 notifyDataSetChanged() 或其他通知方法),RecyclerView 可能会回收并重新绑定视图。
(3)视图被移除:当某个视图从 RecyclerView 中被移除(例如,通过调用notifyItemRemoved()方法,该视图将被回收到缓存池中。
根据卡顿的场景来看,只有可能是(1)和(2)造成的,首先查看数据变更时执行的代码,发现是已经调用了通知方法的,如下: 因此考虑下原因一,从抓包来看,异常的Recycelview对象中包含了几百个子view,那我们用可视化的工具来查看下布局,这下还真是发现了问题,在有限的屏幕里面,Recycelview中包含了上百个item,点都点不完,也就是本应该已经移出屏幕的itemView还存在于屏幕中,这或许就是过度绘制的根本原因。

继续往下看,看下各个控件的UI参数。 RecycleView UI参数:

RefreshLayout UI参数:

NestScrollerView UI参数:

可以发现除了最外层的NestScrollerView, 里层的FrameLayout、Refreshlayout和RecycleView的高度均是异常的,也就是"移出屏幕的item"其实没有移出屏幕,造成onViewRecyceled()执行失效,我们简单做个测试,将RecycelView的高度设置成"2048px",运行之后,确实卡顿的问题没有了,而且日志中也出现了onViewRecyceled()方法执行的日志,抓取 Heap Dump 文件,查看RecycleView的内存情况,mChildren的数量恢复到正常范围了。
onViewRecyceled()方法执行情况:

总体内存情况:

持续上拉刷新页面,内存占用稳定维持在520M左右,不再出现持续上涨的情况。
控件内存占用详细情况:

Recycelview的mChildren项,子view的个数只有24个,其中有12个是空的,恢复到了正常水平(满屏刚好是12个,第四级缓存池是有12个位置)。此时卡顿现象也完全修复了。但是,是什么原因导致控件高度异常? 查看下页面布局:

从bug的现象可以看出,控件的高度测量有问题,可以考虑以下点:
(1)首先从自身的代码出发,看下是否有逻辑修改了FrameLayout(@content_layout)的高度;
(2)容易让人怀疑的是上图标注的控件,因为它既不是来源于Android原生,也不是权威的机构所写,因此我们怀疑是com.scwang.smartrefresh.layout.SmartRefreshLayout重写了原生控件的onMeasure()方法造成的测量异常;
解决方式:

问题处理后续
修复该卡顿之后,出现了一个新的问题,到底的页面,最后一页的刷新会让footer跑到最后一页的上面。
稍微思考下这个现象之后,我们可以确定的是,就是因为延迟add数据造成的。之前的卡段原因是因为,过长页面的渲染与回弹动画同时并行,当修改之后,页面渲染任务变小,如果它能够在动画执行过程中完成,新的内容会自动填充到空余的部分。这便一举两得,优化了体验。将add数据的延迟操作去掉之后,发现此bug已解,并且刷新动作变得更加的合理和流畅了。

问题本质的分析
得出两个方向之后,我们首先从我们自己的业务代码出发,分析涉及修改筛选页面FrameLayout(@+id/content_layout)的高度的代码。

断点发现,问题出现的时候,并没有执行此处的代码,因此排除是业务代码造成的。继续看com.scwang.smartrefresh.layout.SmartRefreshLayout的源码。发现了设置异常高度地方的代码,从这我继续往下看。
setMeasuredDimension(int measuredWidth, int measuredHeight)方法是用来设置父容器的最终宽和高,因此从这里可以看出,确实是SmartRefreshLayout的计算问题导致的卡顿问题。通过连续刷新几次,发现每次增长的值是固定的如(37485、42840、48195等,每次增长5355--->1785dp,这个长度刚好是一刷数据展示后的高度 ,一刷九排数据,198*9 = 1782,与1785基本吻合,手工测量存在些许误差)。

可以看出,这个footerView.getMeasuredHeight()拿到的高度是新增一刷数据的高度

错误死循环:刷数据动作 ---> 计算的屏幕高度没有重置-->RecycelView("屏幕区域")高度增加一刷的高度 --->onRecycelView()不执行 ---->View无法回收--->"屏幕"增大一刷的高度 --->......