性能优化之内存分析工具的使用(整理)

前文讲到了内存泄漏的原因,那么要怎么定位内存泄漏呢?这里列出了常用的分析工具及其使用方法

以下Heap SnapshotMATHeap ViewerAllaction TrackingLeakCanaryTraceView资料均来源于网络

Heap Snapshot

获取Java堆内存详细信息,可以分析出内存泄漏的问题
在2.X版本中,Android Studio使用的分析工具
点击Monitor便可查看CPUMemoryNetworkGPU的情况

其打开面板如下:

该面板里的信息可以有三种类型:app heap/image heap/zygote heap
分别代表app堆内存信息,图片堆内存信息,zygote进程的堆内存信息

A区域

列举了堆内存中所有的类,一下是列表中列名:

名称 意义
Total Count 内存中该类的对象个数
Heap Count 堆内存中该类的对象个数
Sizeof 物理大小
Shallow size 该对象本身占有内存大小
Retained Size 释放该对象后,节省的内存大小

B区域

当我们点击某个类时,右边的B区域会显示该类的实例化对象,这里面会显示有多少个实体,以及详细信息

名称 意义
depth 深度
Shallow Size 对象本身内存大小
Dominating Size 管辖的内存大小

当你点击某个对象时,将展开该对象内部含有哪些对象,同时C区域也会显示哪些对象引用了该对象

C区域


点击查看

某对象引用树对象,在这里面能看出其没谁引用了,比如在内存泄漏中,可以看出来它被谁引用,比如上图,引用树的第一行,可以看出来,该对象被Object[12]对象引用,索引值为1,那我们展开后,可以看到,该Object[12]是一个ArrayList

在3.X版本,Android Studio采用了新的分析工具,但其使用都是类似的
其启动界面如下

分析界面如下

MAT

下载:http://eclipse.org/mat/downloads.php
MAT工具全称为Memory Analyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大,为了使用该工具,我们需要hprof文件。但是该文件不能直接被MAT使用,需要进行一步转化,可以使用hprof-conv命令来转化,但是Android Studio可以直接转化,转化方法如下
选择一个hprof文件,点击右键选择Export to standard .hprof选项

MAT工具所需的文件就生成了,下面我们用MAT来打开该工具:

  1. 打开MAT后选择File -> Open File选择我们刚才生成的hprof文件
  2. 选择该文件后,MAT会有几秒种的时间解析该文件,有的hprof文件可能过大,会有更长的时间解析,解析后,展现在我们的面前的界面如下

    这是个总览界面,会大体给出一些分析后初步的结论
  • Overview视图
    该视图会首页总结出当前这个Heap dump占用了多大的内存,其中涉及的类有多少,对象有多少,类加载器,如果有没有回收的对象,会有一个连接,可以直接参看(图中的Unreachable Objects Histogram)。
    比如该例子中显示了Heap dump占用了41M的内存,5400个类,96700个对象,6个类加载器。
    然后还会有各种分类信息:
    • Biggest Objects by Retained Size
      会列举出Retained Size值最大的几个值,你可以将鼠标放到饼图中的扇叶上,可以在右侧看出详细信息,在这里可以找到我们关心的内容
    • histogram视图
      histogram视图主要是查看某个类的实例个数,比如我们在检查内存泄漏时候,要判断是否频繁创建了对象,就可以来看对象的个数来看。也可以通过排序看出占用内存大的对象,默认是类名形式展示,也可以选择不同的显示方式
    • Dominator tree视图
      该视图会以占用总内存的百分比来列举所有实例对象,注意这个地方是对象而不是类了,这个视图是用来发现大内存对象的。这些对象都可以展开查看更详细的信息,可以看到该对象内部包含的对象
    • Leaks suspects视图
      这个视图会展示一些可能的内存泄漏的点

Navigation History中可以选择Histogram,然后右键加入对比,实现多个histogram数据的对比结果,从而分析内存泄漏的可能性

Heap Viewer

实时查看App分配的内存大小和空闲内存大小
发现Memory Leaks

  • 使用条件
    5.0以上的系统,包括5.0
    开发者选项可用

在2.x的Android Studio中,
可以直接在Android studio工具栏中直接点击小机器人启动
还可以在Android studio的菜单栏中Tools
或者是在sdk的tools工具下打开
在3.x的IDE中,默认已经找不到启动图标,但在tools目录下依旧可以打开使用

Heap Viewer面板如下

按上图的标记顺序按下,我们就能看到内存的具体数据,右边面板中数值会在每次GC时发生改变,包括App自动触发或者你来手动触发
总览:

