内存泄漏

内存泄漏是 Android 开发常见的一类痛点问题,使用不当会有严重的性能影响,所以本文主要分析在开发过程中如何简单的进行内存泄漏检测,以及经常遇见的内存泄漏案例,最后借助手 q 的 LeakCanary 来分析内存泄漏检测的原理。

内存泄漏

在 Android(Java)平台上,对象是通过 GC(Garbage Collection,垃圾收集器)来回收内存的,而判断一个对象是否需要回收则采用的是 GC-Roots 的标记回收算法,如下图所示:

GC-ROOT

以 GC Roots 为起点遍历堆中的所有对象,对于没有直接或间接引用到 GC-Roots 的对象将会被标记为不可达状态,如图中的蓝色节点,在系统虚拟机 GC 过程中可能会被回收掉。可作为 GC-Roots 对象有:

  • 虚拟机栈(栈桢中的本地变量表)中的引用对象
  • 本地方法栈中 JNI 的引用对象
  • 方法区中的类静态属性引用对象
  • 方法区中的常量引用对象

Android(Java)平台的内存泄漏就是指本该销毁的对象资源仍然与 GC-Roots 保持引用关系,导致系统无法进行回收。

Android Studio 及 MAT 分析

在 Android 开发过程中难免会遇到内存泄漏的问题,但我们可以凭借强大的 Android Studio 和 MAT 工具进行快速定位。

生成 Java Heap 文件

在 Android Studio 中,我们可以借助 Android Monitors 工具帮助我们进行强制GC,再获取 Java Heap 文件。点击 Initate GC (1) 按钮,可以进行多次,充分 GC 。然后点击 Dump Java Heap(2) 按钮,生成的 Java Heap 文件会在新的窗口打开。

java_heap

分析内存泄漏的 Activity

点击 Analyzer TasksPerform Analysis (1) 按钮,即可找出内存泄漏的 Activity (2)。

analyzer_tasks

这个例子比较简单,到这里就可以很直观的看到问题所在。但比较复杂的问题 Android Studio 并不够直观,也不够 MAT 方便,这时我们可以使用 MAT 来进一步分析。

转换成标准的 hprof 文件

Java Heap 是在一个特定时间点的 Java 进程的内存快照,在 MAT 中需要转储为一个 hprof 格式的二进制文件供分析,一个普通的 hprof 文件内,包含下面的信息:

  • 所有的对象
  • 所有的类
  • GC Roots

在 Android Studio 中选中 Captures ,右击选中的 hprofExport to standard .hprof 选择保存的位置,就能生成一个标准的 hprof 文件。

captures

MAT 打开 hprof文件

MAT 的 下载地址,使用 MAT 打开刚才生成的 hprof 文件。点击 (1) 按钮打开 Histogram。在 (2) 的地方搜索可能产生内存泄漏的 Activity 名称。

histogram

  • Shadow Heap:对象自身占用的内存大小,不包括它引用的对象。
  • Retained Heap:当前对象大小+当前对象可直接或间接引用到的对象的大小总和。

查看引用链

右击搜索出来的类名,选择 Merge Shortest Paths to GC Rootsexclude all phantom/weak/soft etc. references ,排除弱引用和软引用,因为弱引用和软引用是会被GC回收的引用,不会产生内存泄露,所以分析内存问题时把这两种引用关系排除掉。

exclude_gcroots

分析泄漏原因

排除平台/弱/软引用后,根据引用链可以看到,LeakActivity 被 this$1 所引用的,它实际上是匿名类对当前类的引用。this$1 又被callback所引用,接着它又被Message中一串的next所引用。到这里,我们已经找到了内存泄漏的原因了,接下来对定位到的问题代码进行改善即可。

leak_analysis

以下是测试内存泄漏的代码,不断重进退出 Activity 时,原先的 Message 一直保持对原有的 Activity 的引用,且没有及时 remove 掉,所以在消息队列中积累的 Activity 引用越来越多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mHelloTv = (TextView) findViewById(R.id.hello_tv);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(0);
}
}, 10000);
}

public Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
if (mHelloTv != null) {
mHelloTv.setText("hello leak");
}
break;
}
}
};

Android studio 3.0 的 Profiler

在 Android studio 3.0 上 Android profiler 更加强大,cpu、memory 和 network 的页面都有很大的变化。以下是上述 handler 泄漏的例子,不断的进入和退出 LeakActivity。

android_studio_3.0

  • 选择 gc 后进行一次 dump java heap
  • 可以选择根据包名查看当前内存的堆栈情况
  • 找到可能产生内存泄漏的 Activity,可以在右上方看到进行 gc 后还未释放的实例
  • 选择其中一个实例,可以找到其引用的 gc 链。在右下方可以看到 Message 一直保持对原有的 Activity 的引用,由此可以大概确定是使用 handler 不当造成的内存泄漏。

内存泄漏经典案例

Static 泄漏

Static 变量存储在方法区中,是 Android 判断对象回收的 GC-ROOTS 起点,它贯穿着应用的整个生命周期。如果 Static 对象对其他对象的引用超过了该对象的生命周期,则会造成内存泄漏。

例子:

1
2
3
4
5
6
7
8
public class LeakActivity extends AppCompatActivity {
private static Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
}
}

如果在 onDestroy() 中没有及时取出静态变量 mContext 的引用,则会产生泄漏,其内存泄漏引用链:

static

解决方案:在引用对象的生命周期结束时,去除 Static 变量的引用。

监听/回调泄漏

很多情况下,为了进行数据通信,我们为此设置了监听器或回调。但在使用过程中往往忽略了在结束时销毁监听器/回调,从而导致了内存泄漏。

例子:

1
2
3
4
5
6
public void addUpdateListener(AnimatorUpdateListener listener) {
if (mUpdateListeners == null) {
mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
}
mUpdateListeners.add(listener);
}

如果没有在销毁时及时去掉监听器/回调,则会造成引用的内存泄漏。

解决方案

  • 在 引用对象的生命周期结束时,去掉回调/监听器
  • 对于一些监听器,如果需要持有的是 Context,则可以用 ApplicationContext 代替 ActivityContext,使得引用对象的生命周期和应用的生命周期保持一致。

匿名内部类/非静态内部类和异步线程

在 Java 中,非静态内部类和匿名类都会潜在的引用它们所属的外部类,但是静态内部类却不会。如果非静态内部类实例做了一些耗时的操作,在外部类本该销毁时还没有完成耗时任务,就会使得外部对象回收不了,从而导致了内存泄漏。

解决方案:

  • 将内部类变成静态内部类
  • 如果对外部类有强引用,则改为弱引用
  • 在外部对象销毁时,结束耗时任务

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mHelloTv = (TextView) findViewById(R.id.hello_tv);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(0);
}
}, 10000);
}

public Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
if (mHelloTv != null) {
mHelloTv.setText("hello leak");
}
break;
}
}
};

这里把 handler 理解为一个非静态的内部类,由于 handler 对 Activity 持有引用,间隔 10 秒发送一条消息给 MessageQueue,这时如果 Activity 想要销毁,则会造成内存泄漏,因为那条消息对 Activity 持有一个引用,而消息还在队列中没有清除。他们的引用链如下所示:

leak_analysis

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 	private static class MyHandler extends Handler {
private final WeakReference<Activity> mActivityReference;
MyHandler(Activity activity) {
this.mActivityReference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = (MainActivity) mActivityReference.get();
switch (msg.what) {
case 0:
if(activity.mHelloTv != null){
activity.mHelloTv.setText("hello leak");
}
break;
}
}
}

@Override
protected void onDestroy() {
super.onDestroy();
handler.removeMessage(0)
}
  • 使用静态内部类
  • 使用弱引用持有 Activity
  • onDestroy 时去除 callback 或 message

