Handler引起的内存泄漏分析

153 阅读2分钟

1. 提出问题

Handler用于线程间的消息传递,它可以将一个线程中的任务切换到另一个线程执行。使用Handler有两种常规写法。

写法一:

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    };

提示

This Handler class should be static or leaks might occur (anonymous android.os.Handler)

写法二:

    class CustomHandler extends Handler {

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    }

提示

This Handler class should be static or leaks might occur (com.gdu.myapplication.SecondActivity.CustomHandler)

如提示,Handler类应该是静态的,否则可能发生内存泄漏。

2. 问题原因

静态内部类和非静态内部类有什么区别呢?

public class TestInnerClass {

    class InnerClassA {

    }

    static class InnerClassB {

    }
}

编译TestInnerClass.java得到TestInnerClass.class、TestInnerClassInnerClassA.classTestInnerClassInnerClassA.class、TestInnerClassInnerClassB.class三个文件。

TestInnerClass$InnerClassA.class

class TestInnerClass$InnerClassA {
    TestInnerClass$InnerClassA(TestInnerClass var1) {
        this.this$0 = var1;
    }
}

TestInnerClass$InnerClassB.class

class TestInnerClass$InnerClassB {
    TestInnerClass$InnerClassB() {
    }
}

可以看到,非静态内部类默认持有外部类的引用。

在Activity(或其他组件)中定义非静态内部类Handler,则Handler默认持有Activity的引用。Activity销毁时,Handler可能有正在执行或未执行的Message,导致GC无法回收Activity。

3. 解决方案

**方案一:**Activity销毁时,清空Handler中正在执行或未执行的Callback以及Message。

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

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    };

这样可以避免内存泄漏,但无法消除IDE的warning。

**方案二:**Handler内部类改为静态,并传入Activity的弱引用。

    private static class NewHandler extends Handler {

        WeakReference<Activity> activity;

        public NewHandler(Activity activity) {
            this.activity = new WeakReference<>(activity);
        }
        
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    }

弱引用Activity,既可以使用Activity对象中的成员,又不影响Activity被回收。

这里可能产生一个疑问:

静态资源的生命周期不是跟随APP进程的生命周期吗?当内部类没有被使用的时候不会占用内存么?

静态内部类和外部静态属性的生命周期是不同的,静态内部类也要通过new来创建,内存也是在new的时候分配的。静态内部类是一个比较常见的内存泄漏解决方案。

其他

上述场景中的内存泄漏,待Handler中消息处理完,会释放对Activity的引用,下次GC即可回收本次未回收的内存。

4. 知识扩展

4.1 内部类

内部类是指在一个外部类的内部再定义一个类。内部类作为外部类的一个成员,并且依附于外部类而存在。内部类可为静态,可用protected和private修饰(而外部类只能使用public和缺省的包访问权限)。

静态内部类

  • 只是为了降低包的深度,方便类的使用。静态内部类适用于包含类当中,但又不依赖于外部类。
  • Java规定静态内部类不能用外部类的非静态属性和方法,所以只是为了方便管理类结构而定义。在创建内部类的时候,不需要外部类对象的引用。
  • 即使外部类消亡,静态内部类还可以存在。
  • 可以声明static的方法和变量。

非静态内部类

  • 持有一个外部类的引用,可以直接访问外部类的属性、方法。
  • 可以解决多继承问题。
  • 不能脱离外部类实例,一起被垃圾回收器回收。
  • 不能声明static的方法和变量,可以声明static的常量。

static类型的属性和方法,在类加载的时候就会存在于内存中。要想使用某个类的static属性和方法,那么这个类必须要加载到Java虚拟机中。非静态内部类并不随外部类一起加载,只有在实例外部类之后才会加载。

4.2 Java类的生命周期

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存,它的整个生命周期包括如下7个阶段:

image.png

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的链接、类的初始化这三个步骤来对类进行初始化。如果不出现意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者初始化。

image.png

装载

Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象(所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用)。

验证

确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性。

准备

为类变量分配内存。

解析

将常量池中的符号引号转换为直接引用的过程(简言之,将类、接口、字段和方法的符号引用转为直接引用)。

初始化

类变量初始化。

使用

类实例。

卸载

要判定一个类型是否属于“不再被使用的类”需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景。如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

image.png

4.3 什么是内存泄漏

内存泄漏是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。

内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。

4.4 强引用、弱引用、软引用、虚引用

Java执行 GC(垃圾回收)判断对象是否存活有两种方式,分别是引用计数法和引用链法(可达性分析法)。JDK 1.2版本开始,对象的引用被划分为4种级别,使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

强引用

在一个线程内,无须引用直接可以使用的对象,强引用不会被JVM清理。我们平时申明变量使用的就是强引用,普通系统99%以上都是强引用,比如,String s="Hello World"。

软引用

软引用是除了强引用外最强的引用类型,我们可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,它不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阈值时,才会去回收软引用的对象。只要有足够的内存,软引用便可能在内存中存活相当长一段时间。通过软引用,垃圾回收器就可以在内存不足时释放软引用可达的对象所占的内存空间,以保证程序正常工作。

SoftReference<String> softR = new SoftReference<>("soft reference");

弱引用

Java中使用WeakReference来表示弱引用。如果某个对象与弱引用关联,那么当JVM在进行垃圾回收时,无论内存是否充足,都会回收此类对象。

WeakReference<String> weakR = new WeakReference<>("weak reference");

虚引用

虚引用的主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。虚引用主要用来跟踪对象被垃圾回收的活动,必须和引用队列联合使用。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<String> phantomR = new PhantomReference<>("phantom reference", queue);

调用phantomR.get(),返回null。即虚引用不能访问关联的对象。

4.5 Handler机制

Handler机制是一套Android消息传递机制/异步通信机制。主要应用场景:将工作线程中需更新UI的操作信息传递到UI主线程,从而实现工作线程对UI的更新处理,最终实现异步消息的处理。

工作流程图如下:

在这里插入图片描述

Handler:负责消息的发送和处理。发送给MessageQueue,接收Looper返回的消息,并处理消息。

Looper:负责管理MessageQueue。Looper会不断地从MessageQueue取出消息,交给Handler处理。

MessageQueue:消息队列(实际上用链表实现),负责存放Handler发送过来的消息。

注意

  1. 一个线程只能创建一个Looper对象。
  2. Looper对象只能通过Looper.prepare()方法创建,并通过Looper.myLooper()方法获取对象。
  3. Looper.prepare()方法只能调用一次。
  4. Looper.loop()方法开启一个死循环,不断地从MessageQueue中取出消息并交给Handler处理,除非调用Looper的quit()或者quitSafely()方法结束消息轮询,queue.next()才会返回null结束循环。
  5. 一个Looper可以绑定多个Handler,一个Handler只能绑定一个Looper。
  6. Handler处理消息有三个优先级:
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }