--- 性能优化口水话 --- #### 目录 1. 包体积优化 2. 布局优化 3. 内存优化 #### 包体积优化 Apk 包主要分为几个部分,libs so 库、dex、res、assets、resources.arsc 以及签名和 Manifest 文件。 常见的代码混淆、无用资源移除、ProGuard 等基本上大家都会做的,我主要做了资源精简这一块的优化,减少了 8M 左右。 首先是 png 图片,这是一个比较大的优化点,因为我们的最低 API 已经是 19 了,所以完全可以用 webp 替代 png。所以最开始我是写了一个 Task 来自动遍历 res 文件夹,使用 Google 开源的 cwebp 工具来转化,但是这有一个弊端,就是无法压缩第三方库里面的资源文件,所以在后面我们使用了 Gradle 3.3 版本新增的 getAllRawAndroidResources 这个 Api 可以获取到所有的资源目录,然后在进行转化。这一操作减少了 5.7M 的包体积大小。 还有就是遍历 res 生成 md5 值来去除重复的图片和 drawable,减少了133kb。最后配置了 resConfig,只保留中文和英文,减少了 1.1M,还可以在 devBuild 时配置 resConfig 只保留中文简体和 xxhdpi 的资源,有助于提升打包时间。 这些和权限输出,我都写在一个插件里面了。 除此之外呢,还有一些算是激进优化手段我们并没有做。对于 libs so 库,可以选择只保留 arm64-v8a,基本上现在手机大多都是这个 CPU 架构;对于 dex 可以使用 Facebook 开源的 ReDex 进行 dex 重分包、去除行号信息等优化手段;对于 resources.arsc 可以使用微信开源的 AndResGuard 对资源路径进行混淆; #### 布局优化 布局优化老生常谈了,说说我在项目中的一些实践手段吧。 首先考虑布局嵌套和过度绘制。 在写布局的时候,考虑是否有必要写布局,如果布局比较简单,是否可以选择直接 new,比如 RecyclerView 的 ItemView 可能就只是一个 TextView,这时 new TextView 显然比解析 xml 文件效率更高。如果要写布局文件了,考虑布局的复杂性,如果比较简单,FrameLayout 就行,如果布局比较复杂,可能导致嵌套过多,这时可以使用约束布局。同时为了后期维护性,我不建议使用线性布局。有时候,我们会封装一些控件放在系统控件里面,这时候就需要考虑是否可以 merge 最外层的布局。最后就是 ViewStub 延时初始化,基本上很少用。处理布局嵌套就这些,可以通过 Layout Inspector 来查看布局层级。 接着是过度绘制,过度绘制可能是项目中遇到最多的了。首先记住最最重要的一点,就是在往顶层布局添加 background 时,一定要考虑是否有必要。我们的 Activity 的默认主题色是灰白色,如果 UI 图给的背景色是白色,基本上无一例外大家都会在顶层布局把 background 置为灰色,这就导致了一层过度绘制,解决办法很简单,再写一个主题即可。还有些系统控件,默认是有背景色的,可以指定 background 为 null 即可,典型的就是 AppBarLayout 了。其次呢,就是代码层面的了,能少绘制就少绘制。比如 RecyclerView 的局部刷新、还有在选中/非选中的场景下,需要注意重复点击已选中的 Item 的处理以及在本地筛选数据的情况下,利用数据缓存避免重复处理数据。过度绘制就这些,可以使用开发者工具打开过度绘制查看。 除了上面比较常用的,还有一些优化手段可以考虑。比如使用 AsyncLayoutInflater 异步创建 View、使用掌阅开源的 X2C,这些都是建立在解析 Layout 是 IO 过程,创建 View 是通过反射为优化基础上的。 最后,布局优化还不能忽略后期的维护性,比如最好不要使用 LinearLayout,布局也可以写适当的注释,可以写适当的 tools 属性利于预览,当布局代码很多时,可以使用 editor-fold 来折叠代码块。 #### 内存优化 内存优化分为内存泄露和内存抖动。 内存泄露我主要的排查思路是使用 LeakCanary + Android Profiler。LeakCanary 平时是一直开着的,在任务的收尾阶段我会跑一下 Android Profiler。打开关闭页面来来回回五六次,看一下 Total Memory 有没有明显升高,然后看一下对应的实例对象有没有被销毁。 在做积分商城时,有一个编辑地址页面,里面有三个 EditText 并设置了 TextChangedListener,在跑 Profiler 时发现页面已经退出了但是还存在三个该 Activity 的应用,并且都定位到了 TextWatcher 那一行,然后我试了强制 gc 再退回桌面还是存在,说明是内存泄露了。但是大家一直都这么写,并没有在 onDestory 时去移除 Listener 呀。我的测试机是三星 7,然后我试了一下我的小米 9,发现就没问题了。初步怀疑是系统 bug,然后我用 Google 的模拟器,实际测试一下 Android 8 以上没有问题,以下就存在内存泄露。但是项目中很多这样的,解决办法就是可以在 BaseActivity 的 onDestory 去遍历 View 树清空 Listener。 我在 review 代码时还遇到同事写的一个动画相关的内存泄露,一个静态方法,参数是一个 View,里面用一个静态的 ObjectAnimator 去对这个 View 进行动画处理,然后 LeakCanary 就检测出来内存泄露了。他的做法就是把静态方法改为实例方法就解决了。但是他以为是静态的 ObjectAnimator 持有了 View 的引用导致 Activity 不能被回收,其实呢 ObjectAnimator 在内部持有的是 View 的弱引用,所以事实上并不是这个原因。到底原因是在哪呢?其实是在他给这个 ObjectAnimator 设置了 Listener 然在 Listener 对 View 进行了相关操作,这就形成了一个强引用链 ObjectAnimator -> ArrayList -> View,最终导致了 Activity 未被销毁。所以之前写的静态方法也是可以的,只是需要在 Activity onDestory 时去 removeAllListeners 即可。 除此之外,对于内存泄露还有一些经验之谈,比如 Handler 的使用不当可能导致的内存泄露,解决办法就是静态内部类 + 弱引用,还有及时关闭资源文件、Context 的使用不当等。 内存抖动遇到的比较少,一般就是不要频繁的去创建对象销毁对象,典型的就是避免在 View 的 onDraw 方法中创建对象。 最后呢,还有一些通用的优化手段,比如 onTrimMemory 回调,这个方法会在 Vsync 信号到来时,Choreographer 执行 COMMIT 回调时执行,还有使用一些优化过的集合,比如 SparseArray、ArrayMap 等。这两个集合我也看过源码,SparseArray 用来替代 HashMap 的,但是 key 只能为 int 类型,简化了数据存储结构以及避免了自动装箱;ArrayMap 内部分别有两个长度为 10 的缓存队列用来缓存大小为 4 和 8 的 ArrayMap 对象,Bundle 内部就是使用 ArrayMap 存储数据的。 至此,内存优化我就讲完了。