列名 意义
Heap Size 堆栈分配给App的内存大小
Allocated 已分配使用的内存大小
Free 空闲的内存大小
%Used Allocated/Heap Size,使用率
Objects 对象数量

详情:

类型 意义
free 空闲的对象
data object 数据对象,类类型对象,最主要的观察对象
class object 类类型的引用对象
1-byte array(byte[],boolean[]) 一个字节的数组对象
2-byte array(short[],char[]) 两个字节的数组对象
4-byte array(long[],double[]) 4个字节的数组对象
non-Java object 非Java对象

下面是每一个对象都有的列名含义

列名 意义
Count 数量
Total Size 总共占用的内存大小
Smallest 将对象占用内存的大小从小往大排,排在第一个的对象占用内存大小
Largest 将对象占用内存的大小从小往大排,排在最后一个的对象占用的内存大小
Median 将对象占用内存的大小从小往大排,拍在中间的对象占用的内存大小
Average 平均值

当我们点击某一行时,可以看到如下的柱状图

横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量

使用:在需要检测内存泄漏的用例执行过后,手动GC下,然后观察data object一栏的total size(也可以观察Heap Size/Allocated内存的情况),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性

Allaction Tracking

追踪内存分配信息。可以很直观地看到某个操作的内存是如何进行一步一步地分配的
Allocation Tracker(AS)工具比Allocation Tracker(Eclipse)工具强大的地方是更炫酷,更清晰,但是能做的事情都是一样的

Allocation Tracker启动

在内存图中点击途中标红的部分,启动追踪,再次点击就是停止追踪,随后自动生成一个alloc结尾的文件,这个文件就记录了这次追踪到的所有数据,然后会在右上角打开一个数据面板

面板左上角是所有历史数据文件列表,后面是详细信息,好,现在我们来看详细介绍信息面板

下面我们用字母来分段介绍

  • A:查看方式选项
    A标识的是一个选择框,有2个选项

    Group by Method:用方法来分类我们的内存分配
    Group by Allocator:用内存分配器来分类我们的内存分配
    不同的选项,在D区显示的信息会不同,默认会以Group by Method来组织,我们来看看详细信息:

    从上图可以看出,首先以线程对象分类,默认以分配顺序来排序,当然你可以更改,只需在Size上点击一下就会倒序,如果以Count排序也是一样,Size就是内存大小,Count就是分配了多少次内存,点击一下线程就会查看每个线程里所有分配内存的方法,并且可以一步一步迭代到最底部

    当你以Group by Allocator来查看内存分配的情况时,详细信息区域就会变成如下

    默认还是以内存分配顺序来排序,但是是以每个分配者第一次分配内存的顺序

    这种方式显示的好处,是我们很好的定位我们自己的代码的分析信息,比如上图中,以包名来找到我们的程序,在这次追踪中包民根目录一共有五个类作为分配器分配了78-4-1=73次内存
  • B:Jump To Source按钮
    如果我们想看内存分配的实际在源码中发生的地方,可以选择需要跳转的对象,点击该按钮就能发现我们的源码,但是前提是你有源码

    如果你能跳转到源码,Jump To Source按钮才是可用的,都是跳转到类
  • C:统计图标按钮
    该按钮比较酷炫,如果点击该按钮,会弹出一个新窗口,里面是一个酷炫的统计图标,有柱状图和轮胎图两种图形可供选择,默认是轮胎图,其中分配比例可以选择分配次数和占用内存大小,默认是大小Size
  • 轮胎图

    轮胎图是以圆心为起点,最外层是其内存实际分配的对象,每一个同心圆可能被分割成多个部分,代表了其不同的子孙,每一个同心圆代表他的一个后代,每个分割的部分代表了某一带人有多人,你双击某个同心圆中某个分割的部分,会变成以你点击的那一代为圆心再向外展开。如果想回到原始状态,双击圆心就可以了。
    1.起点

    圆心是我们的起点处,如果你把鼠标放到我图中标注的区域,会在右边显示当前指示的是什么线程(Thread1)以及具体信息(分配了8821次,分配了564.18k的内存),但是红框标注的区域并不代表Thread1,而是第一个同心圆中占比最大的那个线程,所以我们现在把鼠标放到第一个同心圆上,可以看出来,我们划过同心圆的轨迹时可以看到右边的树枝变化了好几个值

    2.查看某一个扇面
    我们刚打开是全局信息,我们如果想看其中某个线程,详细信息,可以顺着某个扇面向外围滑动,当然如果你觉得不还是不清晰,可以双击该扇面全面展现该扇面的信息

    在某个地方双击时,新的轮胎图是以双击点为圆心,你如果想到刚才的圆,双击圆心空白处就可以

    3.一个内存的完整路径
  • 柱状图

    柱状图以左边为起始点,从左到右的顺序是某个的堆栈信息顺序,纵坐标上的宽度是以其Count/Size的大小决定的。柱状图的内容其实和轮胎图没什么特别的地方
    1.起点

    2.查看某一个分支

    3.Count/Size切换

