前文讲到了内存泄漏的原因,那么要怎么定位内存泄漏呢?这里列出了常用的分析工具及其使用方法
以下Heap Snapshot
、MAT
、Heap Viewer
、Allaction Tracking
、LeakCanary
和TraceView
资料均来源于网络
Heap Snapshot
获取Java堆内存详细信息,可以分析出内存泄漏的问题
在2.X版本中,Android Studio
使用的分析工具
点击Monitor
便可查看CPU
,Memory
,Network
,GPU
的情况
其打开面板如下:
该面板里的信息可以有三种类型: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来打开该工具:
- 打开MAT后选择
File -> Open File
选择我们刚才生成的hprof
文件 - 选择该文件后,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
下添加如下依赖
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:
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的数组中:
@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();
}
}
我们从首页跳转到详情页的时候会进入DetailActivity
的onCreate
的方法,然后就将当前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.201
,375.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=531
,375.201/531 = 0.7065
和Cpu 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;)V
,self
代表自身方法中的语句执行情况,由上面可以看出来,该方法没有多余语句,直接调用了其子类方法。另外一个子类方法,可以看出被当前方法调用了177次,但是该方法被其他方法调用过,因为他的总调用次数为892次,你可以点击进入子类方法的详细信息中 -
Parents while recursive
列举了递归调用当前方法的父类方法,以及其递归次数的占比,犹豫我们当前的方法递归了354
次,以上三个父类方法递归的次数分别为348+4+2=354
次 -
Children while recursive
列举了当递归调用时调用的子类方法
Lint分析工具
检测资源文件是否有没有用到的资源。
检测常见内存泄露
安全问题
SDK版本安全问题
是否有费的代码没有用到
代码的规范—甚至驼峰命名法也会检测
自动生成的罗列出来
没用的导包
可能的bug
Analyze -> Inspect Code
便可执行检查
可以检查project,Module和指定文件
详细信息