之前学习的JVM、DVM、ART就是为了这一章做铺垫的。
这篇先从避免内存泄露开始学习,然后学习常用的内存分析工具 Memory Monitor、Allocation Tracker和Heap Dump。
最后分析内存泄漏的利器MAT和LeakCanary。
内存泄漏是内存优化的重点,如果一旦出现,就会让应用变得非常棘手。所以我们要解决关于内存泄漏的东西。
为了确保Android系统的每个应用都有足够的内存,Android系统需要有效地管理内存分配,当内存不足的时候,就会触发GC,GC采用的垃圾标记算法为根搜索算法。
如果根能到达的对象就是可达对象,不可达的对象就是回收对象。
内存泄漏就是指没有用的对象到GC Roots是可达的(对象被引用),导致GC无法回收该对象。此时,如果一个对象没有任何用处,但是它可达,如下图所示:
比如上图中的 Obj3,它是GC Roots可达的,所以标记算法并不会把Obj3标记为回收对象,但是在项目中,Obj3并没有被引用。
那这个时候Obj3就会造成内存泄漏。
内存泄漏产生的原因一般有三个:
在通常情况下,第二种和第三种情况对于Android应用开发者来说是不可控的,但是第一种情况是可控的,既然是可控的,我们就要尽量在编码时避免造成内存泄漏。
之前我写过一篇blog,较为浅层的解析了以下内存泄漏的问题:Android内存泄漏问题
这里面分讲解了好几个场景以及代码示例,所以这边一些代码上的东西就不会再细纠了,下面列出来这些场景:
removeCallbacksAndMessages(null)
register
和 ungister
监听器,我们需要确保在合适的时候及时unregister
那些监听器。自己手动添加的Listener要记得在合适的时候及时移除这个 Listener这里就开始讲解内存分析的工具,本节就来讲解Memory Monitor。
在AS中Android Monitor
是一个主窗口,它包含了Locat、Memory Monitor,CPU Monitor、GPU Monitor和Network Monitor。
但是在Android Stduio3.0后,AS优化了Android Monitor,把Android Moniter的东西整合到了 Profiler
中,在Android Studio中连接手机然后打开应用,我们在Profiler中可以去查看这个窗口:
可以看到它分为四大块,CPU、MEMORY(内存)、NETWORK(网络)、ENERGY(电量)。
其中 Memory Monitor可以轻松监视应用程序的性能和内存使用情况,以便找到被分配的对象,定位内存泄漏,并跟踪连接设备中正在使用的内存数量,我们点开MEMORY,它的界面是这样的:
Memory Monitor可以报告出你的应用程序的内存分配情况,更形象地呈现出引用程序使用的内存。
它的作用如下:
而在上面的截屏中我标注了几个常用的该工具的功能,按照标注解释如下:
其中横轴是时间,纵轴是分配的内存。
下图是一个大内存分配的场景--------分配得内存在短时间内急速上升:
我们需要判断这次是否是合理分配的内存,是Bitmap还是其他的大数据,并且对这种大数据进行优化,减少内存开销。
接下来分配得内存突然下降,这表示发生了GC,用来释放内存。
内存抖动一般是指 在很短的时间内发生了多次的内存分配和释放:
严重的内存抖动会导致应用卡顿。
内存抖动出现的原因是在短时间内频繁的创建对象(比如在循环中创建很大的对象),内存为了应对这种情况,也会频繁的进行GC,其他线程都会被挂起,等待GC操作完成后恢复工作。
如果是频繁的GC就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过16ms,最后导致界面卡顿。
如果出现了内存抖动,我们要查看我们的 循环、onDraw等会被执行多次的函数体。
因为 new
一次对象是个耗时耗空间的操作,值得引起重视。
Allocation Tracker用来跟踪内存分配,它允许你在执行某些操作的同时监视在何处分配对象,了解这些分配使你能够调整与这些操作相关的方法调用,用来优化程序性能和内存使用。
Allocation Tracker能够做到如下的事情:
我们在柱状图中,随便的截取一段,就能产生内存的分析:
我们能够看到一些对象,它总共的个数、大小,我们还可以根据类、包和线程来进行排序。
这些对象,分为可控和不可控两种类型:
int[]
或 String
或其他,在一般情况下,我们不会去考虑这些 int[]
、String
变量new
等声明一个变量,它是在我们程序中直观的表示的出来的。在上面的图片中,我们随便点击一个对象,如果这个对象是可控的,那么我们就能追溯到代码中,如下图:
图中,我选取了一个 LinearLayout
(标注1),可以看到LinearLayout在程序中有多个Instance(标注2),也就是说在我们程序中写了很多个LinearLayout出来,然后我们随便点击一个一个,可以看到它创建的代码栈(标注3),然后我们在代码栈中,可以找到我们在程序中创建出来的地方(标注4)。
可以看到我是在一个RecyclerView
的Adapter
里面去new出了一个item,我的每一个Item的构造就是一个LinearLayout,而一个RecyclerView里面是有多个item,这也是为什么,在标注2中显示我在很短的时间里面创建出了这么多的 LinearLayout。
Heap Dump的主要功能就是查看不同的数据类型在内存中的使用情况。
它可以帮助我们找到大对象,也可以通过数据的变化发生内存泄漏。
我们通过点击 Dump Java Heap
或者 Crtl + D
来捕获一段Heap Dump:
可以看到其分析原理也差不多,不过它可以通过点击最左边的图标的保存文件,保存成一个 .hprof
文件,便于我们后面使用MAT工具进行分析。
我们可以看到这个图和之前分析内存那个有点不一样,这个图最右边的 Refernces
表示的是该对象的信息:
分析你的heap,按照一下步骤.
Class Name
列表,看看有没有大量对象存在,并且这些对象你认为是不应该存在的,可能存在内存泄漏的情况. 点击类名可以看到详细的对象信息.Instance View
面板中,点击一个实例References
面板就会显示出来,里面都是使用该Instance的Reference,点击箭头可以看到引用它的所有区域。点击鼠标右键可以选择go to instance
去看到引用该引用的引用,或者jump to source
去看调用的源代码.在上面的Heap Dump中我们提到了一个 .hprof
文件,在我们分析内存的时候,我们有时候无法很确定内存泄漏的地方,我们会通过 Heap Dump来生成一个 疑似发生内存泄漏时所产生的 堆存储文件,它就是 .hprof
文件。
而MAT
就是用来分析 .hprof
文件的。
MAT的全称为 Memory Analysis Tool,是对内存进行详细分析的工具,它是Eclipse的插件,AS需要单独下载。
下载地址在 MAT官网下载网址
我们打开来运行一下刚刚生成的 .hprof
文件,可能报以下的错误:
...Unknown HPROF Version (JAVA PROFILE 1.0.3)..
这个意思的是我们由Android虚拟机转出来的.hprof
文件和 MAT所分析的 .hprof
(用JVM)的格式不一样,我们需要转化文件格式。
我们先通过命令行,进入到 该.hprof
所在目录,比如我有一个叫做 android.hprof
由Android导出来的 ,然后执行下面命令:
hprof-conv android.hprof mat.hprof
就会在该目下出现一个 mat.hprof
的文件,我们就可以用这个文件导入啦:
下面我们来举个实例,来介绍怎么使用MAT。
我们先准备一份内存泄漏的代码出来:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面做了一个 非静态内部类LeakThread持有外部 MainActivity的引用,LeakThread中做了耗时操作,导致MainActivity无法被释放。
接着我们运行它,产生一个 .hprof文件。然后我们打开Profiler,横竖屏切换,然后导出一个 .hprof文件,然后通过 hprof-cov转换它,最后拿转换出来的文件导入到 MAT中。
用MAT打开hprof文件后,MAT就会产生一个报告,分为两个标签页,分别是 Overview
和 Leak Suspects
。
在Leak Suspects中给出了MAT认为可能出现内存泄漏问题的地方,在上图中共给出了2个内存泄漏的猜想,通过单击每个猜想的Details可以看到更深入的分析情况。如果内存泄漏不是特别明显,通过Leak Suspects很难发现内存泄漏的位置。
打开Overview标签页,首先看到的是一个饼状图:
它主要用来显示内存的消耗,饼状图的彩色区域代表被分配的内存,灰色区域表示空闲内存区。
再往下看,能看到饼状图下面有 Actions
:
其中分析内存泄漏最常用的就是 Histogram
和 Dominator Tree
。下面我来介绍它怎么使用。
Dominator Tree
的意思是支配树,它更善于去分析对象的引用关系,如下图所示:
左边主要是Class Name类名,右边是三列之前都看到过:
Retained Set
包含对象所占内存的总大小。Retained Set指的是这个对象本身和它持有引用的对象以及这些对象的Retained Set
所占内存大小的总和,引用树官方的图解如下图:
从上图可以看出 E的Retained Set
为E和G,C的Retained Set
为C、D、E、F、G、H。
MAT所定义的支配树就是从上图中的树演化而来。
在引用树中,如果一条到Y的路径必然会经过X,则称为X支配Y。X直接支配Y则指的是在所有支配Y的对象中,X是Y最近的一个对象。支配树反映的就是这种直接支配关系,在支配树中,父节点直接支配子节点。
下图就是官方提供的一个从引用树到支配树的转换示意图。
上图中 由于C直接到达D和E,所以C是D和E的父节点。而到达H有两条路径 DF和EG,但是这两条是互斥的,对于互相来说都是不必要的,抽象一层,相当于C能直接到达H,所以C是H的父节点~
通过支配树,我们就能很容易的分析一个Retained Set
所以 假如 E被回收,E、G的内存会被释放,而H不会。因为可能F还引用着H
所以通过MAT提供的Dominator Tree,我们可以很清晰地得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。在Dominator Tree的顶部Regex可以输入过滤条件,如果查找的是Activity内存泄漏,可以在Regex中输入Activity的名称,比如这个例子可以输入MainActivity:
可以看到MainActivity出现了14个实例,而项目中MainActivity
是不能有那么多实例的,基本可以判定发生了内存泄漏。
具体内存泄漏的原因,可以查看GC引用链,在MainActivity选项上单击右键->Path To GC Roots。
可以看到根据引用类型会有多种选项,比如 with all references就是包含所有的引用,这里我们选择exclude all phantom/weak/soft etc. references,因为这个选项排除了虚引用,弱引用和软引用,这些引用一般是可以被回收的,这是MAT就会给出MainActivity的GC引用链,如下图所示:
可以看到,引用MainActivity的就是LeakThread,this$0的含义就是内部类自动保留一个指向所在外部类的引用,而这个外部类就是MainActivity,这将会导致MainActivity无法被GC。
Histogram与Dominator Tree不同的是,Dominator Tree是从对象实例的角度进行分析,注重引用关系分析,而Histogram则从类的角度进行分析,注重量的分析。
这里有四列,Obejcts表代表对象实例的个数。我们同样可以在这里输入过滤条件 MainActivity。
可以看到MainActivity和LeakThread的示实例各有6个,基本上可以断定发生了内存泄漏,具体内存泄漏的原因,同样可以查看GC引用链。在MainActivity选项上单击鼠标右键->Merge Shortest Paths to GC roots,并在选项中选择 exclude all phantom/weak/soft ets. references,Path To GC Roots的信息如下图所示:
Histogram是从类的角度进行分析,而Path To GC Roots是用来分析单个对象的,因此在Histogram中无法使用Path To GC Roots查询,可以使用 Merge Shortest Paths to GC Roots查询,他表示从GC roots到一个或一组对象的公共路径。得出的结果和之前是相同的,引用MainActivity的是Leak Thread。
OQL全称为Object Query Language,类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有对象。
它的查询语句的基本格式如下:
SELECT * FROM [ INSTANCEOF ] <class_name> [ EHERE <fliter-expression>]
我们输入 select * from instanceof android.app.Activity
并按F5,将当前内存中所有的Activity都显示出来:
当然如果Activity太多,也可以输入具体完整的名称,然后就可以右键他们去查看GC引用链。
因为之前举得例子很简单,可以通过上面的方法可以找到内存泄漏的原因,但是复杂的情况就需要通过对比hprof文件来进行分析了。
我们再生成一个hprof文件然后用MAT打开。对比步骤为:
(1)将每个hprof文件的 Domiator Tree或者Histogram添加到 Compare Basket
中
(2)在Compare Basket中单击红色叹号生成Compared Tables
:
可以看到第一个文件有LeakThread和MainActivity各有6个,而第二个文件的MainActivity实例只有1个,对比之下,说明第一个文件发生了内存泄漏。
到这里我们就介绍完了MAT的常用功能,当然MAT还有其他的功能,需要我们在之后使用中去积累了。
如果使用MAT来分析内存问题,会有一些难度,并且效率也不是很高,对一个内存泄漏的问题,可能要多排查和对比。
为了能够迅速地发现内存泄漏,Square公司基于MAT开源了LeakCanary。Github-leackcanary
首先要导入leakcanary:
dependencies {
...
//这里是2.2版本,和2.0之前的版本玩法很不一样
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}
如果在LeakCanary2.0之前的版本还要去App中加入下面的代码
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
//如果当前的进程是用来给LeakCanary进行堆分析的则返回,否则会执行LeakCanary的install方法
return;
}
LeakCanary.install(this);
}
这样我们在打开App的时候开始检测。LeakCanary2.0前后版本变化较大,在LeakCanary2.0版本之前,需要在Application中初始化LeakCanary,在Activity、Fragment等地方要我们手动的在 onDestroy()
中取监控GC情况。
而2.0版本之后连初始化都不用了,它会自己运行在主进程中。如果检测到某个Activity有内存泄漏,LeakCanary就会提示。
我们还是拿之前的内存泄漏的代码举例子:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(60 * 60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MainActivity存在内存泄漏,这个例子是之前讲过的典型的 非静态内部类持有外部类引用而导致的内存泄漏。
然后我们运行程序, 安装完后,我们发现不仅安装好了这个App,还多了一个 Leaks
的App出来:
然后我们打开App,不断的横屏和竖屏,会发现Leak Canary通过 Notification显示了出来:
我们打开这个通知栏:
从图中就是一个 疑似内存泄漏报告
,它会用 undline
即下划线,标出疑似内存泄漏的地方
它有下面几个信息:
MainActivity
Leaksthis$0
的引用,介绍LeakCanary就到这里,更多玩法要自己去摸索。
我们可以看出,LeakCanary也是通过分析 heap dump
追踪的 .hprof
文件,给出一份可视化、直观的内存分析报告。
这样相比于MAT我们直接去分析,LeakCanary更加高效、傻瓜式。而且也没有人说用MAT来分析就更加厉害、用LeakCanary就是懒的,没有这种说法。所以不妨的更多的去了解 LeakCanary这么一个优秀的工具,让我们的程序变得更好吧。