非UI线程使用 View.post()

在 API 低于 24 的版本上,非 UI 线程使用 View.post,可能会导致内存泄漏。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
new Thread(new Runnable() {
@Override
public void run() {
view.post(new Runnable() {
@Override
public void run() {
//
}
});
});
}
}).start();

接下来我们根据不同的 API 版本,来分析这段代码为什么会有引发内存泄漏。

低于 API 24 的版本

追踪 View.post 的源码:

1
2
3
4
5
6
7
8
9
10
11
//View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}

// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}

这里如果有 attach,则直接使用 handler 抛到消息队列中等待执行。但当 attachInfo 为空时,使用了ViewRootImpl 的 runQueue 进行 post,假设 runnable 会在 attach 后执行,就把它放到了 mRunQueue 的 mActions 的队列中。那这里会不会就有可能一直在这个队列里,得不到执行呢?

1
2
3
4
5
6
7
8
9
10
11
12
//ViewRootImpl.java
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}

可以看到 sRunQueues 是 ThreadLocal 类型,而 ThreadLocal 这是一个线程私有变量,所以每个线程拥有的RunQueue 都是独立的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static final class RunQueue {  
private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();

void post(Runnable action) {
postDelayed(action, 0);
}

void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;

synchronized (mActions) {
mActions.add(handlerAction);
}
}

void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<handleraction> actions = mActions;
final int count = actions.size();

for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}

actions.clear();
}
}

RunQueue 是一个 mActions 的集合,每个 actions 包含了等待执行的 runnable 和 delay。那什么时候这些 runnable 会被执行呢?

1
2
3
4
5
6
7
8
9
10
11
private void performTraversals() {  
……
if (mLayoutRequested && !mStopped) {
// Execute enqueued actions on every layout in case a view that was detached
// enqueued an action after being detached
getRunQueue().executeActions(attachInfo.mHandler);
……
}

……
}

查看调用关系,是在该 RunQueue 所在的线程执行 performTraversals 时调用,可 performTraversals 只会在UI 线程才会被调用。这也就意味着在异步线程调用 View.post(),如果 view 属于 detach 状态,那么,这个 runnable 将永远不会执行。

所以在非 UI 线程调用 View.post 时,如果 View 处于 detach 状态,那就会发生内存泄漏。

API 24 及以上

View.post 这个接口在 API 24 上有所修改,我们可以看看有什么不一样的地方:

view_post_runnable

很显然,这里不再使用 ViewRootImpl,而是直接用 View 本身的 getRunQueue 接口抛出这个 action。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Queue of pending runnables. Used to postpone calls to post() until this
* view is attached and has a handler.
*/
private HandlerActionQueue mRunQueue;

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}

而 mRunQueue 也只是 View 的一个私有变量,再看调用 HandlerActionQueue.executeActions 的地方:

view_dispatch

发现在 View 被 dispatch 时,就会依次取出 mActions,再一个一个投递到 handler 的消息队列里。

再看下在在什么时机会执行 HandlerActionQueue.removeCallbacks 来销毁:

view_removeCallbacks

由此可见,只要在引用对象销毁时调用 view.removeCallbacks 就可以避免内存泄漏了。

解决方案

  • 要保持兼容 API 24 以下的版本,最好不要在非 UI 线程使用 View.post ,而采用 Handler 来 post runnable。
  • 当然在 API 24 及以上,如果不能保证 View 一定会被 attach,那可以在引用对象销毁时,使用 View.removeCallbacks 。

泄漏检测原理

为了及时发现内存泄漏问题,我们会通常会使用弱引用进行一些内存泄漏的检测,其原理一般如下:

  • 弱引用不会影响系统 GC 对该对象的回收
  • 获取 onDestroy 回调,并添加弱引用探针
  • 在子线程循环检测是否可以通过该探针获取到该对象的实例
知道是不会有人点的,但万一被感动了呢?