Android工具TraceView

1、TraceView 简介

TraceView 是 Android 平台特有的数据采集和分析工具,它主要用于分析 Android 中应用程序的 hotspot。TraceView 自己只是一个数据分析工具,而数据的采集则须要使用 Android SDK 中的 Debug 类或者利用 DDMS 工具。两者的用法以下:java

1)开发者在一些关键代码段开始前调用 Android SDK 中 Debug 类的 startMethodTracing 函数,并在关键代码段结束前调用 stopMethodTracing 函数。这两个函数运行过程当中将采集运行时间内该应用全部线程(注意,只能是 Java 线程)的函数执行状况,并将采集数据保存到 /mnt/sdcard/ 下的一个文件中。开发者而后须要利用 SDK 中的 TraceView 工具来分析这些数据。android

2借助 Android SDK 中的 DDMS 工具。DDMS 可采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的状况。shell

DDMS 中 TraceView 使用示意图以下,调试人员能够经过选择 Devices 中的应用后点击  按钮 Start Method Profiling(开启方法分析)和点击  Stop Method Profiling(中止方法分析)缓存

开启方法分析后对应用的目标页面进行测试操做,测试完毕后中止方法分析,界面会跳转到 DDMS 的 trace 分析界面,以下图所示:网络

TraceView 界面比较复杂,其 UI 划分为上下两个面板,即 Timeline Panel(时间线面板)和 Profile Panel(分析面板)。上图中的上半部分为 Timeline Panel(时间线面板),Timeline Panel 又可细分为左右两个 Pane:app

1)左边 Pane 显示的是测试数据中所采集的线程信息。由图可知,本次测试数据采集了 main 线程,传感器线程和其它系统辅助线程的信息。ide

2)右边 Pane 所示为时间线,时间线上是每一个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等。由图可知,Thread-1412 线程对应行的的内容很是丰富,而其余线程在这段时间内干得工做则要少得多。函数

3)另外,开发者能够在时间线 Pane 中移动时间线纵轴。纵轴上边将显示当前时间点中某线程正在执行的函数信息。工具

上图中的下半部分为 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其内涵很是丰富。它主要展现了某个线程(先在 Timeline Panel 中选择线程)中各个函数调用的状况,包括 CPU 使用时间、调用次数等信息。而这些信息正是查找 hotspot 的关键依据。因此,对开发者而言,必定要了解 Profile Panel 中各列的含义。下表列出了 Profile Panel 中比较重要的列名及其描述。oop


注意:

对于Android 1.5及如下的版本:不支持

对于Android 1.5以上2.1下(含2.1)的版本:受限支持。trace文件只能生成到SD卡,且必须在程序中加入代码。

对于Android 2.2上(含2.2)的版本:全支持。能够不用SD卡,不用在程序中加代码,直接本身用DDMS就能够进程Traceview。

2、TraceView工具面板介绍

有两方面用途: 

1  查看跟踪代码的执行时间,分析哪些是耗时操做  

2  能够用于跟踪方法的调用,尤为是Android Framework层的方法调用关系

获取方法的调用顺序

1. 在traceview中搜索响应的方法名不能使用大写字母

2. 搜索出的方法会自动展开,其中包含Parents 和 Children 两组信息

3. 点击Parents下的方法名,直接跳转到调用当前的方法处。Children相反


Traceview 面板分上下两部分:

1)上面是时间轴面板 (Timeline Panel)

左侧显示的是线程信息

右侧黑色部分是显示执行时间段、白色是线程暂停时间段,

右侧鼠标放在上面会出现时间线纵轴,在顶部会显示当前时间线所执行的具体函数信息

2)下面是分析面板(Profile Panel) -  每一列内容

Inclusive time  - 函数自己运行花费时间 + 函数调用其余函数时间

Exclusive time - 函数自己运行花费时间。

Calls + RecurCall/Total 调用 + 重复调用次数 / 函数总调用次数

Cpu Time/Call 总的Cpu时间与总的调用次数之比


3、TraceView工具如何使用

了解完 TraceView 的 UI 后,如今介绍如何利用 TraceView 来查找 hotspot。通常而言,hotspot 包括两种类型的函数:

1)一类是调用次数很少,但每次调用却须要花费很长时间的函数。

2)一类是那些自身占用时间不长,但调用却很是频繁的函数。

测试背景: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() 方法出现了问题,下面找到这个方法进行代码上的验证:

package com.sunzn.app.utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.HashMap;

