Android Handler 内存泄漏分析&解决方案

一、前言

我们在开发过程中,经常使用 Handler,而使用 Handler 很容易造成内存泄漏,Android Studio 也会提示我们:This Handler class should be static or leaks might occur (anonymous android.os.Handler) ,如下图所示:

Handler 内存泄漏.png

为了代码的规范以及我们应用的健壮性,我们有必要了解关于 Handler 的内存泄漏,所以这篇文章就用来记录 Handler 内存泄漏的知识。


二、什么是内存泄漏?

内存泄漏,即Memory Leak,指程序中不再使用到的对象因某种原因而无法被GC正常回收,发生内存泄漏会对我们的应用造成如下影响:

  • 导致一些不再使用到的对象没有及时释放,这些对象占据着宝贵的内存空间,很容易导致后续分配内存的时候,内存空间不足而出现OOM(内存溢出)
  • 无用对象占据的空间越多,那么可用的空闲空间也就越少,GC就会更容易被触发,GC进行时会停止其他线程的工作,因此有可能造成卡顿等情况。

三、出现内存泄漏的原因分析

在上面的例子中,Android Studio 提示我们:This Handler class should be static or leaks might occur (anonymous android.os.Handler) 。这里说这个 Handler 类应用使用静态的,否则可能会出现内存泄漏。

注意看,这里用了 might,也就是说这样写不一定会出现内存泄漏,那么什么情况下一定会出现内存泄漏呢,请看下面一个列子:

public class ExampleActivity extends AppCompatActivity {

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // Do Something
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_example);
        new ExampleThread().start();
        finish();
    }

    class ExampleThread extends Thread {
        @Override
        public void run() {
            mHandler.sendEmptyMessageDelayed(0, 10000);
        }
    }
}

这里我们在主线程定义了一个 Handler 对象,然后在子线程中使用 Handler 对象的 sendEmptyMessageDelayed() 方法发送一个延时 10000ms 的消息,接着使用 finish(),这就一定会发生内存泄漏了。

下面我们来分析一下,为什么这种情况一定会发生内存泄漏。首先我们先给出结论:

Handler 对象持有 Activity 的引用,主线程 Looper 又持有 Handler 对象的引用。当 Activity 的生命周期结束时,主线程 Looper 还持有 Handler 对象的引用。所以这个 Activity 无法被垃圾回收,内存泄漏就发生了。

大家可能会疑惑:为什么 Handler 对象会持有 Activity 的引用呢?为什么主线程 Looper 会持有 Handler 对象的引用呢?

1.Handler 为什么会持有 Activity 的引用?

要解答这个问题,我们需要掌握一些关于非静态内部类的储备知识:

  • 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用。
  • 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值。
  • 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。

了解了非静态内部类的知识,那么我们就清楚了:

我们这里的 Handler 为匿名类(非静态内部类),内部类和外部类虽然写在同一个java文件中,但是编译完成后,它们还是会生成各自的 class 文件,内部类通过 this 访问外部类的成员。所以非静态内部类(handler)会隐式调用外部类对象(this,也就是Activity)。

2.主线程 Looper 为什么会持有 Handler 对象的引用?

我们在调用 Handler 对象的 sendMessage() 或者 post() 系列方法去发送消息时(无论 Handler 是在哪个线程创建的),最终都调用了 Handler 的 enqueueMessage() 方法,代码如下:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

其中 this 就是我们创建的 Handler 对象,然后 msg 引用了 Handler 对象并进入了消息队列中,而 Looper 持有 MessageQueue 的引用。所以我们的 Handler 对象就一直被 Looper 引用着,直到 Looper 取出这个 Message。

现在我们就清楚 Handler 发生内存泄漏的原因了:Handler 对象持有 Activity 的引用,主线程 Looper 又持有 Handler 对象的引用。当 Activity 的生命周期结束时,主线程 Looper 还持有 Handler 对象的引用。所以这个 Activity 无法被垃圾回收,内存泄漏就发生了。


四、解决方案

现在我们知道了 Handler 出现内存泄漏的原因其中之一,就是 Handler 这个非静态内部类隐式引用了 Activity,那么针对这点,我们可以想出两个解决方法:

  • 将 Handler 独立出去,单独一个 java 文件
  • 使用 静态内部类+弱引用 来实现 Handler

第一个解决方法比较简单,我们就主要来介绍一下第二个方法。通常我们会使用 静态内部类+弱引用 的方法来解决 Handler 的内存泄漏问题。

示例代码如下:

public class StaticHandlerActivity extends AppCompatActivity {
    private TextView mStateTv; //UI控件
    private MyHandler mHandler = new MyHandler(this); //创建handler对象时传入当前Activity

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_static_handler);

        initView();
    }

    private void initView() {
        mStateTv = findViewById(R.id.tv_state);
    }

    private static class MyHandler extends Handler {
        private final WeakReference mActivity;

        public MyHandler(StaticHandlerActivity activity) {
            mActivity = new WeakReference(activity); //获取弱引用Activity对象
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            if (mActivity.get() != null) {
                //TODO
                //利用弱引用来获取UI控件,不会对回收造成影响
                mActivity.get().mStateTv.setText("state change");
                
                //如果直接new Activity()来获取Activity属于强引用,依然会造成内存泄漏
                //new StaticHandlerActivity().mStateTv.setText("state change"); 
            }
        }
    }
}

如上代码,使用静态内部类,解决了 Handler 持有 Activity 的隐式引用的问题;采用弱引用的方式,来获取 Activity 的引用,以此来更新 UI。

关于 java.lang.ref.WeakReference,下面是 Android 官方给出的介绍

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed. Weak references are most often used to implement canonicalizing mappings.

Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all weak references to that object and all weak references to any other weakly-reachable objects from which that object is reachable through a chain of strong and soft references. At the same time it will declare all of the formerly weakly-reachable objects to be finalizable. At the same time or at some later time it will enqueue those newly-cleared weak references that are registered with reference queues.

弱引用对象,这些对象不会阻止对其引用对象进行终结,终结和回收。弱引用最常用于实现规范化映射。

假设垃圾收集器在某个时间点确定对象是弱可访问的。到那时,它将自动清除对该对象的所有弱引用,以及对所有其他弱可达对象的弱引用,这些对象都可以通过一系列强引用和软引用从该对象到达。同时,它将声明所有以前难以到达的对象都是可终结的。同时或在以后的某个时间,它将排队那些已在参考队列中注册的新清除的弱引用。

通俗的讲,一个对象如果只有被弱引用,那么它就会被回收。所以在使用弱引用的对象时,需要判断当前对象是否已经被回收了。

if (mActivity.get() != null) {
    //TODO       
}

到这里,我们的 Handler 已经不会出现内存泄漏的情况了,不过为了保险起见和代码的逻辑完整性,我们可以在 Activity 的 onDestroy() 中将消息队列中的消息全部移除,代码如下:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

五、总结

让我们来回顾一下关于 Handler 内存泄漏出现的原因:

  • Handler 对象持有 Activity 的引用,主线程 Looper 又持有 Handler 对象的引用。当 Activity 的生命周期结束时,主线程 Looper 还持有 Handler 对象的引用。所以这个 Activity 无法被垃圾回收,内存泄漏就发生了。

我们可以通过以下两种方式来解决:

  • 将 Handler 独立出去,单独一个 java 文件
  • 使用 静态内部类+弱引用 来实现 Handler

同时配合 Activity 的 onDestroy() 方法中使用 Handler.removeCallbacksAndMessages(null) 来移除未处理的消息。

不过还记得我们在文章最开始说过的吗?并不是每个 Handler 都会产生内存泄漏,如果你清楚你的 Activity 在结束时,消息队列中已经没有未处理的消息了,那你也不必加一堆代码来防止内存泄漏了。

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // Do Something
        }
    };

强迫症患者可以直加上 @SuppressLint("HandlerLeak") 不让 Android Studio 提示你那一串英文了。

那么 Handler 内存泄漏分析的文章到这里也结束了,虽然很多时候也不用加上一堆代码来防止内存泄漏,但是出于技术的完整性,以及在使用 Handler 心里有个底,了解其背后的原理还是有必要的。

你可能感兴趣的:(Android Handler 内存泄漏分析&解决方案)