Android的Context泄露分析和解决方案

前言

在Android中可能会有多种状况的内存泄露,其中比较常见的就是Context泄露,即上下文泄露。这个问题不容忽视,由于Activity、Service、Application都是Context的子类,因此一个Context对象有时会比较庞大,因此若是Context对象没法释放那么很容易形成OOM。java

咱们都知道,形成内存泄露的根本缘由是某个已经无用的对象还被其余对象引用着,因此GC没法释放。Context泄露也是这样,那么在Android中有什么样的场景会致使Context泄露呢?经笔者总结,有如下两个场景(也许还有其余的,笔者目前暂时没有碰到,欢迎大神指正):android

1. Java语言的机制形成的泄露。ide

2. Android生命周期形成的泄露。工具

有时候两者没有明显的区分,甚至有时候是两种缘由一块儿形成了Context泄露。本文咱们就从上面两点进行阐述。性能

检测内存泄露的工具

由于本文要讨论内存泄露,因此咱们须要能够检测内存泄露的工具,笔者总结了三种方式:优化

1. Dump Java Heap生成hprof文件,用DDMS或者MAT工具分析。this

2. StrictMode严格模式:编码

3. LeakCanary。spa

第一种较为繁琐,不适用于今天的场景。严格模式和LeakCanary见效比较快,因此本文就用第二种和第三种来检测内存泄露。.net

严格模式的相关文章能够参考《Android性能调优利器StrictMode》

LeakCanary网上的教程不少,这里就不一一列举了。

说点题外话,严格模式和LeakCanary检测Activity泄露,两者的检查时机是同样的,都是在某个Activity onDestroy时检测,但两者的判断方式是不同的。

严格模式会检测应用中某一个Activity是否存在多个实例,默认是一个,若是存在多个就会给出提示。因此若是一个Activity存在内存泄露,第一次是检测不出来的,由于第一次即便内存泄露也仍是有一个实例,除非再次进到这个Activity而后退出,那么严格模式才会给出提示。

LeakCanary则在Activity销毁时检查该Activity是否还存在,若是存在就发起一次GC再检测,若是没法销毁就打印内存快照而后分析给出Notification提示。

Java语言的机制形成的泄露

这标题有点唬人,这里不是说Java语言有bug。。。请听我细细道来。让咱们先来看一段代码

package com.example;

public class MyClass {


    public static void main(String[] args) throws Throwable {

    }

    public class A {
        public void methed1(){

        }
    }

    public static class B {
        public void methed1(){

        }
    }
}

新建一个类MyClass,其中有两个内部类,非静态内部类A和静态内部类B。若是咱们把上面代码编译再反编译,能够看到非静态内部类A自动生成的构造方法里,默认的参数是外部类,所以使用内部类的时候会保存一个外部类的引用。而静态内部类B呢,没有生产默认的构造方法。这里只是一个结论,具体探究的过程能够参考《Java内部类的实现原理与可能的内存泄漏》。

再重述一遍结论,非静态内部类会持有外部类的引用,而静态内部类则不会持有外部类的应用。再加上一点,匿名内部类也会持有外部类的引用。

举个Android的例子,看下面的代码,

public class LeakActivity extends AppCompatActivity {

