子线程耗时操作导致内存泄漏分析

编码经常遇到这么一种情况:

public class Manager {

    private Context context;

    public Manager(Context context){

        this.context = context;
    }

    //模拟子线程耗时操作
    public void net(){

        new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //TODO use context
            }
        }).start();
    }
}

使用时:

Manager manager = new Manager(this);
manager.net();

Manager持有context,会导致内存泄漏吗?

简单来讲,GC会根据引用树进行垃圾回收,当对象没有被引用时,就会被GC选中。

下面一步步开始分析:

1.假如代码是如下:

public class Manager {

    private Context context;

    public Manager(Context context){

        this.context = context;
    }

    //模拟主线程非耗时操作
    public void mainDoSomething(){
        //TODO
    }
}

引用关系如下:


子线程耗时操作导致内存泄漏分析_第1张图片
589086DC-5238-4528-9B28-FACC205451ED.png

箭头指向表示被引用的对象

Activity与Manager存在循环引用,这种情况,Activity销毁时,引用树指向Activity的箭头断开,故会被GC回收,不存在内存泄漏。

2.如果是文中开头的代码呢?

它的引用关系如下:

子线程耗时操作导致内存泄漏分析_第2张图片
0C72F414-A629-4907-962E-40244EF5DB3A.png

要理解这个图,首先要知道:

Java中,非静态内部类、非静态匿名内部类持有对外部类的引用。 所以代码中Thread持有Manager的引用。
Java中,所有运行线程不会被回收。Activity销毁时,由于Thread不会被回收,所以它持有的Manager不会被回收,同时也就导致Manager持有的context不会被回收,导致Activity内存泄漏。

从图上看出,指向Activity的箭头有俩个,当Activity销毁时,Activity仍然被Manager引用,故会内存泄漏。

3.其实文中开头的代码,简化后就是我们常见的版本:

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


        //模拟子线程耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

它的引用关系如下:

子线程耗时操作导致内存泄漏分析_第3张图片
F453C191-FA94-459B-AE09-6E847D247206.png

可见匿名内部类线程会导致内存泄漏。

4.匿名内部类线程导致内存泄漏如何避免呢?

可以使用弱引用解决这个问题:

    //创建静态类以避免对外部类引用!
    public static class MyThread extends Thread {

        private WeakReference reference;

        public MyThread(Activity activity) {

            reference = new WeakReference<>(activity);
        }

        @Override
        public void run() {
            super.run();

            Activity activity = reference.get();
            
            if (activity != null) {
                //TODO
            }
        }
    }
    
    //调用
    MyThread myThread = new MyThread(this);
    myThread.start();

5.实际代码编写中,下面情况非常常见。

比如使用Volley进行网络访问,访问成功后更改UI,代码如下:

    public void net(){

        String request = new StringRequest("xxx", new Response.Listener() {
            @Override
            public void onResponse(String response) {

                tv.setText(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                
            }
        });

        //访问网络...
    }

new Response.Listener是匿名内部类,持有context的引用,这种代码存在内存泄漏的可能,那么是否都要改成上述写法呢?

答案是视情况而定,原因如下:

<1.Volley等框架提供了cancel方法,可以在onDestroy()里执行request.cancel()避免内存泄漏。
详情可以参考https://blog.csdn.net/aq15756005983/article/details/70230106。
Volley cancel()机制在旧版存在内存泄漏问题,好在新版已经解决。

<2.之所以会内存泄漏,是因为Thread运行时间过长,大大超出Activity存活时间所致。但是当Thread运行完毕,Activity就会被正常回收。从上述代码可以看出,增加弱引用等方式会增加代码复杂度,所以这种应对内存泄漏的写法,多用于频繁打开、且网络超时较长的页面,因为这类页面会因内存泄漏而短时间内大量占用内存。

当然,健壮的代码,应当尽可能的避免内存泄漏的发生。

以上仅个人看法。

你可能感兴趣的