TraceView 是 Android 平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到 method。详细内容参考:Profiling with Traceview and dmtracedump

TraceView 简介

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

  • 开发者在一些关键代码段开始前调用 Android SDK 中 Debug 类的 startMethodTracing 函数,并在关键代码段结束前调用 stopMethodTracing 函数。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是 Java 线程)的函数执行情况,并将采集数据保存到 /mnt/sdcard/ 下的一个文件中。开发者然后需要利用 SDK 中的 TraceView 工具来分析这些数据。
  • 借助 Android SDK 中的 DDMS 工具。DDMS 可采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的情况。

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

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

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

  • 左边 Pane 显示的是测试数据中所采集的线程信息。由图可知,本次测试数据采集了 main 线程,传感器线程和其它系统辅助线程的信息。
  • 右边 Pane 所示为时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等。由图可知,Thread-1412 线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。
  • 另外,开发者可以在时间线 Pane 中移动时间线纵轴。纵轴上边将显示当前时间点中某线程正在执行的函数信息。

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

TraceView 实战

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

 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 占用率的现象也消失了。

Android 编程下的 TraceView 简介及其案例实战的更多相关文章

  1. Android 编程下的自定义 xmlns

    什么是 xmlns xmlns是 XML Namespaces 的缩写,中文名称是 XML命名空间. xmlns 使用规则 xmlns:namespace-prefix="namespace ...

  2. Android 编程下 Touch 事件的分发和消费机制

    Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev).onInterceptTouchEvent(MotionEvent ev). ...

  3. Android 编程下 App Install Location

    从 API 8 开始(参考官方文档:App Install Location | Android Developers),你可以将你的应用安装在外部储存中(例如,安装到设备的 SD 卡上).这是一个可 ...

  4. Android 编程下 DP、SP 以及屏幕像素密度

    有时需为视图属性指定大小尺寸值(通常以像素为单位,但有时也用点.毫米或英寸).最常见的属性有: 文字大小(Text Size),指设备上显示的文字像素高度: 边距(Margin),指定视图组件间的距离 ...

  5. Android 编程下的四大组件之服务(Service)

    服务(Service) 是一种在后台运行,没有界面的组件,由其他组件调用开始.Android 中的服务和 Windows 中的服务是类似的东西,它运行于系统中不容易被用户发觉,可以使用它开发如监控之类 ...

  6. Android 编程下的 Secret Code

    我们很多人应该都做过这样的操作,打开拨号键盘输入 *#*#4636#*#* 等字符就会弹出一个界面显示手机相关的一些信息,这个功能在 Android 中被称为 Android Secret Code, ...

  7. Android 编程下图片的内存优化

    1. 对图片本身进行操作 尽量不要使用 setImageBitmap.setImageResource. BitmapFactory.decodeResource 来设置一张大图,因为这些方法在完成 ...

  8. Android 编程下的代码混淆

    什么是代码混淆 Java 是一种跨平台的.解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中.由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名.方法名,并且通 ...

  9. Android 编程下去除 ListView 上下边界蓝色或黄色阴影

    默认的情况下,在 ListView 滑动到顶部或者是底部的时候,会有黄色或者蓝色的阴影出现.在不同的版本上解决的方法是不同的,在 2.3 版本之前可以在 ListView 的属性中通过设置 andro ...

随机推荐

  1. java8之lambda表达式(2)-方法引用

    方法引用使用的地方也是在函数式接口,使用方法引用可以使代码更加简单和便捷 在如下代码中 根据List中字符串长度排序的代码可以写成如下: public static void test1_() { L ...

  2. 微信web开发者工具、破解文件、开发文档和开发Demo下载

    关注,QQ群,微信应用号社区 511389428 下载: Win: https://pan.baidu.com/s/1bHJGEa Mac: https://pan.baidu.com/s/1slhD ...

  3. Android开发艺术探索学习笔记(十)

    第十章  Android的消息机制 面试中经常会被问到的一个问题:handler是如何在子线程和主线程中进行消息的传递的,这个问题通过了解Android的消息机制可以得到一个准确的答案. Androi ...

  4. 【转】获取到元素的 offsetLeft 、offsetTop属性不正常的解决方法。

    原地址:http://hi.baidu.com/huidust520/item/85da006981a6c635ad3e834e 我在工作中遇到个问题: 在ie7下和360浏览器下获取到的  offs ...

  5. mpvue小程序图片404

    mpvue开发小程序时候,要添加静态本地图片 <img src="../../images/bg.png" alt=""> 会报错: VM14878 ...

  6. 安装python-devel开发包

    1.概述 有时在安装某些软件的时候,会报错: Error: must have python development packages -devel, python2.-devel, python2. ...

  7. Vue笔记:使用 axios 发送请求

    在Vue1.0的时候有一个官方推荐的 ajax 插件 vue-resource,但是自从 Vue 更新到 2.0 之后,官方就不再更新 vue-resource. 关于为什么放弃推荐? -> 尤 ...

  8. zmq 三种模型的python实现

    1.Request-Reply模式: 客户端在请求后,服务端必须回响应 server: #!/usr/bin/python #-*-coding:utf-8-*- import time import ...

  9. webpack4 自学笔记四(style-loader)

    全部的代码及笔记都可以在我的github上查看, 欢迎star:https://github.com/Jasonwang911/webpackStudyInit/tree/master/css 引入- ...

  10. RowFilter遇上特殊字符*%'[]\

    开发时,时不时需要分析DataView的数据.计算,过滤等 .但是处理的值有可能会遇上是带有特殊字符*.%.'.[.]和\ .需要手动在表达式中,处理这些特殊字符,需要再使用"[" ...