阅读 1194

Android 关于内存泄露,你必须了解的东西

前言

内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄露」。

如果程序发生了内存泄露,则会带来以下这些问题

  • 应用可用的内存减少,增加了堆内存的压力

  • 降低了应用的性能,比如会触发更频繁的 GC

  • 严重的时候可能会导致内存溢出错误,即 OOM Error

OOM 发生在,当我们尝试进行创建对象,但是堆内存无法通过 GC 释放足够的空间,堆内存也无法再继续增长,从而完成对象创建请求的时候,OOM 发生很有可能是内存泄露导致的,但并非所有的 OOM 都是由内存泄露引起的,内存泄露也并不一定引起 OOM。

一、基础准备


如果真的想比较清楚的了解内存泄露的话,对于 Java 的内存管理以及引用类型有一个清晰的认识是必不可少的。

  • 理解 Java 的内存管理能让我们更深一层地了解 Java 虚拟机是怎样使用内存的,一旦出现内存泄露,我们也能更加从容地排查问题。

  • 了解 Java 的引用类型,能让我们更加理解内存泄露出现的原因,以及常见的解决方法。

具体的内容,可以看下这篇文章 你真的懂 Java 的内存管理和引用类型吗?

二、Android 中内存泄露的常见场景 & 解决方案


1、单例造成的内存泄露

单例模式是非常常用的设计模式,使用单例模式的类,只会产生一个对象,这个对象看起来像是一直占用着内存,但这并不意味着就是浪费了内存,内存本来就是拿来装东西的,只要这个对象一直都被高效的利用就不能叫做泄露。

但是过多的单例会让内存占用过多,而且单例模式由于其 静态特性,其生命周期 = 应用程序的生命周期,不正确地使用单例模式也会造成内存泄露。

举个例子:

public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

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

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}
复制代码

上面是一个比较简单的单例模式用法,需要外部传入一个 Context 来获取该类的实例,如果此时传入的 Context 是 Activity 的话,此时单例就有持有该 Activity 的强引用(直到整个应用生命周期结束)。这样的话,即使该 Activity 退出,该 Activity 的内存也不会被回收,这样就造成了内存泄露,特别是一些比较大的 Activity,甚至还会导致 OOM(Out Of Memory)。

解决方法: 单例模式引用的对象的生命周期 = 应用生命周期

public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

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

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}
复制代码

可以看到在 SingleInstanceTest 的构造函数中,将 context.getApplicationContext() 赋值给 mContext,此时单例引用的对象是 Application,而 Application 的生命周期本来就跟应用程序是一样的,也就不存在内存泄露。

这里再拓展一点,很多时候我们在需要用到 Activity 或者 Context 的地方,会直接将 Activity 的实例作为参数传给对应的类,就像这样:

public class Sample {
    
    private Context mContext;
    
    public Sample(Context context){
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);
复制代码

这种情况如果不注意的话,很容易就会造成内存泄露,比较好的写法是使用弱引用(WeakReference)来进行改进。

public class Sample {

    private WeakReference<Context> mWeakReference;

    public Sample(Context context){
        this.mWeakReference = new WeakReference<>(context);
    }

    public Context getContext() {
        if(mWeakReference.get() != null){
            return mWeakReference.get();
        }
        return null;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);
复制代码

被弱引用关联的对象只能存活到下一次垃圾回收之前,也就是说即使 Sample 持有 Activity 的引用,但由于 GC 会帮我们回收相关的引用,被销毁的 Activity 也会被回收内存,这样我们就不用担心会发生内存泄露了。

2、非静态内部类 / 匿名类

我们先来看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别。

class 对比 static inner class non static inner class
与外部 class 引用关系 如果没有传入参数,就没有引用关系 自动获得强引用
被调用时需要外部实例 不需要 需要
能否调用外部 class 中的变量和方法 不能
生命周期 自主的生命周期 依赖于外部类,甚至比外部类更长

可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。

举个例子:

public class MainActivity extends AppCompatActivity {

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

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
复制代码

可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这时候因为 MyAsyncTask 持有 Activity 的强引用,便会导致 Activity 的内存无法被回收,这时候便会产生内存泄露。

解决方法: 将 MyAsyncTask 变成非静态内部类

public class MainActivity extends AppCompatActivity {

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

    static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
复制代码

这时候 MyAsyncTask 不再持有 Activity 的强引用,即使 AsyncTask 的耗时操作还在继续,Activity 的内存也能顺利地被回收。

匿名类和非静态内部类最大的共同点就是 都持有外部类的引用,因此,匿名类造成内存泄露的原因也跟静态内部类基本是一样的,下面举个几个比较常见的例子:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ① 匿名线程持有 Activity 的引用,进行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(50000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // ② 使用匿名 Handler 发送耗时消息
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 60000);
    }
复制代码

上面举出了两个比较常见的例子

  • new 出一个匿名的 Thread,进行耗时的操作,如果 MainActivity 被销毁而 Thread 中的耗时操作没有结束的话,便会产生内存泄露

  • new 出一个匿名的 Handler,这里我采用了 sendMessageDelayed() 方法来发送消息,这时如果 MainActivity 被销毁,而 Handler 里面的消息还没发送完毕的话,Activity 的内存也不会被回收

解决方法:

  • 继承 Thread 实现静态内部类

  • 继承 Handler 实现静态内部类,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);

3、集合类

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露,举个例子:

   static List<Object> objectList = new ArrayList<>();
   for (int i = 0; i < 10; i++) {
       Object obj = new Object();
       objectList.add(obj);
       obj = null;
    }
复制代码

在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。

解决方法: 在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

    objectList.clear();
    objectList = null;
复制代码

4、其他的情况

除了上述 3 种常见情况外,还有其他的一些情况

  • 1、需要手动关闭的对象没有关闭

    • 网络、文件等流忘记关闭
    • 手动注册广播时,退出时忘记 unregisterReceiver()
    • Service 执行完后忘记 stopSelf()
    • EventBus 等观察者模式的框架忘记手动解除注册
  • 2、static 关键字修饰的成员变量

  • 3、ListView 的 Item 泄露

三、利用工具进行内存泄露的排查


除了必须了解常见的内存泄露场景以及相应的解决方法之外,掌握一些好用的工具,能让我们更有效率地解决内存泄露的问题。

1、Android Lint

Lint 是 Android Studio 提供的 代码扫描分析工具,它可以帮助我们发现代码机构 / 质量问题,同时提供一些解决方案,检测内存泄露当然也不在话下,使用也是非常的简单,可以参考下这篇文章:Android 性能优化:使用 Lint 优化代码、去除多余资源

2、leakcanary

LeakCanary 是 Square 公司开源的「Android 和 Java 的内存泄漏检测库」,Square 出品,必属精品,功能很强大,使用也很简单。建议直接看 Github 上的说明:leakcanary,也可以参考这篇文章:Android内存优化(六)LeakCanary使用详解


参考资料

关注下面的标签,发现更多相似文章
评论