Android 中内存泄漏的原因和解决方案

前言

之前研究过一段时间关于 Android 内存泄漏的知识,大致了解了导致内存泄漏的一些原因,但是没有深入去探究,很多细节也理解的不够透彻,基本上处于一种似懂非懂的状态,最近又研究了一波,发现有很多新的收获,遂在此记录一些心得体会。

Android 中内存泄漏的原因和解决方案_第1张图片

首先引用一下开源项目 LearningNotes 中关于 Java 内存分配策略和 Java 是如何管理内存的说明。

Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈与堆的区别

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();
    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}
Sample mSample3 = new Sample();

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。 mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。 以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。


Android 中内存泄漏的原因和解决方案_第2张图片

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

什么是Java中的内存泄漏

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

(以上内容引用自开源项目 LearningNotes)

Android 中内存泄漏的原因

在 Android 中内存泄漏的原因其实和在 Java 中是一样的,即某个对象已经不需要再用了,但是它却没有被系统所回收,一直在内存中占用着空间,而导致它无法被回收的原因大多是由于它被一个生命周期更长的对象所引用。其实要分析 Android 中的内存泄漏的原因非常简单,只要理解一句话,那就是生命周期较长的对象持有生命周期较短的对象的引用。

举个例子,如果一个 Activity 被一个单例对象所引用,那么当退出这个 Activity 时,由于单例的对象依然存在(单例对象的生命周期跟整个 App 的生命周期一致),而单例对象又持有 Activity 的引用,这就导致了此 Activity 无法被回收,从而造成内存泄漏。

知道了内存泄漏的根本原因,再分析为什么会出现内存泄漏就很简单了,下面就针对一些常见的内存泄漏进行分析。

单例造成的内存泄漏

刚才已经分析过了,假设有一个单例是这样的

public class SingleTon {
    
    private static SingleTon singleTon;

    private Context context;

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

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

}

这是单例模式饿汉式的双重校验锁的写法,这里的 singleTon 持有 Context 对象,如果 Activity 中调用 getInstance 方法并传入 this 时,singleTon 就持有了此 Activity 的引用,当退出 Activity 时,Activity 就无法回收,造成内存泄漏,所以应该修改它的构造方法

private SingleTon(Context context) {
    this.context = context.getApplicationContext();
}

通过 getApplicationContext 来获取 Application 的 Context,让它被单例持有,这样退出 Activity 时,Activity 对象就能正常被回收了,而 Application 的 Context 的生命周期和单例的生命周期是一致的,所有再整个 App 运行过程中都不会造成内存泄漏。

非静态内部类造成的内存泄漏

我们知道,非静态内部类会持有外部类的引用,如果这个非静态的内部类的生命周期比它的外部类的生命周期长,那么当销毁外部类的时候,它无法被回收,就会造成内存泄漏。

外部类中持有非静态内部类的静态对象

假设 Activity 的代码是这样的

public class MainActivity extends AppCompatActivity {
    
    private static Test test;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (test == null) {
            test = new Test();
        }

    }
    
    private class Test {

    }
    
}

这个其实和单例的原理是一样的,由于静态对象 test 的生命周期和整个应用的生命周期一致,而非静态内部类 Test 持有外部类 MainActivity 的引用,导致 MainActivity 退出的时候不能被回收,从而造成内存泄漏,解决的方法也很简单,把 test 改成非静态,这样 test 的生命周期和 MainActivity 是一样的了,就避免了内存泄漏。或者也可以把 Test 改成静态内部类,让 test 不持有 MainActivity 的引用,不过一般没有这种操作。

Handler 或 Runnable 作为非静态内部类

handler 和 runnable 都有定时器的功能,当它们作为非静态内部类的时候,同样会持有外部类的引用,如果它们的内部有延迟操作,在延迟操作还没有发生的时候,销毁了外部类,那么外部类对象无法回收,从而造成内存泄漏,假设 Activity 的代码如下

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                
            }
        }, 10 * 1000);
    }
}

上面的代码中,Handler 和 Runnable 作为匿名内部类,都会持有 MainActivity 的引用,而它们内部有一个 10 秒钟的定时器,如果在打开 MainActivity 的 10 秒内关闭了 MainActivity,那么由于 Handler 和 Runnable 的生命周期比 MainActivity 长,会导致 MainActivity 无法被回收,从而造成内存泄漏。

那么应该如何避免内存泄漏呢?这里的一般套路就是把 Handler 和 Runnable 定义为静态内部类,这样它们就不再持有 MainActivity 的引用了,从而避免了内存泄漏

