目前所开发的应用是在开发板
上的一款应用市场
类应用,设备主要涉及到拉杆音响和电视盒子两类,其中拉杆音响的设备性能要略好于电视盒子类的设备,年初应用在添加了一些高级功能比如GIF轮播
这类对性能比较有挑战的功能后,在上述两类系统和性能排名较低的设备上暴力测试和Monkey
测试时有较大的概率发生ANR
,加上平时打开一些比较复杂页面的响应速度也比较堪忧,于是就开启了一系列优化的过程。
问题分析
从设备的表现上来看,产生UI
卡顿甚至ANR
的原因有两个层面,一个是系统层面,出现ANR
的设备主要集中在Android 4.4
这一系统上,再加上硬件参数相对较低,就会出现即使不运行任何应用仅停留在桌面,通过logcat
也可以看到一些系统的进程在不断地进行GC
,另一个是应用层面,面对这类设备情况,常规的开发手段显然不能有很好的表现,再加上一些API
在使用上不严谨就更加加剧了卡顿的情况。
面对这些情况,系统层面我们肯定没法干预,虽然我司在系统固件定制方面有一些业务和经验,但显然不会单独为了这一两类设备去单独做优化,那我们只能从应用层面着手,采用一些非常规的开发手段,比如统一图片库、统一线程池、限制线程数、设备分级处理、使用代码代替xml
创建布局、优化列表数据加载、View
缓存复用和异步加载等等,其中一些手段我们在日常开发时就已经考虑到了,今天我们就来详细说说部分优化手段的实现以及优化后的效果。
要想知道优化的手段有没有起到效果就需要有优化前后的数据对比,首先想到的是统计Activity/Fragment
的生命周期方法的执行时间,本身应用的页面架构采用的是单Activity + Fragments
,路由则是使用Jetpack
中的Navigation
,所以我们实现一个继承FragmentLifecycleCallbacks
的类,在其中的回调中统计Fragment
的生命周期的方法执行时间即可。
XML 转 Code
以下是在Fragment
的生命周期中结合业务的伪代码:
1 | // Fragment lifecycle methods |
这是封装的通用的页面逻辑处理过程,观察在FragmentLifecycleCallbacks
的回调方法中打印生命周期方法的执行时间,在性能比较差的设备上且页面布局稍微复杂一点的时候,可以发现onCreateView
的执行时间普遍在200ms
左右,这还是我们在布局结构上只使用ConstraintLayout
防止布局嵌套和优化过度绘制之后的数据表现,为了进一步提升性能,因此有了把XML
布局转Java
代码实现的尝试。
把XML
布局转换成Java
代码实现是一种常见的优化手段,我们都知道xml
布局在inflate
的过程中会涉及到大量的IO
和反射,这也就成为了低端机上的性能瓶颈之一,在最开始的阶段有想过寻找一些成熟的框架来解决这一问题,其中比较有影响力的像X2C框架,但是这个框架已经很久没有更新了,最新的Android Stuido
已经不能引入改库了,我也有想过阅读X2C源码并仿写一个适用最新的Android Stuido
的版本,直到后续读到Booster作者的博客才打消了这一念头,这个大佬最初也有过这一想法,因为官方的Compose
出来之后就放弃了,View
体系后面也会成为历史产物,继续在上面折腾是没有太大的意义的。
后续功能的实现也只剩手搓这一选项了,手搓的伪代码大致如下:
首先是创建类:
1 | public class XXXLayoutCreator { |
在Fragment
中改动:
1 | public class XXXFragment extends BaseFragment { |
之所以使用一个开关保留XML
布局主要有如下考虑:
XML
布局适配了三套设计图,是经过测试验证过的,使用代码实现某些细节有可能会有所遗漏- 更加容易定位问题,后续遇到相关
Bug
可以方便切换XML
布局和Java
布局,做一定的问题排查 - 方便维护,不管后续代码谁来维护,直接面对一堆代码肯定会略感无力,保留
XML
布局可以多一分参考
面对项目里众多的XML
布局,一一转换的过程枯燥且费时,好在我们在创建各类XXXLayoutCreator
的过程中使用Trae这个AI IDE工具来帮助我们提升转换的效率,主要思路是让它参考已经转换好的XML
文件和Java
文件,然后根据XML
文件自动创建对应的Java
文件,生成的Java
文件略加修改即可直接使用,整个过程中大概节约了**30%**的时间。
在实现XML
转Code
这一优化后,同一页面onCreateView
的执行时间优化到100ms
以内,虽然没有达到有些博客里介绍的20 倍的优化效果,但也很不错了。
RecyclerView
数据加载
项目里使用RecyclerView
的页面比较多,虽然我把列表里Item
的布局也转换为Java
代码实现,但是在页面创建和销毁比较高频的场景下还是偶尔会发生ANR
,使用内存分析工具也没有发现内存泄漏的地方,进一步的优化场景就只有mAdapter.notifyDataSetChanged()
的使用了。
首先把出现问题的场景交代一下,这个页面是应用的主页,页面采用TabLayout
+ViewPager
+Fragments
的结构,是一种比较典型的TV
类布局结构,原型图大致如下:
使用遥控器在TabLayout
中快速切换Tab
时,会造成Fragment
下的View
频繁的创建与销毁,而且页面在数据加载中这一状态时我们构建了一个无内容
的数据列表去填充RecyclerView
以达到提前预览的效果,这就导致了在数据显示到屏幕上时已经调用了两次notifyDataSetChanged()
,对象的频繁创建与销毁会导致内存抖动,这是导致该场景下ANR
发生的根本原因。
解决思路主要有以下尝试:
- 在
TabLayout
切换时加防抖功能,避免过快切换Fragment
- 移除
notifyDataSetChanged()
方法的使用,使用notifyItemRangeXXX
或者DiffUtil
去替代 - 跨页面使用
RecyclerViewPool
来复用同类型的RecyclerView
下的ViewHolder
- 使用对象池来缓存和复用一些创建时损耗或占用资源比较大的对象
对于第二点我后续使用DiffUtil
来替代notifyDataSetChanged()
的使用,在使用细节上首先有根据AsyncListDiffer
来封装自己的BaseAdapter
,只不过在构造方法里初始化AsyncListDiffer
对象时使用一个共享的ThreadPool
静态变量来做DiffUtil
内部异步数据对比,其次是页面加载中
状态下构造无实际内容的数据时根据单个数据在列表中的位置赋予该数据一个唯一id
,在真实数据到来时也进行这一过程,这样在DiffUtil.ItemCallback
下的areItemsTheSame
中使用该id
来做比较可以保证页面状态变化后,Adapter
不会执行两次onCreateViewHolder
,当然这一细节只有该场景下才适用。
第三点其实很早之前就用了,只不过在使用Glide
加载的场景下一直报错,使用Fresco
加载的时候不会出现这一问题,后续找到原因是我在ViewHolder
里持有了Fragment
的对象,其主要用于图片加载时传入,这样该图片的资源就跟Fragment
的生命周期绑定,但是在ViewPager
+Fragments
的场景下,复用同类型的ViewHolder
就会出现ImageView
使用已经回收的Bitmap
的异常,后续是在onBindViewHolder
时Fragment
作为方法参数传入ViewHolder
中使用才解决这一问题。
总结
以上优化方法的应用,总体上使得ANR
的情况得到控制,如果不是以上很多因素叠加到一起可能就不会出现ANR
的问题,卡顿优化也是要具体问题具体分析,必要时使用一些工具,最近听说字节的btrace
最新版的工具快要发布了,后续肯定要在项目里尝试一波。