@Android性能分析之TraceView的使用
TraceView简介
TraceView是AndroidSDK里面自带的工具,用于对Android的应用程序以及Framework层的代码进行性能分析。
TraceView是图形化的工具,最终它会产生一个图表,用于对性能分析进行说明。
TraceView可以跟踪到具体的Method。
TraceView界面
现来看一下整个界面的图,整个界面包括上下两部分,上面是你测试的进程中每个线程的执行情况,每个线程占一行;下面是每个方法执行的各个指标的值
上面一部分是你测试进程的中每个线程运行的时间线,下图中可以可以看到,主要只有一个main线程在执行,因为我滑动了一下列表,main线程(UI线程)正在进行绘制View呢~
然后我点击了序号为133的一个方法io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView
,就会出现两部分数据:
- Parents
- Children
Parents表示调用133这个方法的父方法,可以看到序号为130。Children表示方法133调用的其他方法,可以看到有好几个方法。
点住某个线程在上部时间轴的执行点不放,向左或向右拖动,能放大显示时间轴(显示的时间范围由拖动的距离决定),点击时间轴左上角方法信息的那个小方块图,可还原默认时间轴显示
如何使用TraceView
因为这次我主要是分析列表滑动卡顿问题,我就讲讲我是怎么使用这个工具的,并且我是怎么分析的。
使用TraceView主要有两种方式:
- 最简单的方式就是直接打开DDMS,选择一个进程,然后按上面的“Start Method Profiling”按钮,等红色小点变成黑色以后就表示TraceView已经开始工作了。然后我就可以滑动一下列表(现在手机上的操作肯定会很卡,因为Android系统在检测Dalvik虚拟机中每个Java方法的调用,这是我猜测的)。操作最好不要超过5s,因为最好是进行小范围的性能测试。然后再按一下刚才按的按钮,等一会就会出现上面这幅图,然后就可以开始分析了。
- 第2种方式就是使用
android.os.Debug.startMethodTracing();
和android.os.Debug.stopMethodTracing();
方法,当运行了这段代码的时候,就会有一个trace文件在/sdcard
目录中生成,也可以调用startMethodTracing(String traceName)
设置trace文件的文件名,最后你可以使用adb pull /sdcard/test.trace /tmp
命令将trace文件复制到你的电脑中,然后用DDMS工具打开就会出现第一幅图了,如果没有trace文件,可以增加写外部存储权限再尝试
第一种方式相对来说是一种简单,但是测试的范围很宽泛,第二中方式相对来说精确一点,不过我个人喜欢使用第一种,因为简单,而且它是检测你的某一个操作。因为第二中更适合检测某一个方法的性能,其实也没有那种好,看使用的场景和喜好了。。。
看懂TraceView中的指标
其实我今年7月份就已经开始使用TraceView工具了,但是当时不懂其中每个指标的含义,就没注意到它强大的地方。看不懂界面下方表格中的指标,这些数据其实一点意义都没有。
网上包括Android官网也没有对TraceView工具的使用有详细的说明文档,这点确实比较蛋疼。
纵轴
TraceView界面下方表格中纵轴就是每个方法,包括了JDK的,Android SDK的,也有native方法的,当然最重要的就是app中你自己写的方法,有些Android系统的方法执行时间很长,那么有很大的可能就是你app中调用这些方法过多导致的。
每个方法前面都有一个数字,可能是全部方法按照Incl CPU Time 时间的排序序号(后面会讲到)
点一个方法后可以看到有两部分,一个是Parents,另一个是Children。
- Parent表示调用这个方法的方法,可以叫做父方法
- Children表示这个方法中调用的其他方法,可以叫做子方法
横轴
横轴上是很多指标,这些指标表示什么意思真的困扰了我很长一段时间。。。
能够很衡量一个方法性能的指标应该只有时间了吧? 一个方法肯定就是执行时间越短约好咯~~
1. Incl Cpu Time
define inclusive : 全包括的
上图中可以看到0(toplevel)
的Incl Cpu Time 占了100%的时间,这个不是说100%的时间都是它在执行,请看下面代码:
1
2
3
4
5
6
|
publicvoidtop(){
a();
b();
c();
d();
}
|
Incl Cpu Time表示方法top执行的总时间,假如说方法top的执行时间为10ms,方法a执行了1ms,方法b执行了2ms,方法c执行了3ms,方法d执行了4ms(这里是为了举个栗子,实际情况中方法a、b、c、d的执行总时间肯定比方法top的执行总时间要小一点)。
而且调用方法top的方法的执行时间是100ms,那么:
Incl Cpu Time | ||
---|---|---|
top | 10% | |
a | 10% | |
b | 20% | |
c | 30% | |
d | 40% |
从上面图中可以看到:toplevel
的 Incl Cpu Time 是1110.943,而io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView
方法的Incl Cpu Time为12.859,说明后者的Incl Cpu Time % 约为1.2%
这个指标表示 这个方法以及这个方法的子方法(比如top方法中的a、b、c、d方法)一共执行的时间
2. Excl Cpu Time
理解了Incl Cpu Time以后就可以很好理解Excl Cpu Time了,还是上面top方法的栗子:
方法top 的 Incl Cpu Time 减去 方法a、b、c、d的Incl Cpu Time 的时间就是方法top的Excl Cpu Time 了
3. Incl Real Time
这个感觉和Incl Cpu Time 差不多,第7条会讲到。
4. Excl Real Time
同上
5. Calls + Recur Calls / Total
这个指标非常重要!
它表示这个方法执行的次数,这个指标中有两个值,一个Call表示这个方法调用的次数,Recur Call表示递归调用次数,看下图:
我选中了一个方法,可以看到这个方法的Calls + Recur Calls
值是14 + 0,表示这个方法调用了14次,但是没有递归调用
从Children这一块来看,很多方法调用都是13的倍数,说明父方法中有一个判断,但是这不是重点,有些Child方法调用Calls为26,这说明了这些方法被调用了两遍,是不是可能存在重复调用的情况?这些都是可能可以优化性能的地方。
6. Cpu Time / Call
重点来了!!!!!!!!!!
这个指标应该说是最重要的,从上图可以看到,133这个方法的调用次数为20次,而它的Incl Cpu Time为12.859ms,那么133方法每一次执行的时间是0.643ms(133这个方法是SimpleAdapter
的getItemView
方法)
对于一个adapter
的getView
方法来说0.643ms是非常快的(因为这个adapter
中只有一个TextView
,我为了测试用的)
如果getView
方法执行时间很长,那么必然导致列表滑动的时候产生卡顿现象,可以在getView
方法的Children方法列表中找到耗时最长的方法,分析出现问题的原因:
- 是因为有过多的计算?
- 还是因为有读取SD卡的操作?
- 还是因为
adapter
中View
太复杂了? - 还是因为需要有很多判断,设置
View
的显示还是隐藏 - 还是因为其他原因…
7. Real Time / Call
Real Time 和 Cpu Time 我现在还不太明白它们的区别,我的理解应该是:
- Cpu Time 应该是某个方法占用CPU的时间
- Real Time 应该是这个方法的实际运行时间
为什么它们会有区别呢?可能是因为CPU的上下文切换、阻塞、GC等原因方法的实际执行时间要比Cpu Time 要长
实战:
了解完 TraceView 的 UI 后,现在介绍如何利用 TraceView 来查找 hotspot。一般而言,hotspot 包括两种类型的函数:
- 一类是调用次数不多,但每次调用却需要花费很长时间的函数。
- 一类是那些自身占用时间不长,但调用却非常频繁的函数。
测试背景:APP 在测试机运行一段时间后出现手机发烫、卡顿、高 CPU 占有率的现象。将应用切入后台进行 CPU 数据的监测,结果显示,即使应用不进行任何操作,应用的 CPU 占有率都会持续的增长。
按照 TraceView 简介中的方法进行测试,TraceView 结果 UI 显示后进行数据分析,在 Profile Panel 中,选择按 Cpu Time/Call 进行降序排序(从上之下排列,每项的耗费时间由高到低)得到如图所示结果:
图中 ImageLoaderTools$2.run() 是应用程序中的函数,它耗时为 1111.124。然后点击ImageLoaderTools$2.run() 项,得到更为详尽的调用关系图:
上图中 Parents 为 ImageLoaderTools$2.run() 方法的调用者:Parents (the methods calling this method);Children 为 ImageLoaderTools$2.run() 调用的子函数或方法:Children (the methods called by this method)。本例中 ImageLoaderTools$2.run() 方法的调用者为 Framework 部分,而 ImageLoaderTools$2.run() 方法调用的自方法中我们却发现有三个方法的 Incl Cpu Time % 占用均达到了 14% 以上,更离谱的是 Calls+RecurCalls/Total 显示这三个方法均被调用了 35000 次以上,从包名可以识别出这些方法为测试者自身所实现,由此可以判断 ImageLoaderTools$2.run() 极有可能是手机发烫、卡顿、高 CPU 占用率的原因所在。
代码验证
大致可以判断是 ImageLoaderTools$2.run() 方法出现了问题,下面找到这个方法进行代码上的验证:
1 package com.sunzn.app.utils; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.lang.ref.SoftReference; 7 import java.util.ArrayList; 8 import java.util.HashMap; 9 10 import android.content.Context; 11 import android.graphics.Bitmap; 12 import android.os.Environment; 13 import android.os.Handler; 14 import android.os.Message; 15 16 public class ImageLoaderTools { 17 18 private HttpTools httptool; 19 20 private Context mContext; 21 22 private boolean isLoop = true; 23 24 private HashMap<String, SoftReference<Bitmap>> mHashMap_caches; 25 26 private ArrayList<ImageLoadTask> maArrayList_taskQueue; 27 28 private Handler mHandler = new Handler() { 29 public void handleMessage(android.os.Message msg) { 30 ImageLoadTask loadTask = (ImageLoadTask) msg.obj; 31 loadTask.callback.imageloaded(loadTask.path, loadTask.bitmap); 32 }; 33 }; 34 35 private Thread mThread = new Thread() { 36 37 public void run() { 38 39 while (isLoop) { 40 41 while (maArrayList_taskQueue.size() > 0) { 42 43 try { 44 ImageLoadTask task = maArrayList_taskQueue.remove(0); 45 46 if (Constant.LOADPICTYPE == 1) { 47 byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET); 48 task.bitmap = BitMapTools.getBitmap(bytes, 40, 40); 49 } else if (Constant.LOADPICTYPE == 2) { 50 InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET); 51 task.bitmap = BitMapTools.getBitmap(in, 1); 52 } 53 54 if (task.bitmap != null) { 55 mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap)); 56 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 57 if (!dir.exists()) { 58 dir.mkdirs(); 59 } 60 String[] path = task.path.split("/"); 61 String filename = path[path.length - 1]; 62 File file = new File(dir, filename); 63 BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); 64 Message msg = Message.obtain(); 65 msg.obj = task; 66 mHandler.sendMessage(msg); 67 } 68 } catch (IOException e) { 69 e.printStackTrace(); 70 } catch (Exception e) { 71 e.printStackTrace(); 72 } 73 74 synchronized (this) { 75 try { 76 wait(); 77 } catch (InterruptedException e) { 78 e.printStackTrace(); 79 } 80 } 81 82 } 83 84 } 85 86 }; 87 88 }; 89 90 public ImageLoaderTools(Context context) { 91 this.mContext = context; 92 httptool = new HttpTools(context); 93 mHashMap_caches = new HashMap<String, SoftReference<Bitmap>>(); 94 maArrayList_taskQueue = new ArrayList<ImageLoaderTools.ImageLoadTask>(); 95 mThread.start(); 96 } 97 98 private class ImageLoadTask { 99 String path; 100 Bitmap bitmap; 101 Callback callback; 102 } 103 104 public interface Callback { 105 void imageloaded(String path, Bitmap bitmap); 106 } 107 108 public void quit() { 109 isLoop = false; 110 } 111 112 public Bitmap imageLoad(String path, Callback callback) { 113 Bitmap bitmap = null; 114 String[] path1 = path.split("/"); 115 String filename = path1[path1.length - 1]; 116 117 if (mHashMap_caches.containsKey(path)) { 118 bitmap = mHashMap_caches.get(path).get(); 119 if (bitmap == null) { 120 mHashMap_caches.remove(path); 121 } else { 122 return bitmap; 123 } 124 } 125 126 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 127 128 File file = new File(dir, filename); 129 130 bitmap = BitMapTools.getBitMap(file.getAbsolutePath()); 131 if (bitmap != null) { 132 return bitmap; 133 } 134 135 ImageLoadTask task = new ImageLoadTask(); 136 task.path = path; 137 task.callback = callback; 138 maArrayList_taskQueue.add(task); 139 140 synchronized (mThread) { 141 mThread.notify(); 142 } 143 144 return null; 145 } 146 147 }
以上代码即是 ImageLoaderTools 图片工具类的全部代码,先不着急去研究这个类的代码实现过程,先来看看这个类是怎么被调用的:
1 ImageLoaderTools imageLoaderTools = imageLoaderTools = new ImageLoaderTools(this); 2 3 Bitmap bitmap = imageLoaderTools.imageLoad(picpath, new Callback() { 4 5 @Override 6 public void imageloaded(String picPath, Bitmap bitmap) { 7 if (bitmap == null) { 8 imageView.setImageResource(R.drawable.default); 9 } else { 10 imageView.setImageBitmap(bitmap); 11 } 12 } 13 }); 14 15 if (bitmap == null) { 16 imageView.setImageResource(R.drawable.fengmianmoren); 17 } else { 18 imageView.setImageBitmap(bitmap); 19 }
ImageLoaderTools 被调用的过程非常简单:1.ImageLoaderTools 实例化;2.执行 imageLoad() 方法加载图片。
在 ImageLoaderTools 类的构造函数(90行-96行)进行实例化过程中完成了网络工具 HttpTools 初始化、新建一个图片缓存 Map、新建一个下载队列、开启下载线程的操作。这时候请注意开启线程的操作,开启线程后执行 run() 方法(35行-88行),这时 isLoop 的值是默认的 true,maArrayList_taskQueue.size() 是为 0 的,在任务队列 maArrayList_taskQueue 中还没有加入下载任务之前这个循环会一直循环下去。在执行 imageLoad() 方法加载图片时会首先去缓存 mHashMap_caches 中查找该图片是否已经被下载过,如果已经下载过则直接返回与之对应的 bitmap 资源,如果没有查找到则会往 maArrayList_taskQueue 中添加下载任务并唤醒对应的下载线程,之前开启的线程在发现 maArrayList_taskQueue.size() > 0 后就进入下载逻辑,下载完任务完成后将对应的图片资源加入缓存 mHashMap_caches 并更新 UI,下载线程执行 wait() 方法被挂起。一个图片下载的业务逻辑这样理解起来很顺畅,似乎没有什么问题。开始我也这样认为,但后来在仔细的分析代码的过程中发现如果同样一张图片资源重新被加载就会出现死循环。还记得缓存 mHashMap_caches 么?如果一张图片之前被下载过,那么缓存中就会有这张图片的引用存在。重新去加载这张图片的时候如果重复的去初始化 ImageLoaderTools,线程会被开启,而使用 imageLoad() 方法加载图片时发现缓存中存在这个图片资源,则会将其直接返回,注意这里使用的是 return bitmap; 那就意味着 imageLoad() 方法里添加下载任务到下载队列的代码不会被执行到,这时候 run() 方法中的 isLoop = true 并且 maArrayList_taskQueue.size() = 0,这样内层 while 里的逻辑也就是挂起线程的关键代码 wait() 永远不会被执行到,而外层 while 的判断条件一直为 true,就这样程序出现了死循环。死循环才是手机发烫、卡顿、高 CPU 占用率的真正原因所在。
解决方案
准确的定位到代码问题所在后,提出解决方案就很简单了,这里提供的解决方案是将 wait() 方法从内层 while 循环提到外层 while 循环中,这样重复加载同一张图片时,死循环一出现线程就被挂起,这样就可以避免死循环的出现。代码如下:
1 private Thread mThread = new Thread() { 2 3 public void run() { 4 5 while (isLoop) { 6 7 while (maArrayList_taskQueue.size() > 0) { 8 9 try { 10 ImageLoadTask task = maArrayList_taskQueue.remove(0); 11 12 if (Constant.LOADPICTYPE == 1) { 13 byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET); 14 task.bitmap = BitMapTools.getBitmap(bytes, 40, 40); 15 } else if (Constant.LOADPICTYPE == 2) { 16 InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET); 17 task.bitmap = BitMapTools.getBitmap(in, 1); 18 } 19 20 if (task.bitmap != null) { 21 mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap)); 22 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 23 if (!dir.exists()) { 24 dir.mkdirs(); 25 } 26 String[] path = task.path.split("/"); 27 String filename = path[path.length - 1]; 28 File file = new File(dir, filename); 29 BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); 30 Message msg = Message.obtain(); 31 msg.obj = task; 32 mHandler.sendMessage(msg); 33 } 34 } catch (IOException e) { 35 e.printStackTrace(); 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } 39 40 } 41 42 synchronized (this) { 43 try { 44 wait(); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 } 49 50 } 51 52 }; 53 54 };
最后再附上代码修改后代码运行的性能图,和之前的多次被重复执行,效率有了质的提升,手机发烫、卡顿、高 CPU 占用率的现象也消失了。