Android 性能检测工具
trace文件
trace.txt是系统用于保存ANR Log的文件,通过这个文件可以找到系统检测到的ANR的应用。
ANR的定义
在Android上,如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应(ANR:Application Not Responding)对话框。用户可以选择“等待”而让程序继续运行,也可以选择“强制关闭”。所以一个流畅的合理的应用程序中不能出现anr,而让用户每次都要处理这个对话框。因此,在程序里对响应性能的设计很重要,这样系统不会显示ANR给用户。
ANR的类型
-
KeyDispatchTimeout(5 seconds) —-主要类型
按键或触摸事件在特定时间内无响应 -
BroadcastTimeout(10 seconds)
BroadcastReceiver在特定时间内无法处理完成 -
ServiceTimeout(20 seconds) —-小概率类型
Service在特定的时间内无法处理完成
KeyDispatchTimeout超时原因
(1)当前的事件没有机会得到处理(即UI线程正在处理前一个事件,没有及时的完成或者looper被某种原因阻塞住了)
(2)当前的事件正在处理,但没有及时完成
如何避免KeyDispatchTimeout
(1)UI线程尽量只做跟UI相关的工作
(2)耗时的工作(比如数据库操作,I/O,连接网络或者别的有可能阻碍UI线程的操作)把它放入单独的线程处理
(3)尽量用Handler来处理UIthread和别的thread之间的交互
UI线程
首先要知道事件发生的线程,一般来说大多数可能是UI线程操作超时,那么UI线程都有哪些呢:
(1). Activity 生命周期
(2). View post 的runnable方法 、 handler(MainLooper) 的 handleMessage()
(3). Asycktask 的 onPreExecute(), onPostExecute() , onProgressUpdate()方法
(4). Broadcast 的onReceive()
(5). Service
其他原因造成的anr
(1). 线程死锁
(2). cpu 饥饿
如何去分析ANR
我们写一个主线程阻塞的demo:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doANR();
}
private void doANR() {
try {
Thread.sleep(60000); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
出现Application Not Responding的提示后,系统会将日志LOG写到到data\anr\traces.txt文件,将手机上的traces.txt导出到电脑的d目录下:
adb pull /data/anr/traces.txt d:/
Log中定位ANR信息:
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x7523f450 self=0x55a8df6b20
| sysTid=10299 nice=-1 cgrp=top_visible sched=0/0 handle=0x7f97788fd0
| state=S schedstat=( 958207562 23237861 227 ) utm=65 stm=30 core=0 HZ=100
| stack=0x7fef1d1000-0x7fef1d3000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x04746b7d> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1046)
- locked <0x04746b7d> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1000)
at com.hx.tools.MainActivity.doANR(MainActivity.java:17) /**就是这里**/
at com.hx.tools.MainActivity.onCreate(MainActivity.java:12)
at android.app.Activity.performCreate(Activity.java:6367)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1110)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2511)
at android.app.ActivityThread.access$900(ActivityThread.java:165)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1375)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5621)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)
其他关于死锁以及cpu饥饿的Log分析可以参考Android ANR 分析学习总结。
TraceView
TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。
通过TraceView,可以得到以下两种数据:
- 单次执行最耗时的方法
- 执行次数最多的方法
trace 文件是 log 信息文件的一种,生成 trace 文件有三种方法:
- 使用代码
- 使用 Android Studio
- 使用 DDMS
代码生成 trace文件
/**开始 trace,保存文件到 "/sdcard/mytrace.trace" */
Debug.startMethodTracing("mytrace");
...
//结束
Debug.stopMethodTracing();
代码很简单,当你调用开始代码的时候,系统会生产 trace 文件,并且产生追踪数据,当你调用结束代码时,会将追踪数据写入到 trace 文件中,6.0以上系统请自己完成运行时权限。使用代码生成 trace 方式的好处是容易控制追踪的开始和结束,缺点就是步骤稍微多了一点。
本地的trace文件我们就可以使用SDK下的TraceView或DDMS来直接打开分析了。
Android Studio 生成 trace 文件
Android Studio 内置的 Android Monitor 可以很方便的生成 trace 文件到电脑。
在 CPU 监控的那栏会有一个闹钟似的的按钮,未启动应用时是灰色:
启动应用后,这个按钮会变亮,点击后开始追踪,相当于代码调用 startMethodTracing:
当要结束追踪时再次点击这个按钮,就会生成 trace 文件了。
生成 trace 后 Android Studio 自动加载的 traceview 图形如下:
从这个图可以大概了解一些方法的执行时间、次数以及调用关系,也可以搜索过滤特定的内容。
左上角可以切换不同的线程,这其实也是直接用 Android Studio 查看 trace 文件的缺点:无法直观地对比不同线程的执行时间。
鼠标悬浮到黄色的矩形上,会显示对应方法的开始、结束时间,以及自己占用和调用其他方法占用的时间比例:
DDMS 生成 trace 文件
DDMS 即 Dalvik Debug Monitor Server ,是 Android 调试监控工具,它为我们提供了截图,查看 log,查看视图层级,查看内存使用等功能,可以说是如今 Android Studio 中内置的 Android Monitor 的前身。
从Android Studio中启动android Device Monitor: Tools -> Android -> Android Device Monitor.
选择你要调试的进程—>点击start mothod profiling—>选择sample base profiling—>…—>stop mothod profiling
开启方法分析后对应用的目标页面进行测试操作,测试完毕后停止方法分析,界面会跳转到 DDMS 的 trace 分析界面,下面会分析。操作最好不要超过5s,因为最好是进行小范围的性能测试。
TraceView界面
下面我的分析是针对DDMS抓取的trace文件。
TraceView 界面比较复杂,其 UI 划分为上下两个面板,即 Timeline Panel(时间线面板)和 Profile Panel(分析面板)。上图中的上半部分为 Timeline Panel(时间线面板),Timeline Panel 又可细分为左右两个 Pane:
- 左边 Pane 显示的是测试数据中所采集的线程信息。由图可知,本次测试数据采集了 main 线程,监视线程和其它网络操作线程的信息。
- 右边 Pane 所示为时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等。由图可知,main 线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。
- 另外,开发者可以在时间线 Pane 中移动时间线纵轴。纵轴上边将显示当前时间点中某线程正在执行的函数信息。
上图中的下半部分为 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其内容非常丰富。它主要展示了某个线程(先在 Timeline Panel 中选择线程)中各个函数调用的情况,包括 CPU 使用时间、调用次数等信息。而这些信息正是查找 hotspot 的关键依据。所以,对开发者而言,一定要了解 Profile Panel 中各列的含义。下表列出了 Profile Panel 中比较重要的列名及其描述。
关键指标有三个:
- Cpu Time/Call :该方法平均占用 CPU 的时间
- Real Time/Call :平均执行时间,包括切换、阻塞的时间,>= Cpu Time
- Calls + Recur Calls/Total :调用、递归次数
Profile Panel界面下方表格中纵轴就是每个方法,包括了JDK的,Android SDK的,也有native方法的,当然最重要的就是app中你自己写的方法,有些Android系统的方法执行时间很长,那么有很大的可能就是你app中调用这些方法过多导致的。
每个方法前面都有一个数字,是全部方法按照Incl CPU Time 时间的排序序号。点一个方法后可以看到有两部分,一个是Parents,另一个是Children。
- Parent表示调用这个方法的方法,可以叫做父方法
- Children表示这个方法中调用的其他方法,可以叫做子方法
根据 TraceView 定位问题
定位问题时 TraceView 的使用方式:
- 从上半部分查看哪些线程执行时间长?什么时候开始执行?与主线程交错时间?
- 哪些方法的执行需要花费很长时间
点击 TraceView 中的 Cpu Time/Call,按照占用 CPU 时间从高到低排序 - 哪些方法调用次数非常频繁
点击 TraceView 中的 Calls + Recur Calls/Total ,按照调用次数从高到底排序,排序后,然后逐个排查是否有项目代码或者依赖库代码,有的话点击查看详情,查看是这个方法还是调用的子方法的问题,进一步定位问题。
我这里写了一个循环打印的例子:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doLoop();
}
private void doLoop() {
for (int i = 0; i < 100000; i++) {
showLog(i);
}
}
private void showLog(int i) {
Log.v("hx", "i="+i);
}
}
按照Cpu Time/Call大小排序:
按照Calls + Recur Calls/Total大小排序:
都能很快的定位到出错函数。