在开发中,常常会用到回调模型,为了避免回调监听未被主动释放,导致内存泄露,我们会用到 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<OnItemSelectedListener> 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
,如图:
弱引用什么时候才会为 null
呢?
源码中的文档已经告诉我们,当被引用的实例被 GC 回收的时候会返回 null
,而且关于 referent
变量的状态是由虚拟机特殊对待的:
1 2 3 4 5 6 7 8 9 | public abstract class Reference<T> { private T referent; /* Treated specially by GC */ volatile ReferenceQueue<? super T> 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<Callback> 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
局部变量没有被回收,运行结果如下:
接下来模拟存在垃圾回收的情况,我们手动调用 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); |
再看运行结果:
结果正如所想,在脱离函数的局部作用域后,强引用失效,垃圾回收将不存在其它强引用的 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<Callback> 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); } } |
执行结果如下:
总体来说,弱引用其实就是不额外增加强引用的情况下,能够取得类的实例,可以帮助我们避免许多容易引起内存泄露的情况,但在使用的过程中仍需小心。