弱引用回调引发的坑

在开发中,常常会用到回调模型,为了避免回调监听未被主动释放,导致内存泄露,我们会用到 WeakReference 来存放回调引用,然而要注意的是回调类被回收的坑。本文记录笔者开发中遇到弱引用回调被回收的坑及思考。

奇怪的现象

平常的一天,像往常一样敲着项目代码,今天要完成的需求是为我们的自定义 View 添加一个回调,当用户操作自定义 View 时,会回调指定的监听器。

很容易的一个需求,常规写法很快写出来了:

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
31
32
33
public class MyView extends LinearLayout {

    // 回调引用
    private OnItemSelectedListener mListener;
    
    // 回调接口定义
    public interface OnItemSelectedListener {
        void onSelect(String text);
    }

    // 设置回调
    public void setListener(OnItemSelectedListener listener) {
        mListener = listener;
    }
    
    // 释放回调
    public void dispose() {
        mListener = null;
    }
    
    ...
    
    public void something() {
        ...
        // 回调
        if (mListener != null) {
            mListener.onSelect(text);
        }
    }

    ...
 
}

这时候发现,调用方设置回调后,可能并不会主动调用 dispose() 方法对监听进行释放,所以我们简单优化一下:

使用弱引用代替强引用,这样当调用方的 Listener 被回收时,弱引用会自动被释放掉,不会造成内存泄露:

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
31
32
public class MyView extends LinearLayout {

    // 回调弱引用
    private WeakReference mListener;
    
    // 回调接口定义
    public interface OnItemSelectedListener {
        void onSelect(String text);
    }

    // 设置回调
    public void setListener(OnItemSelectedListener listener) {
        mListener = new WeakReference<>(listener);
    }
    
    ...
    
    public void something() {
        ...
        // 回调
        if (mListener != null) {
            // 弱应用取出实例
            OnItemSelectedListener listener = mListener.get();
            if (listener != null) {
                listener.onSelect(text);
            }
        }
    }

    ...
 
}

这样确实没什么问题,WeakReference 并不会强持有引用。

1
2
3
4
5
6
7
8
9
public void initView() {
    ...
    myView.setListener(new MyView.OnItemSelectedListener() {
        @Override
        public void onSelect(String text) {
            ...
        }
    });
}

然而,当这样使用时,发现一个奇怪的现象:某些时候回调的 onSelect() 方法不会被回调,或者是仅仅在初期能够回调,过一会儿就不被回调了。

大胆猜测

没错,很神奇的现象,接下来我们使用调试工具进行一步步调试,发现更神奇,listener 竟然为 null,如图:

WX20180503-201328@2x.png

弱引用什么时候才会为 null 呢?

弱引用回调引发的坑_第1张图片

源码中的文档已经告诉我们,当被引用的实例被 GC 回收的时候会返回 null,而且关于 referent 变量的状态是由虚拟机特殊对待的:

1
2
3
4
5
6
7
8
9
public abstract class Reference {

    private T referent;         /* Treated specially by GC */

    volatile ReferenceQueue queue;
    
    ...
        
}

那么,可以猜想到为什么会出现这样的情况,就是:我们的匿名内部类被 GC 回收掉了。

具体而言,对于 new 出来的 OnItemSelectedListener 实例只有 MyView 中有一个弱引用对其引用,而不存在任何一个强引用对其引用,这样当 GC 到来时,就会将其标记为即将被回收的对象,并排队执行 finalize() 方法,然后很快在下一次 GC 到来时将其回收。

这样一来,也就解释了为什么刚开始能正常工作,之后 listener 一直为 null 了。

实验证实

刚刚只是进行一个猜测,下面来做一个实验验证一下我们的想法。

(1)声明一个回调接口

1
2
3
public interface Callback {
    void call();
}

(2)我们的测试类

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
public class InnerClassGc {

    public WeakReference reference;

    public void fun() {

        // 匿名内部类
        Callback callback = new Callback() {

            @Override
            public void call() {
                // do something
            }

            @Override
            protected void finalize() throws Throwable {
                super.finalize();
                // 监控被垃圾回收
                System.out.println("base finalize()");
            }
        };

        reference = new WeakReference<>(callback);

    }

}

这里的回调 Callback 中,重写了 finalize() 方法,该方法将在实例被垃圾回收时调用,这里能方便我们看实例是否被回收

(3)测试

首先测试首次正常的情况:

1
2
3
4
5
6
7
// 实例化
InnerClassGc innerClassGc = new InnerClassGc();
// 调用
innerClassGc.fun();
// 检查是否被回收
Callback callback = innerClassGc.reference.get();
System.out.println(callback);

此时,即便是弱引用,但没有发生垃圾回收情况,所以 callback 局部变量没有被回收,运行结果如下:

弱引用回调引发的坑_第2张图片

接下来模拟存在垃圾回收的情况,我们手动调用 System.gc() 来触发诱导 JVM 进行垃圾回收:

1
2
3
4
5
6
7
8
9
// 实例化
InnerClassGc innerClassGc = new InnerClassGc();
// 调用
innerClassGc.fun();
// ***** 触发gc *****
System.gc();
// 检查是否被回收
Callback callback = innerClassGc.reference.get();
System.out.println(callback);

再看运行结果:

弱引用回调引发的坑_第3张图片

结果正如所想,在脱离函数的局部作用域后,强引用失效,垃圾回收将不存在其它强引用的 callback 实例回收了,导致弱引用 get() 为 null

进一步思考

到了这里,可能读者已经明白如何解决这个问题了,在函数内部的变量会被垃圾回收,如果将它移到类成员变量级别,类成员变量级的强引用在类销毁的时候才会失效。在这之前的整个过程,由于强引用的存在,实例不会被回收,弱应用 WeakReference 也将一直有数据,故最容易的解决方案就是指定一个类成员变量强引用它:

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
public class InnerClassGc {

    private WeakReference reference;

    private Callback callback;

    private void fun() {

        // 类成员变量赋值
        callback = new Callback() {

            @Override
            public void call() {
                // do something
            }

            @Override
            protected void finalize() throws Throwable {
                super.finalize();
                System.out.println("base finalize()");
            }
        };

        reference = new WeakReference<>(callback);

    }

}

执行结果如下:

弱引用回调引发的坑_第4张图片

总体来说,弱引用其实就是不额外增加强引用的情况下,能够取得类的实例,可以帮助我们避免许多容易引起内存泄露的情况,但在使用的过程中仍需小心。

你可能感兴趣的:(java,java)