Android内存泄漏问题分析及解决方案

大家新年好,由于工作繁忙原因,有好一段时间没有更新博文了(当然Github是一直都有更新的),趁着年底有点放假时间,我觉得抽空更新下博客,总结一下工作中最常见内存泄漏问题,也是自己之前踩过的坑,为了让大家少走弯路,系统全面总结一下内存泄漏问题分析原因及寻找解决方案。


概念

首先要理解什么叫做内存泄漏(Memory Leak),有很多人把内存泄漏和内存溢出(Out of Memory)混为一谈,其实他们两者并不相同。当然有些人可能“内存泄漏”打成“内存泄露”,个人认为前者更加准确地描述它本身的含义。 
大家都知道Android中的应用的最大使用内存是有限的,而不是可以占完所有的手机剩余内存(具体又有Java Heap和Native Heap区别,而且又有版本的区别,所以这里不做详细叙述,可以查阅其他文章看看),而android采用的是内存自动回收机制,也就是对于不使用的内存,Android会自动回收等待复用。由于这两个原因:如果分配内存超出了应用可以使用的最大内存,就会发生内存溢出;如果一个应该被回收的对象没有被Android自动回收机制回收,这种情况称为内存泄漏。

简单总结为:

  • 内存泄漏(Memory Leak):应该被回收的对象没有被回收
  • 内存溢出(Out of Memory):分配内存超出了应用可以使用的最大内存

值得注意,两者并没有必然关系,内存泄漏不一定导致内存溢出,内存溢出时不一定发生了内存泄漏。


常见原因及解决方案

了解了内存泄漏的概念后,我们知道,如果一个对象分配内存使用完毕后并没有被内存回收机制自动回收,这就会导致这个对象内存泄漏,下面来说说内存泄漏的几种常见原因。

  1. 资源使用未关闭 
    这是最常见的内存泄漏原因之一,对于使用了文件、图片、广播、数据库指针、流等资源的时候,应该在使用完成后关闭资源,否则可能导致对象回收时无法回收内存。值得注意的是,调用关闭的位置不正确,也会导致内存泄漏。 
    比如说,下面一段代码:

    try {
        InputStream inputStream = new FileInputStream("path");
        //inputStream.read();具体操作
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    初看这一段代码并没有什么问题,但是如果具体操作过程中出现了IOException,这样最后的inputStream.close();就不会执行,导致流实际上并没有关闭,从而导致使用流的对象最终无法回收。 
    这种问题解决方法应该把close方法放到finally里面,保证其一定执行:

    InputStream inputStream = null;
    try {
        inputStream = new FileInputStream("path");
        //inputStream.read();具体操作
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  2. 单例对象成员为可回收对象 
    这种情况也是常见的内存泄漏原因,很多第三方SDK早期版本甚至最新版也存在这个问题。 
    最常见就是一个单例类持有Activity对象作为成员变量,例如下面代码:

    public class SampleClass {
    
        private Context context;
    
        //一堆单例代码...
    
        public void init(Context context) {
            this.context = context;
            //...
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面这段代码,如果传入的Context是Activity的话,就会导致Activity销毁时候由于被其他对象所持有,自动回收机制不会回收这个对象,从而导致Activity内存泄漏。

  3. 外部类创建非静态内部类的静态实例 
    这个比较隐晦一些,也是经常会出现的错误。比如下面的代码

    public class SampleClass extends Activity{
    
        private static TestInner testInner;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            testInner = new TestInner();
        }
    
        class TestInner{}
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上述的TestInner生命周期比Activity长,Activity销毁后,TestInner依然持有Activity的对象引用,导致Activity的内存无法回收。 
    正确的方法应该把TestInner改成静态内部类,当然也可以把它抽取出来作为单例类。 
    这里要注意了,有人说自己不会犯这种错,但是有时候使用系统的某些对象作为静态成员也会出现这种情况:

    最典型就是使用getDrawable获得对象作为静态成员变量,Drawable对象设置到View身上时候,会持有View对象,View对象则会持有Activity对象,而Drawable对象生命周期比Activity长,最后Activity销毁后无法被自动回收。
    

    要注意处理上述的两种情况,实质上匿名内部类也是一种非静态的内部类,比如Handler泄漏、AsyncTask以及线程泄漏等等,这两种是非常常见的。这些和上述的类似,Handler这个典型的问题,Android Studio中Lint还可以检查出来,如果一定要使用Activity对象的话,可以采用WeakReference来处理。以Handler内部类为例:

    private static class MyHandler extends Handler { //注意这个是内部类
    
        private final WeakReference<Context> hContext;
    
        private MyHandler(Context hContext) {
            this.hContext = new WeakReference<>(hContext);
        }
    
        @Override
        public void handleMessage(Message msg) {
            //利用get()方法获得实例,注意判断非空
            Context context = this.hContext.get();
            if (context != null) {
                //...
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

检测内存泄漏

除了上述的原因以外还有其他可能导致内存泄漏,而且我们不可能在实际开发中重新review一遍代码来解决内存泄漏,我们需要一些工具来帮助我们解决内存泄漏。 
这里介绍一款我在实际项目中使用的工具Leakcanary,这个工具使用非常简单:

  1. 首先添加Gradle依赖并且同步
  2. 在Application中注册初始化,添加下面的代码

    LeakCanary.install(this);
    • 1
    • 1
  3. 然后就可以交给测试测试了,出现了问题会记录成log,并且弹出对应问题,如下图(借用下官方的图)

    这里写图片描述

这样就可以定位到哪里有内存泄漏了,更多使用方法可以参考官方的Github文档,这里就不一一阐述了。


总结

到此为止我们了解了什么是内存泄漏,内存泄漏原因以及如何检测内存泄漏。 
实际上在实际开发项目中,大部分的错误都是上述的常见原因,所以开发的时候要注意:

  • 关闭使用了的资源
  • 不要随便传递Activity,尽量使用Application Context或者使用弱引用
  • 谨慎使用statiic成员,使用的时候要注意生命周期问题

做到这些内存泄漏就不再是令人头疼的问题。


声明

原创文章,欢迎转载,请保留出处。
有任何错误、疑问或者建议,欢迎指出。
我的邮箱:[email protected]

猜你喜欢

转载自blog.csdn.net/zhjmyx/article/details/77709613