|
|
|
---
|
|
|
|
性能优化口水话
|
|
|
|
---
|
|
|
|
|
|
|
|
#### 目录
|
|
|
|
|
|
|
|
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 对资源路径进行混淆;还有 R 文件内联等。
|
|
|
|
|
|
|
|
#### 布局优化
|
|
|
|
|
|
|
|
布局优化老生常谈了,说说我在项目中的一些实践手段吧。
|
|
|
|
|
|
|
|
首先考虑布局嵌套和过度绘制。
|
|
|
|
|
|
|
|
在写布局的时候,考虑是否有必要写布局,如果布局比较简单,是否可以选择直接 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,因为 ObjectAnimator 是静态的,所以最终导致了 Activity 未被销毁。所以之前写的静态方法也是可以的,只是需要在 Activity onDestory 时去 removeAllListeners 即可。一般情况下,我们写的都是 ObjectAnimator 的实例,并且并没有在 onDestory 时去 cancle、removeAllListeners,这样会不会有问题呢?其实并没有什么太大的问题,也不会导致内存泄露。在动画执行完成后,并且在下一次 gc 到来时,对象还是会被回收的。如果动画的执行时间比较长,导致动画还持有 Activity 的引用以至于 Activity 未被及时回收,这是有问题的。但是一般动画执行时间比较短,再加上 gc 时机不确定,所以这时没有 cancle removeAllListeners 也是可以忽略的,加上其实也有好处,提前回收罢了,避免占有系统资源。
|
|
|
|
|
|
|
|
除此之外,对于内存泄露还有一些经验之谈,比如 Handler 的使用不当可能导致的内存泄露,解决办法就是静态内部类 + 弱引用,还有及时关闭资源文件、Context 的使用不当等。
|
|
|
|
|
|
|
|
内存抖动遇到的比较少,一般就是不要频繁的去创建对象销毁对象,典型的就是避免在 View 的 onDraw 方法中创建对象。
|
|
|
|
|
|
|
|
最后呢,还有一些通用的优化手段,比如 onTrimMemory 回调,这个方法会在 Vsync 信号到来时,Choreographer 执行 COMMIT 回调时执行,还有使用一些优化过的集合,比如 SparseArray、ArrayMap 等。这两个集合我也看过源码,SparseArray 用来替代 HashMap 的,但是 key 只能为 int 类型,简化了数据存储结构以及避免了自动装箱;ArrayMap 内部分别有两个长度为 10 的缓存队列用来缓存大小为 4 和 8 的 ArrayMap 对象,Bundle 内部就是使用 ArrayMap 存储数据的。
|
|
|
|
|
|
|
|
至此,内存优化我就讲完了。
|
|
|
|
|
|
|
|
|
|
|
|
|