LeakCanary

可以直接在手机端查看内存泄露的工具
实现原理:本质上还是用命令控制生成hprof文件分析检查内存泄露

添加LeakCanary依赖包

https://github.com/square/leakcanary
在主模块app下的build.gradle下添加如下依赖

1
2
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'

开启LeakCanary

添加Application子类
首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:
LeakCanary.install(this);

在配置文件中注册ExampleApplication

AndroidManifest.xml中的application标签中添加如下信息:
android:name=".ExampleApplication"

这个时候安装应用到手机,会自动安装一个Leaks应用,如下图

制造一个内存泄漏的点

建立一个ActivityManager类,单例模式,里面有一个数组用来保存Activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.android.sunshine.app;
import android.app.Activity;
import android.util.SparseArray;
import android.view.animation.AccelerateInterpolator;
import java.util.List;
public class ActivityManager {
private SparseArray<Activity> container = new SparseArray<Activity>();
private int key = 0;
private static ActivityManager mInstance;
private ActivityManager(){}
public static ActivityManager getInstance(){
if(mInstance == null){
mInstance = new ActivityManager();
}
return mInstance;
}

public void addActivity(Activity activity){
container.put(key++,activity);
}
}

然后在DetailActivity中的onCreate方法中将当前activity添加到ActivityManager的数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ActivityManager.getInstance().addActivity(this);
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.

Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());

DetailFragment fragment = new DetailFragment();
fragment.setArguments(arguments);

getSupportFragmentManager().beginTransaction()
.add(R.id.weather_detail_container, fragment)
.commit();
}
}

我们从首页跳转到详情页的时候会进入DetailActivityonCreate的方法,然后就将当前activity添加到了数组中,当返回时,我们没把他从数组中删除。再次进入的时候,会创建新的activity并添加到数组中,但是之前的activity仍然被引用,无法释放,但是这个activity不会再被使用,这个时候就造成了内存泄漏。我们来看看LeakCanary是如何报出这个问题的

演示


解析的过程有点耗时,所以需要等待一会才会在Leaks应用中,当我们点开某一个信息时,会看到详细的泄漏信息

TraceView

从代码层面分析性能问题,针对每个方法来分析,比如当我们发现我们的应用出现卡顿的时候,我们可以来分析出现卡顿时在方法的调用上有没有很耗时的操作,关注以下两个问题:

  • 调用次数不多,但是每一次执行都很耗时
  • 方法耗时不大,但是调用次数太多
    简单一点来说就是我们能找到频繁被调用的方法,也能找到执行非常耗时的方法,前者可能会造成cpu频繁调用,手机发烫的问题,后者就是卡顿的问题

TraceView工具启动

打开Monitor,点击图中的标注的按钮,启动追踪

TraceView工具面板

打开App操作你的应用后,再次点击的话就停止追踪并且自动打开traceview分析面板

traceview的面板分上下两个部分:

  • 时间线面板以每个线程为一行,右边是该线程在整个过程中方法执行的情况
  • 分析面板是以表格的形式展示所有线程的方法的各项指标

时间线面板


左边是线程信息,main线程就是Android应用的主线程,这个线程是都会有的,其他的线程可能因操作不同而发生改变.每个线程的右边对应的是该线程中每个方法的执行信息,左边为第一个方法执行开始,最右边为最后一个方法执行结束,其中的每一个小立柱就代表一次方法的调用,你可以把鼠标放到立柱上,就会显示该方法调用的详细信息

你可以随意滑动你的鼠标,滑倒哪里,左上角就会显示该方法调用的信息。
1.如果你想在分析面板中详细查看该方法,可以双击该立柱,分析面板自动跳转到该方法

2.放大某个区域
刚打开的面板中,是我们采集信息的总览,但是一些局部的细节我们看不太清,没关系,该工具支持我们放大某个特殊的时间段

如果想回到最初的状态,双击时间线就可以
3.每一个方法的表示

可以看出来,每一个方法都是用一个凹型结构来表示,坐标的凸起部分表示方法的开始,右边的凸起部分表示方法的结束,中间的直线表示方法的持续

分析面板

面板列名含义如下