public class MainActivity extends AppCompatActivity {

    private Handler handler;

    private Runnable runnable;

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

        handler = new TestHandler();
        runnable = new TestRunnable();
        handler.postDelayed(runnable, 10 * 1000);
    }

    private static class TestHandler extends Handler {

    }

    private static class TestRunnable implements Runnable {
        @Override
        public void run() {
            Log.d(TAG, "run: ");
        }
    }
    
    private static final String TAG = "MainActivity";
}

最好再在 onDestory 调用 handler 的 removeCallbacks 方法来移除 Message,这样不但能避免内存泄漏,而且在退出 Activity 时取消了定时器,保证 10 秒以后也不会执行 run 方法

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacks(runnable);
}

还有一种特殊情况,如果 Handler 或者 Runnable 中持有 Context 对象,那么即使使用静态内部类,还是会发生内存泄漏

public class MainActivity extends AppCompatActivity {

    private Handler handler;

    private Runnable runnable;

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

        handler = new TestHandler(this);
        runnable = new TestRunnable();
        handler.postDelayed(runnable, 10 * 1000);
    }

    private static class TestHandler extends Handler {
        private Context context;
        private TestHandler(Context context) {
            this.context = context;
        }
    }

    private static class TestRunnable implements Runnable {
        @Override
        public void run() {
            Log.d(TAG, "run: ");
        }
    }

    private static final String TAG = "MainActivity";
}

上面的代码,使用 leakcanary 工具会发现依然会发生内存泄漏,而且造成内存泄漏的原因和之前用非静态内部类是一样的,那么为什么会出现这种情况呢?

这是由于在 Handler 中持有 Context 对象,而这个 Context 对象是通过 TestHandler 的构造方法传入的,它是一个 MainActivity 对象,也就是说,虽然 TestHandler 作为静态内部类不会持有外部类 MainActivity 的引用,但是我们在调用它的构造方法时,自己传入了 MainActivity 的对象,从而 handler 对象持有了 MainActivity 的引用,handler 的生命周期比 MainActivity 的生命周期长,因此会造成内存泄漏,这种情况可以使用弱引用的方式来引用 Context 来避免内存泄漏,代码如下

public class MainActivity extends AppCompatActivity {

    private Handler handler;

    private Runnable runnable;

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

        handler = new TestHandler(new WeakReference(this));
        runnable = new TestRunnable();
        handler.postDelayed(runnable, 10 * 1000);
    }

    private static class TestHandler extends Handler {
        private Context context;
        private TestHandler(WeakReference weakContext) {
            context = weakContext.get();
        }
    }

    private static class TestRunnable implements Runnable {
        @Override
        public void run() {
            Log.d(TAG, "run: ");
        }
    }

    private static final String TAG = "MainActivity";
}

其他的内存泄漏情况

还有一些其他的会导致内存泄漏的情况,比如 BraodcastReceiver 未取消注册,InputStream 未关闭等,这类内存泄漏非常简单,只要在平时写代码时多多注意即可避免。

总结

通过前面的分析我们可以发现,造成 Android 内存泄漏的最根本原因就是生命周期较长的对象持有生命周期较短的对象的引用,只要理解了这一点,内存泄漏问题就可迎刃而解了。

应用

在 MVP 的架构中,通常 Presenter 要同时持有 View 和 Model 的引用,如果在 Activity 退出的时候,Presenter 正在进行一个耗时操作,那么 Presenter 的生命周期会比 Activity 长,导致 Activity 无法回收,造成内存泄漏

public class MainActivity extends AppCompatActivity implements TestView {

    private Presenter presenter;

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

        presenter = new Presenter(this);

        presenter.request();
    }

}
public class Presenter {

    private TestView view;
    private Model model;

    public Presenter(TestView view) {
        this.view = view;
        model = new Model();
    }

    public void request() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG, "run: ");
            }
        }).start();
    }

    private static final String TAG = "Presenter";
}

上面的代码中,假设 request 是一个需要耗时 10 秒的操作,那么在 10 秒之内如果退出 Activity 就会内存泄漏。

解决方法也很简单,在 onDestory 方法中把 presenter 中的 view 对象置为空就可以了

@Override
protected void onDestroy() {
    super.onDestroy();
    presenter.detachView();
}
public void detachView() {
    view = null;
}

也就是在退出 Activity 的时候,让 Presenter 不再持有 Activity 的引用,避免了内存泄漏。

你可能感兴趣的:(Android 中内存泄漏的原因和解决方案)