引起内存泄漏的原因有很多种,归结到最后都是由于GC不能回收对应的对象导致。

今天主要说明单例模式一种情况下引起的内存泄漏,这类问题引起的内存泄漏,也可以归纳为:长生命周期对象持有短生命周期对象,导致短生命周期对象不能回收


首先介绍一下内存泄漏和内存溢出的区别,内存泄漏和内存溢出是两码事,内存溢出是由于应用所消耗的内存或者应用申请的内存超出了虚拟机分配的内存,也就是内存不够用了。内存泄漏是某个不再使用对象由于被其他实例引用,导致不能被GC回收,而导致的内存不能释放。内存泄漏可能会引起内存溢出,因为如果内存泄漏严重,导致存在大量GC不能 回收的对象占用内存,内存占用会越来越高,导致其他对象不能被分配到内存,从而导致内存溢出。

今天讲解的是单例模式引起的内存泄漏。在项目开发过程当中,我们为了保证某个类的对象唯一性,可能会使用单例模式,单例模式可以保证在该进程下,该类的最多只存在一个实例对象,也可能一个也不存在(没调用)。

首先看一下我们测试代码的结构:

一个主界面MainActivity里面有一个按钮,点击进入SecondActivity,SecondActivity中有一个按钮点击进入LastActivity。

首先看一下单例模式代码

package com.example.wei.memory;


import android.content.Context;
import android.util.Log;

public class SingleInstance {
    private Context mContext;
    private static SingleInstance instance;

    private SingleInstance(Context context) {
        this.mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstance(context);
        }
        return instance;
    }

    public void say() {
        Log.i("tag", "this is single instance");
        Log.i("tag", ":code:" + instance.hashCode());
    }
}

上面是一个很简单的单例模式,

单例模式要求:构造函数私有化,提供公共的可访问的获取该对象实例的方法。

我们不去讨论上面的单例模式是懒汉式还是饿汉式,也不去讨论在多线程下该写法有问题,我们讨论的重点不在单例模式的写法上,而是单例模式引起的内存泄漏,关注重点,忽略其他。

下面我们看 一下MainActivity

package com.example.wei.memory;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private Button mBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtn = (Button) findViewById(R.id.btnStart);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });
    }
}

很简单,点击按钮之后进入SecondActivity。看一下SecondActivity代码

package com.example.wei.memory;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class SecondActivity extends AppCompatActivity {
    private Button mBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mBtn = (Button) findViewById(R.id.btn);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SingleInstance instance = SingleInstance.getInstance(SecondActivity.this);
                instance.say();

                Intent intent = new Intent(SecondActivity.this, LastActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    public void onBackPressed() {
        finish();
    }

}

代码也很难简单,点击之后,调用单例模式对象方法,启动LastActivity。看一下LastActivity代码

package com.example.wei.memory;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

public class LastActivity extends AppCompatActivity {

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

    }

    @Override
    public void onBackPressed() {
        finish();
    }
}
只是一个简单的界面,点击返回之后,关闭界面
这种单例模式的用法可能很多开发人员都使用过。现在我们看一下上述代码存在的问题。
首先上述单例模式当中,需要使用Context变量,我们在SecondActivity当中将this传递进去,也就是将当前的Activity实例传递进去。看上去没有什么问题。我们分析一下:
首先单例模式当中SingleInstance对应的实例是静态的,静态变量被创建之后,是被所有的该类对象公用的,静态变量的生命周期是跟随类的生命周期的,也就是只有该类被jvm从内存中干掉后,这个对应的实例的生命周期也就结束了,占用的内存也就被回收了。
我们在Activity中创建了一个SingleInstance,并且将Activity的实例this传递给了该类的对象,导致该单例对象持有了对应的Activity的引用。当我们Activity退出后,由于SingleInstance还存在,它的生命周期并没有结束,所以SingleInstance依然持有对Activity实例的引用,由于Activity有被引用,导致Activity的实例不能被回收,Activity会长时间的存在内存中,尽管这个界面已经被关闭。如果该Activity中存在很多大的对象,例如Bitmap或者其他占用内存较多的对象,由于Activity没有被回收,导致这些大的对象也不能被回收,从而导致被占用的内存一直不能被回收,从而导致了内存泄漏。
我们看一下操作过程中对象实例在内存中的存在情况:
应用第一次启动进入主界面不做其他操作,内存中对象结构如下:


内存中存在我们的主Activity 1个,还有一个内部类对象,因为Button设置点击我们使用的是匿名内部类的形式,所以会有一个内部类对象,由于我们MainActivity会startActivity SecondActivity,所以可以看到SecondActivity,由于没启动所以对象个数为0
点击按钮启动SecondActivity,再看内存中的对象个数


此时除了MainActivity对象以及内部的一个内部类对象,多了SecondActivity,SecondActivity中有按钮点击,所以会有一个匿名内部对象,由于SecondActivity中引用了SingleInstance和LastActivity。所以可以看到对应的类名,个数为0

点击SecondActivity中的按钮,调用单例模式对象的方法,并启动LastActivity,查看内存中的对象


此时LastActivity 和SingleInstance对象个数都为1
点击返回到主界面,也就是MainActivity,手动触发GC,查看内存中的对象。


LastActivity 界面被关闭,所以被回收,对象个数为0,SingleInstance由于是static所以没有被回收,所以对象个数为1,可以理解,但是SecondActivity,界面已经被关闭,但是对象个数为1,没有被回收,理论上Activity都已经被关闭了,应该被回收才对,该对象没有被回收,肯定是有对象引用他,我们查看一下SecondActivity的引用情况


可以看到SingleInstance中mContext引用了SecondActivity。
所以上述的单例模式导致SecondActivity不能被回收,出现了内存泄漏的情况。

如何修复:

上述代码产生内存泄漏的主要原因是:长生命在后期的对象(static的SingleInstance实例)引用了短生命周期对象(SecondActivity实例)导致,那么我们只要解决SecondActivity实例引用的问题即可。SingleInstance需要一个Context,那么我们可以将应用的ApplicationContext赋值给SingleInstance  的Context
将单例模式进行修改如下:

package com.example.wei.memory;


import android.content.Context;
import android.util.Log;

public class SingleInstance {
    private Context mContext;
    private static SingleInstance instance;

    private SingleInstance(Context context) {
        this.mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstance(context.getApplicationContext());
        }
        return instance;
    }

    public void say() {
        Log.i("tag", "this is single instance");
        Log.i("tag", ":code:" + instance.hashCode());
    }
}
接收用户传递的Context之后,我们使用context.getApplicationContext() 由于使用了应用的ApplicationContext,而ApplicationContext的生命周期是伴随整个应用的,而且ApplicationContext每个应用只存在一个。所以就避免了单例模式引用具体的Activity实例
进行相同的操作:进入主界面,打开SecondActivity,打开LastActivity,然后关闭LastActivity,SecondActivity回到MainActivity界面,再次查看内存中的对象

此时SecondActivity和LastActivity对象个数都为0,都可被正常回收。此时内存泄漏的问题被解决。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