名称 意义
Name 方法的详细信息,包括包名和参数信息
Incl Cpu Time Cpu执行该方法该方法及其子方法所花费的时间
Incl Cpu Time % Cpu执行该方法该方法及其子方法所花费占Cpu总执行时间的百分比
Excl Cpu Time Cpu执行该方法所话费的时间
Excl Cpu Time % Cpu执行该方法所话费的时间占Cpu总时间的百分比
Incl Real Time 该方法及其子方法执行所话费的实际时间,从执行该方法到结束一共花了多少时间
Incl Real Time % 上述时间占总的运行时间的百分比
Excl Real Time % 该方法自身的实际允许时间
Excl Real Time 上述时间占总的允许时间的百分比
Calls+Recur 调用次数+递归次数,只在方法中显示,在子展开后的父类和子类方法这一栏被下面的数据代替
Calls/Total 调用次数和总次数的占比
Cpu Time/Call Cpu执行时间和调用次数的百分比,代表该函数消耗cpu的平均时间
Real Time/Call 实际时间于调用次数的百分比,该表该函数平均执行时间

你可以点击某个函数展开更详细的信息

展开后,大多数有以下两个类别:

  • Parents:调用该方法的父类方法
  • Children:该方法调用的子类方法

如果该方法含有递归调用,可能还会多出两个类别:

  • Parents while recursive:递归调用时所涉及的父类方法
  • Children while recursive:递归调用时所涉及的子类方法

首先我们来看当前方法的信息

Name 24 android/widget/FrameLayout.draw(L android/graphics/Canvas;)V
Incl Cpu% 20.9%
Incl Cpu Time 375.201
Excl Cpu Time % 0.0%
Excl Cpu Time 0.000
Incl Real Time % 1.1%
Incl Real Time 580.668
Excl Real Time % 0.0%
Excl Real Time 0.000
Calls+Recur 177+354
Cpu Time/Call 0.707
Real Time/Call 1.094

根据下图中的toplevel可以看出总的cpu执行时间为1797.167ms,当前方法占用cpu的时间为375.201375.201/1797.167=0.2087,和我们的Incl Cpu Time%是吻合的。当前方法消耗的时间为580.668,而toplevel的时间为53844.141ms,580.668/53844.141=1.07%,和Incl Real Time %也是吻合的。在来看调用次数为177,递归次数为354,和为177+354=531375.201/531 = 0.7065Cpu Time/Call也是吻合的,580.668/531=1.0935,和Real Time/Call一栏也是吻合的

  • Parents
    现在我们来看该方法的Parents一栏
Name 22 com/android/internal/policy/impl/PhoneWindow$DecorView.draw(Landroid/graphics/Canvas;)V
Incl Cpu% 100%
Incl Cpu Time 375.201
Excl Cpu Time %
Excl Cpu Time
Incl Real Time % 100%
Incl Real Time 580.668
Excl Real Time %
Excl Real Time
Call/Total 177/531
Cpu Time/Call
Real Time/Call

其中的Incl Cpu Time%变成了100%,因为在这个地方,总时间为当前方法的执行时间,这个时候的Incl Cpu Time%只是计算该方法调用的总时间中被各父类方法调用的时间占比,比如Parents有2个父类方法,那就能看出每个父类方法调用该方法的时间分布。因为我们父类只有一个,所以肯定是100%Incl Real Time一栏也是一样的,重点是Call/Total,之前我们看当前方式时,这一栏的列名为Call+Recur,而现在变成了Call/Total,这个里面的数值变成了177/531,因为总次数为531次,父类调用了177次,其他531次是递归调用。这一数据能得到的信息是,当前方法被调用了多少次,其中有多少次是父类方法调用的

  • Children

    可以看出来,我们的子类有2个,一个是自身,一个是23android/view/View.draw(L android/graphics/Canvas;)Vself代表自身方法中的语句执行情况,由上面可以看出来,该方法没有多余语句,直接调用了其子类方法。另外一个子类方法,可以看出被当前方法调用了177次,但是该方法被其他方法调用过,因为他的总调用次数为892次,你可以点击进入子类方法的详细信息中

  • Parents while recursive

    列举了递归调用当前方法的父类方法,以及其递归次数的占比,犹豫我们当前的方法递归了354次,以上三个父类方法递归的次数分别为348+4+2=354

  • Children while recursive

    列举了当递归调用时调用的子类方法

Lint分析工具

检测资源文件是否有没有用到的资源。
检测常见内存泄露
安全问题
SDK版本安全问题
是否有费的代码没有用到
代码的规范—甚至驼峰命名法也会检测
自动生成的罗列出来
没用的导包
可能的bug

Analyze -> Inspect Code便可执行检查
可以检查project,Module和指定文件

详细信息

Donate comment here