• 弱引用回调引发的坑


    在开发中,常常会用到回调模型,为了避免回调监听未被主动释放,导致内存泄露,我们会用到 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 呢?

    WX20180503-202013@2x.png

    源码中的文档已经告诉我们,当被引用的实例被 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 局部变量没有被回收,运行结果如下:

    WX20180503-205729@2x.png

    接下来模拟存在垃圾回收的情况,我们手动调用 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);
    

    再看运行结果:

    WX20180503-210345@2x.png

    结果正如所想,在脱离函数的局部作用域后,强引用失效,垃圾回收将不存在其它强引用的 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);
    
        }
    
    }
    

    执行结果如下:

    WX20180503-212007@2x.png

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

  • 相关阅读:
    记一次oracle存储过程转mysql百万级数据量 java代码查询 插入修改过程
    003 选择排序(lua)
    定制密钥管理系统的好处 安当加密
    中国“互联网+”大学生创新创业大赛“的实施方案及评审规则
    前端发起请求,后端响应请求的整个过程
    算法-贪心-122. 糖果传递
    springboot基于微信小程序的选课系统毕业设计源码060000
    NK-RTU980 USB bulk传输
    linux安装Sentinal1.8.6
    Keras深度学习实战(36)——基于编码器-解码器的机器翻译模型
  • 原文地址:https://blog.csdn.net/xiaopangcame/article/details/132842936