    private static final String TAG = "LeakActivity";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        MyThread myThread = new MyThread();
        myThread.start();
    }

    class MyThread extends Thread {

        @Override
        public void run() {
            while (true) {
                Log.e(TAG, "run: ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在LeakActivity中声明一个非静态内部类MyThread每一个一秒打印log。由于这是一个非静态内部类,持有了一个LeakActivity的引用。当咱们从这个Activity退出时,从理论上说,LeakActivity内存泄露了。让咱们看看检测工具,LeakCanary会给出提示

再次进入LeakActivity而后退出,StrictMode会给出提示(至于为何要再次进入而后退出,请参考上文)

E/StrictMode: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 at 
android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

从以上能够看出,MyThread这个类确实引用了LeakActivity致使了LeakActivity的内存泄露。

其实非静态内部类会致使内存泄露,Google已经老早给出了提醒,若是咱们在Activity中建立一个非静态内部类继承Handler,Android Studio会给出这样的提示

发现问题就要解决问题,那咱们该如何应对这种状况的内存泄露呢。回头再看看Google的那段话,咱们能够找到解决方案:

1. 将非静态内部类声明为静态内部类;

2. 若是在静态内部类中须要引用外部类,咱们须要用WeakReference进行引用。由于Android系统对WeakReference的回收是至关积极的,因此在使用前必定要记得判空!

如今让咱们就修改一下LeakActivity的代码来消灭内存泄露的问题吧

public class LeakActivity extends AppCompatActivity {

    private static final String TAG = "LeakActivity";

    // 建立一个非静态变量, 让静态内部类访问
    private String mStr = "LeakActivity还没释放...";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        MyThread myThread = new MyThread(this);
        myThread.start();
    }

    static class MyThread extends Thread {

        // 使用WeakReference引用LeakActivity
        private WeakReference<LeakActivity> mWeakRef;

        public MyThread(LeakActivity leakActivity) {
            this.mWeakRef = new WeakReference<>(leakActivity);
        }

        @Override
        public void run() {
            while (true) {
                // WeakReference必定要记得判空
                if (mWeakRef.get() != null) {
                    Log.e(TAG, "run: " + mWeakRef.get().mStr);
                } else {
                    Log.e(TAG, "run: LeakActivity已经释放掉了...");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这里建立了一个非静态全局变量mStr来模拟让静态内部类MyThread访问。在MyThread的构造方法中加入参数传入LeakActivity对象,内部使用WeakReference持有这个引用。当退出这个页面后,若是系统发生GC,这个WeakReference就会马上释放掉,因此必定要记得判空。

有童鞋也许要问,若是发生GC后,那mStr变量不就访问不了了么,逻辑就不正确了啊。这里有必要说一下,既然mStr定义为LeakActivity的非静态全局变量,那就默认这个变量的生命周期应该和Activity一致,在这个Activity销毁以后,mStr变量从代码设计的角度看就是不容许再被访问的。若是出于某些业务上的需求,那就声明为LeakActivity的静态全局变量或是MyThread的内部变量。

跑一下上面的代码,数次进入退出LeakActivity,LeakCanary和StrictMode都没有再给出内存泄露的提示了,这个小bug就这么愉快地解决啦~ ^_^

Android生命周期形成的泄露

跟上一节同样,一样是个很唬人的标题,就是这么标题党,哈哈。这里不是说Android自身的生命周期会形成内存泄露,而是说某个持有了Context的对象的生命周期和Context的生命周期不一样步致使了Context对象没法释放,从而形成了内存泄露。

这句话有点拗口,举个例子就很容易理解了。下面的LeakObject是一个单例,构造是须要传入一个Context对象。

public class LeakObject {

    private static final String TAG = "LeakObject";

    private static LeakObject instance = null;

    private Context context;

    private LeakObject(Context context) {
        this.context = context;
    }

    public static LeakObject getInstance(Context context) {
        if (instance == null) {
            synchronized (LeakObject.class) {
                if (instance == null) {
                    instance = new LeakObject(context);
                }
            }
        }
        return instance;
    }

    public void sayHi() {
        Log.e(TAG, "sayHi: ");
    }
}

咱们在LeakAcitivity中获取一个LeakObject的单例对象并调用sayHi方法。

public class LeakActivity extends AppCompatActivity {

    private static final String TAG = "LeakActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        LeakObject object = LeakObject.getInstance(this);
        object.sayHi();
    }
}

这是很常见的单例模式的应用,咱们也常常这么写,这也会出现内存泄露的问题么?咱们运行一遍,来回进入几回LeakActivity,能够看到StrictMode给出了下面的提示

E/StrictMode: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 at 
android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

这是为何呢?由于咱们在申请单例的时候,传入的是LeakActivity的Context,而咱们知道LeakObject中的单例静态变量instance是常驻内存中的,它的生命周期跟应用同样长,只要应用没关闭,它就一直在。它持有了一个LeakActivity的引用,这样致使LeakActivity的生命周期跟应用同样长,没法被释放,形成呢内存泄露。

那么该如何解决这个问题呢,咱们只要把在getInstance()中传入getApplicationContext()就能够了,这样就确保了单例的持有的是Application的Context。由于ApplicationContext自己就是跟应用的生命周期同样长的,这样就不存在内存泄露了。完美~

经过上面的例子能够看出,若是某个持有Context的对象的生命周期跟Context的生命周期不一致,就会致使Context的内存泄露。不光是例子中说的单例,咱们经常使用的AsyncTask、Handler等都会存在这个问题。

好比AsyncTask持有了Activity的Context,在Activity退出时AsyncTask的任务还没作完,Activity就没法被释放。因此咱们须要在onDestroy时手动cancel AsyncTask,确保AsyncTask的生命周期跟Activity同步。

一样,指向Activity等Context资源的静态变量也会致使内存泄露,原理都同样,就再也不赘述了。

总结

下面作一个总结,本文从两个角度介绍了Context上下文泄露的缘由。

1. Java语言的角度:非静态内部类会持有外部类的引用,可能会致使内存泄露。解决方法是使用静态内部类,同时用WeakReference持有Context引用。一样匿名内部类也会持有外部类的引用,尽可能不要使用匿名内部类。

2. Android生命周期的角度:若是某个持有Context的对象的生命周期跟Context的生命周期不一致,就会致使Context的内存泄露。单例、指向Activity等Context资源的静态变量、AsyncTask等都有可能由于这个缘由致使内存泄露。确保持有Context引用的对象跟Context的生命周期保持一致。可使用getApplicationContext或者手动控制。

两者没有严格的区分,有时是某一个的缘由形成了泄露,有时是两者结合形成了泄露。咱们须要在编码过程当中时刻长个心眼,有条件时也能够利用工具来进行分析。

参考资料

相关文章
相关标签/搜索