import android.content.Context;
import android.graphics.Bitmap;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;

public class ImageLoaderTools {

    private HttpTools httptool;

    private Context mContext;

    private boolean isLoop = true;

    private HashMap<String, SoftReference<Bitmap>> mHashMap_caches;

    private ArrayList<ImageLoadTask> maArrayList_taskQueue;

    private Handler mHandler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            ImageLoadTask loadTask = (ImageLoadTask) msg.obj;
            loadTask.callback.imageloaded(loadTask.path, loadTask.bitmap);
        };
    };

    private Thread mThread = new Thread() {

        public void run() {

            while (isLoop) {

                while (maArrayList_taskQueue.size() > 0) {

                    try {
                        ImageLoadTask task = maArrayList_taskQueue.remove(0);

                        if (Constant.LOADPICTYPE == 1) {
                            byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET);
                            task.bitmap = BitMapTools.getBitmap(bytes, 40, 40);
                        } else if (Constant.LOADPICTYPE == 2) {
                            InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET);
                            task.bitmap = BitMapTools.getBitmap(in, 1);
                        }

                        if (task.bitmap != null) {
                            mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap));
                            File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
                            if (!dir.exists()) {
                                dir.mkdirs();
                            }
                            String[] path = task.path.split("/");
                            String filename = path[path.length - 1];
                            File file = new File(dir, filename);
                            BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap);
                            Message msg = Message.obtain();
                            msg.obj = task;
                            mHandler.sendMessage(msg);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    synchronized (this) {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }

            }

        };

    };

    public ImageLoaderTools(Context context) {
        this.mContext = context;
        httptool = new HttpTools(context);
        mHashMap_caches = new HashMap<String, SoftReference<Bitmap>>();
        maArrayList_taskQueue = new ArrayList<ImageLoaderTools.ImageLoadTask>();
        mThread.start();
    }

    private class ImageLoadTask {
        String path;
        Bitmap bitmap;
        Callback callback;
    }

    public interface Callback {
        void imageloaded(String path, Bitmap bitmap);
    }

    public void quit() {
        isLoop = false;
    }

    public Bitmap imageLoad(String path, Callback callback) {
        Bitmap bitmap = null;
        String[] path1 = path.split("/");
        String filename = path1[path1.length - 1];

        if (mHashMap_caches.containsKey(path)) {
            bitmap = mHashMap_caches.get(path).get();
            if (bitmap == null) {
                mHashMap_caches.remove(path);
            } else {
                return bitmap;
            }
        }

        File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);

        File file = new File(dir, filename);

        bitmap = BitMapTools.getBitMap(file.getAbsolutePath());
        if (bitmap != null) {
            return bitmap;
        }

        ImageLoadTask task = new ImageLoadTask();
        task.path = path;
        task.callback = callback;
        maArrayList_taskQueue.add(task);

        synchronized (mThread) {
            mThread.notify();
        }

        return null;
    }

}

以上代码便是 ImageLoaderTools 图片工具类的所有代码,先不着急去研究这个类的代码实现过程,先来看看这个类是怎么被调用的:

ImageLoaderTools imageLoaderTools = imageLoaderTools = new ImageLoaderTools(this);

Bitmap bitmap = imageLoaderTools.imageLoad(picpath, new Callback() {

    @Override
    public void imageloaded(String picPath, Bitmap bitmap) {
        if (bitmap == null) {
            imageView.setImageResource(R.drawable.default);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }
});

if (bitmap == null) {
    imageView.setImageResource(R.drawable.fengmianmoren);
} else {
    imageView.setImageBitmap(bitmap);
}

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 循环中,这样重复加载同一张图片时,死循环一出现线程就被挂起,这样就能够避免死循环的出现。代码以下:

private Thread mThread = new Thread() {

    public void run() {

        while (isLoop) {

            while (maArrayList_taskQueue.size() > 0) {

                try {
                    ImageLoadTask task = maArrayList_taskQueue.remove(0);

                    if (Constant.LOADPICTYPE == 1) {
                        byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET);
                        task.bitmap = BitMapTools.getBitmap(bytes, 40, 40);
                    } else if (Constant.LOADPICTYPE == 2) {
                        InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET);
                        task.bitmap = BitMapTools.getBitmap(in, 1);
                    }

                    if (task.bitmap != null) {
                        mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap));
                        File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
                        if (!dir.exists()) {
                            dir.mkdirs();
                        }
                        String[] path = task.path.split("/");
                        String filename = path[path.length - 1];
                        File file = new File(dir, filename);
                        BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap);
                        Message msg = Message.obtain();
                        msg.obj = task;
                        mHandler.sendMessage(msg);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
            
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

    };

};

