总述
本文基于 Android Studio 3.6.3,Android 9.0。
Memory Profiler 可帮助识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。
文本旨在 Memory Profiler 快速实战入门,详细文档参见官网。
基础说明
打开 Memory Profiler
1.点击工具栏中的 Profiler 图标。
2.点击内存轴上的任意位置以打开 Memory Profiler。
基础图示说明
基础图示.png
图中,曲线代表了不同对象占用的内存:
Java:从 Java 代码分配的对象的内存;
Native:从 C 或 C++ 代码分配的对象的内存;
Android 原生框架中内部使用了 C++,所以即使你的代码是纯 Java 创建对象,也可能是 Native 这一栏内存升高。总之,不用太纠结是哪一种内存增高。
Stack:原生堆栈和 Java 堆栈使用的内存。
Allocated:当前 Java 对象数,不包含 C/C++ 对象。
注意:
1.虚线即 Allocated 的对象数。
2.Allocation Tracking 一栏,务必选 Full。默认是 Simple,它会对监测做优化,导致虚线不显示、划定区域内的对象数并非真实对象数等等,进而影响内存泄漏检测。
3.由于 GC 回收机制 不是 对象一旦弃用就立即回收,因此你可能会看到虚线条一直增加。为了方便测试,需要即时点击垃圾回收按钮(图中标注1)。
实战
目标:使用 Memory Profiler 检测内存泄漏。
例子一 基础操作
说明
首先通过实战说明如何查看当前对象数以及对象数的创建、销毁。
下面给出了简单的例子,包名为 com.dixon.profiledemo:
public class SecondActivity extends AppCompatActivity {
private Card[] array = new Card[10000];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
for (int i = 0; i < array.length; i++) {
array[i] = new Card(i);
}
}
}
每次启动 SecondActivity,就创建 10000 个 Card 对象,其中 Card.class 代码如下:
public class Card {
private int num;
public Card(int num) {
this.num = num;
}
@Override
public String toString() {
return "Card{" +
"num=" + num +
'}';
}
}
再次说明这个例子是没有内存泄漏的,只为了说明如何查看对象数。
操作步骤
1.点击启动 SecondActivity。
2.在基础图示中,划定你要检测的范围。
这里我们划定的范围就是 SecondActivity 启动前和启动完毕的区间。划定完毕后,出现下面的界面:
划定范围
其中图中标注 1 就是我划定的范围。
图示说明
图中标注 1:划定的范围,即 SecondActivity 启动前到启动完毕的一段时间。
图中标注 2:筛选漏斗,可以使用它搜索我们应用自己类,以排除系统类的干扰。
图中标注 3:Class Name 这一栏列出了当前时间范围内的对象情况。其中:
Allocations:划定 范围内创建 的对象数。这里我们创建了 10000 个 Card 实例,所以 Lcom/dixon/profiledemo/Card 一栏显示 1000。
Deadllocation:划定 范围内销毁 的对象数。这里我们还没有退出 SecondActivity,所以 Lcom/dixon/profiledemo/Card 一栏显示 0。
Total Count:当前此对象类型的总对象数。
Shallow Size:当前此对象类型总共占用的内存,单位为字节。
通过图示,我们了解到,SecondActivity 内部确实创建了 10000 个 Card 对象。
再次操作
1.退出 SecondActivity;
2.点击垃圾桶按钮,强制执行垃圾回收。
前面说过,由于 GC 策略,退出 SecondActivity 后不会立即进行垃圾回收,所以需要手动强制执行以方便我们测试。
另外,为了保险起见,垃圾桶按钮至少应该 点击俩次以上,以保证完整回收。
点击垃圾桶按钮后,如果白色虚线保持高度不变,说明至少此刻没有垃圾可回收了。
3.划定检测范围。这里我们要划定 SecondActivity 退出前到垃圾回收彻底完毕这段区间。此时图示如下。
图示
可以看到,此段时间范围内,创建对象数为 0,销毁了 10000 个 Card 对象,以及 1 个 SecondActivity 对象。
多出的一个 Card 对象这里不用关心,是作者的例子里多写了个 Card 而已,懒得删了,直接忽略就好...
例子二 累积性内存泄漏分析
说明
关于累积性内存泄漏,这里给出示例代码:
public class MemoryLeak {
private static final List sList = new ArrayList<>();
public static void putContext(Context context) {
sList.add(context);
}
...
}
public class SecondActivity extends AppCompatActivity {
private Card card = new Card(0);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
MemoryLeak.putContext(this);
}
}
每次启动 SecondActivity,都会将一个新的实例添加到 MemoryLeak 的静态集合中,也就是说启动多少个 SecondActivity,就会有多少个 SecondActivity 不会被回收,该内存泄漏会不断累加,直至 OOM,是最危险的内存泄漏方式。
操作步骤
1.反复多次启动、退出 SecondActivity;
2.点击垃圾桶按钮,强制执行多次垃圾回收;
3.划定检测范围。本例中范围应该从 SecondActivity 第一次启动前到垃圾最后一次回收结束。
4.筛选。因为我要检测应用自身的内存泄漏,所以直接筛选自己的包名。
这里我进行了 5 次反复启动,3 次垃圾回收,此时划定范围结果如下:
累积性内存泄漏
分析
通过图表可以看出,在此期间内,Lcom/dixon/profiledemo/SecondActivity 一共创建了 5 个实例,并且回收次数为 0。说明 SecondActivity 确实存在严重的内存泄漏。
例子三 覆盖性内存泄漏分析
照旧先上例子:
public class MemoryLeak {
private static Context sContext;
public static void setContext(Context context) {
sContext = context;
}
}
public class SecondActivity extends AppCompatActivity {
private Card card = new Card(0);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
MemoryLeak.setContext(this);
}
每次启动 SecondActivity,都会有新的 SecondActivity 实例覆盖旧的存在内存泄漏的 SecondActivity 实例,即每次总有一个 SecondActivity 实例常驻内存。
操作步骤
操作步骤与前例完全相同。
这里我进行了 5 次反复启动,3 次垃圾回收,此时划定范围结果如下:
覆盖性内存泄漏
分析
通过图表可知,回收结束后,Deallocation 的数量总比 Allocations 小 1,即总有一个 SecondActivity 实例处于无法被回收的状态,因此可以得出 SecondActivity 存在内存泄漏。
例子四 短期性内存泄漏
说明
照旧,先上代码:
public class SecondActivity extends AppCompatActivity {
private Card card = new Card(0);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
如果子线程还没运行完毕,SecondActivity 就退出了,由于匿名内部类持有外部类的实例,所以会导致 SecondActivity 在休眠期间的 10s 内(实际是子线程销毁前)无法被回收,导致短期性内存泄漏。
操作
操作步骤同上。
这里我进行了 3 次反复启动,3 次垃圾回收,此时划定范围结果如下:
短期性内存泄漏
分析
从图表可以看出,这里创建了 3 个 SecondActivity 实例,但是内存回收结束后却没有销毁,确实存在内存泄漏。
但是如果 10s 后我们再次执行强制垃圾回收,这时的情况又会如何?
10s后再次强制回收
可见 10s 后这部分泄漏的内存确实可以回收了。
实际开发中,由于网络调用,这种场景可能会很常见,但是并非 10s 这么理想,很可能只有短短几秒,如果是频繁调度的重型页面,又恰好用户手机性能不佳,短期性的内存泄漏就可能导致你的应用崩溃。
而且这种问题很难排查,因为有可能在我们点击强制回收按钮时,线程已经执行完毕,外部类 Activity 已处于可回收状态,此时 Memory Profiler 将很难察觉。
好在还有另一款利器:LeakCanary,关于它不是本文重点,后续会单独说明。
总结
如果对开发者来说, CPU Profiler 用于分析应用卡顿,那么 Memory Profiler 则主要用于排查内存泄漏。
关于 Memory Profiler 还有更多深入的使用方式,详情参考 官方文档,本文只说明 Memory Profiler 的基础用法。
[TOC]