本文将介绍优化Android App的N种方法和N种工具。
###信条
- 用数据说话:在细微的差别面前大部分人无法感知辨别,在每一次优化代码后使用如下将介绍的工具进行检验。
- 使用低性能机器测试:低性能的机器会更多地暴露问题。
- 意识到等价交换的思想:所有优良代码之上的优化都是基于等价交换,而这其中基本上都是以空间换时间。
###Systrace
Systrace是大多数开发者不去使用的工具,因为大家不知道如何使用它提供的数据。Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载以及 Android 各个子系统的运行状况等。
它有三部分组成:
- 内核部分:Systrace 利用了Linux Kernel 中的 ftrace 功能。所以,如果要使用 Systrace 的话,必须开启 kernel 中和 ftrace 相关的模块。
- 数据采集部分:Android 定义了一个 Trace 类。应用程序可利用该类把统计信息输出给 ftrace。同时,Android 还有一个 atrace 程序,它可以从 ftrace 中读取统计信息然后交给数据分析工具来处理。
- 数据分析工具:Android 提供一个 systrace.py(python脚本文件,位于Android SDK 目录 /tools/systrace 中,其内部将调用 atrace 程序)用来配置数据采集的方式(如采集数据的标签、输出文件名等)和收集 ftrace 统计数据并生成一个结果网页文件供用户查看。
Systrace使用方式 有两种:
- 1.Device Monitor
- 2.命令行
$ cd android-sdk/platform-tools/systrace
$ python systrace.py --set-tags=gfx,view,wm
$ adb shell stop
$ adb shell start
ps:先在设备中打开Settings > Developer options > Monitoring > Enable traces。
抓取的信息使用浏览器打开如下:
我们来选取一个Alert来看一下:
可以看到Alert原因是Long View#draw(),下面还会有相关文档链接。选择Frame这一行,就能看到每一帧的渲染,通过不同的颜色发现一些问题,我们来选择红色的一帧:
在底部,有三个Alert信息,打开“Inflation during ListView recycling”:
看这部分耗费的时间,32ms,超过了保证60fps的16ms。再看下面细分每一步耗费的时间,有5个obtainView都消耗了大约5ms。Description指出了问题所在,没有使用ListView的复用机制造成inflate单个Item成本过高。再看上面的图形部分,它将所有的东西都可视化,我们放大来看“inflate”:
在选择一帧之后,可以通过“m”键显示具体耗费的时间。可以看到耗费超过19ms渲染这一帧。展开这一帧唯一的一个Alert,看到“Scheduling delay”。选择最长的DrawFrame来看一下:
Wall duration是从时间片的开始到结束。CPU Duration是CPU的实际运行时间。注意这两个时间的巨大差别,来看一下CPU这段时间做了什么:
四个核心都处于忙碌状态,选择一个Thread看它从哪里发起,一个包名为com.udinic.keepbusyapp。原来是其它app的原因。这种情况通常是暂时的,因为其它app不太可能在后台长时间占用CPU,这些线程可能来自你的App的其它进程,或者是主进程。由于Systrace是以系统的角度返回一些信息,要进一步获取CPU满负荷运行的原因,我们来使用另一个工具Traceview。
###TraceviewTraceview是一个很好的性能分析工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到method:
使用方式同样有两种,Android Device Monitor或者使用代码,参考 这里 .
我们来看一下不同列代表的含义:
- Name:method的名称,前面有一个和上图对应的颜色标识。
- Inclusive CPU Time:此方法和子方法调用占用的CPU时间。
- Exclusive CPU Time:此方法单独调用占用的CPU时间。
- Inclusive / Exclusive Real Time:从方法开始到结束的时间,类似于“Wall Duration“。
- Calls+Recursion:调用次数。
- CPU/Real time per Call:每次调用平均占用的CPU时间。
我打开了一个不能平滑滚动的App,开始跟踪,滚动以下部分,停止跟踪。找到getView()方法,展开:
这个方法调用了12次,每次CPU时间3ms,但是每次调用的真实时间是162ms,绝对有问题!
查看这个方法下地Children,Thread.join()耗费了约98%的inclusive real time。这个方法是用来等待其他线程结束。另一个children是Thread.start(),由此认定getView()方法启动了一个线程并等待它结束。
但是这个线程在哪?
我们不知道这个getView()中的线程做了什么,因为getView()中没有直接启动它。为了找到它,我查找了Thread.start()方法。最终发现了它:
经过作者 Udi Cohen 同意,本文内容参考引用了 Speed up your app