最后再附上代码修改后代码运行的性能图,和以前的屡次被重复执行,效率有了质的提高,手机发烫、卡顿、高 CPU 占用率的现象也消失了。

附加:

若是想更精确到方法可使用这种方法,首先,必须在程序当中加入代码

Debug.startMethodTracing("love_world_");  
Debug.stopMethodTracing();

以便生成trace文件,有了这个trace文件咱们才能够将其转化为图形。

1)启动追踪

使用Debug的如下静态方法方法来启动:

static void startMethodTracing(String traceName)

使用指定trace文件的名字和默认最大容量(8M)的方式开始方法的追踪

static void startMethodTracing()

使用默认trace文件的名字(dmtrace.trace)和默认最大容量(8M)的方式开始方法的追踪

static void startMethodTracing(String traceName, int bufferSize, int flags)

使用指定trace文件的名字和最大容量的方式开始方法的追踪。并可指定flags.

注:int flags好像没意义。通常都用0.

static void startMethodTracing(String traceName, int bufferSize)

使用指定trace文件的名字和最大容量的方式开始方法的追踪。

注1:以上的方法的文件都会建立于SD卡下,即"/sdcard/"下,对默认文件名的就是"/sdcard/dmtrace.trace"

若是没SD卡,以上方法会抛异常导致程序crash.

注2:若是文件名没有指定类型,系统为其加上类型.trace

1)中止追踪

使用Debug的静态方法方法来中止:

public static void stopMethodTracing ()


例如,onCreate与onStart方法之间方法跟踪

public class MainActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
          
        Debug.startMethodTracing("Love_World_");  
    }  
  
    @Override  
    protected void onStart() {  
        super.onStart();  
          
        Debug.stopMethodTracing();  
    }  
      
}

添加SD卡访问权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

若是不添加,执行项目会出现如下异常:

java.lang.RuntimeException:Unable to open trace file '/mnt/sdcard/Love_World_.trace': Permission denied

若是手机没有SD卡也会出现一样的问题。

导出traceview文件

1)首先执行项目,查看trace文件是否生成

进入shell模式。

adb shell

查看是否已经生成这个文件。

ls sdcard/Love_World_.trace

Ctrl + C 退出adb shell模式。

2)导出trace文件

adb pull sdcard/Love_World_.trace

打开trace文件:

打开trace文件须要Android提供的traceview.bat工具,工具所在目录:sdk\tools\traceview.bat, 有两种方式执行:

1) 在命令行中切换到此目录。

2) 将此目录添加到系统环境变量中。

//  cmd在calc.trace所在目录执行
traceview C:\Users\YourName\Desktop\Love_World_.trace

其中“C:\Users\YourName\Desktop\” 表示trace所在你系统中的目录,此工具须要输入trace文件的绝对路径才行。

在新版本的SDK 会有如下提示:

The standalone version of traceview is deprecated.

Please use Android Device Monitor (tools/monitor) instead.

因此建议使用tools/monitor 启动后跟Eclipse DDMS界面差很少,而后File -> Open File -> 选择trace文件。

异常处理:

'C:\Windows\system32\java.exe' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
SWT folder '' does not exist.
Please set ANDROID_SWT to point to the folder containing swt.jar for your platfo
rm.

配置Java环境变量,把java bin 添加到系统环境变量PATH中。

异常信息:

The standalone version of traceview is deprecated.
Please use Android Device Monitor (tools/monitor) instead.
Failed to read the trace filejava.io.IOException: Key section does not have an *
end marker
at com.android.traceview.DmTraceReader.parseKeys(DmTraceReader.java:420)
at com.android.traceview.DmTraceReader.generateTrees(DmTraceReader.java:91)
at com.android.traceview.DmTraceReader.<init>(DmTraceReader.java:87)
at com.android.traceview.MainWindow.main(MainWindow.java:286)

一般是trace文件有异常,再从新生成并导出试试。

没有SD卡会出现异常:

Unable to open trace file '/sdcard/Love_World_.trace': Permission denied
 Caused by: java.lang.RuntimeException: Unable to open trace file '/sdcard/Love_World_.trace': Permission denied

生成的trace系统自动放在SDCARD上,没有sd卡因此会出现这种异常。