前言:一个刚上班和三年工作经验的安卓程序猿,做同一个项目。如果只从表面上看,并没有什么区别。但从项目的性能、稳定性、扩展性等方面来观察的话,差别不要太大。言归正传,今天我们来说一下安卓开发中很常见的问题,内存溢出 out of memory。

一、什么是内存溢出和内存泄漏。

内存溢出:内存溢出有两种情况,一种是指你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,就是溢出。还有一种是内存泄漏导致的内存溢出,Google OS默认为每个应用程序分配的内存为16M,但是各个厂家的OS会对这个值进行修改。分配几十到几百M不等。如果内存泄漏严重,导致jvm剩余内存不能满足新对象的创建时,就会出现out of memory。

内存泄漏:内存泄漏是指应用程序不再使用的对象,仍然被其他对象持有引用,导致jvm 垃圾回收器无法回收,内存空间无法释放。一次内存泄漏的危害可以忽略不计,但内存泄漏的堆积,最终会导致内存溢出。

二、解决内存溢出
内存溢出的第一种情况很好理解,你申请了多大的内存,就要在该内存上写入范围以内的数据。
比如 int a ;jvm 会提供4byte 32位的内存空间给我们,如果我们给a赋值超过32位的数值,就会出现out of memory。
这种情况很好解决。
内存溢出的第二种情况,也就是我们今天主要解决的,要解决内存泄漏引起的out of memory,就是避免内存泄漏。

内存泄漏的原因大多是由于开发者代码书写不规范造成的。下面列出安卓常见出现内存泄漏的地方。以下是干货,觉得有用的可以Mark下。

1、Hanlder的使用导致内存泄漏

在Android开发中,我们经常会使用Handler来控制主线程UI程序的界面变化,使用非常简单方便,但是稍不注意,很容易引发内存泄漏。
我们知道,Handler、Message、MessageQueue是相互关联在一起的,Handler通过发送消息Message与主线程进行交互,如果Handler发送的消息Message尚未被处理,该Message及发送它的Handler对象将被MessageQueue一直持有,这样就可能会导致Handler无法被回收。
请看下面的代码:

public class HandlerActivity extends AppCompatActivity {

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

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

    }

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

}

代码中有一个延迟1秒执行的消息Message,当界面从HandlerActivity 跳转到ThirdActivity时,HandlerActivity 自动进入后台,此时如果系统资源紧张(或者打开设置里面的“不保留活动”选项),HandlerActivity 将会被finish。但问题来了,由于HandlerActivity 的Handler对象mHandler为非静态匿名内部类对象,它会自动持有外部类HandlerActivity 的引用,从而导致HandlerActivity 无法被回收,造成内存泄漏。
解决办法:将Handler声明为静态内部类,就不会持有外部类HandlerActivity 的引用,其生命周期就和外部类无关,如果Handler里面需要context的话,可以通过弱引用方式引用外部类。

public class HandlerActivity extends AppCompatActivity {

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

        mHandler.postDelayed(mRunnable,1000);
    }


    private static final Runnable mRunnable = new Runnable() {
        @Override
        public void run() {


        }
    };


    private final MyHandler mHandler = new MyHandler(this);

    private static class MyHandler extends Handler{
        private WeakReference mActivity;

        public MyHandler(HandlerActivity activity){
            mActivity = new WeakReference(activity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            HandlerActivity activity = (HandlerActivity) mActivity.get();
            if(activity != null){
                //......
            }

        }
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacks(mRunnable);
    }
}

通过上面的方法,创建一个静态Handler内部类,其持有的对象context使用弱引用,可以避免HandlerActivity 内存泄漏,但是Looper线程的消息队列中可能还有待处理的消息,所以在Activity的onDestroy方法中,还要记住移除消息队列中待处理的消息。
Handler提供了如下方法移除消息队列中待处理的消息。

removeCallbacks(Runnable r)
removeCallbacks(Runnable r, Object token)
removeMessages(int what, Object object)
removeMessages(int what)
removeCallbacksAndMessages(Object token)

2、单例模式引起的内存泄漏(善用Context)
由于单例的生命周期是和app的生命周期一致的,如果使用不当很容易引发内存泄漏。如下代码:

public class Singleton {
    private static Singleton mInstance = null;
    private Context mContext;
    private Singleton(Context context){
        mContext = context;
    }
    public static Singleton getInstance(Context context){
        if(mInstance == null){
            mInstance = new Singleton(context);
        }
        return mInstance;
    }

}

这是一个单例模式的标准写法,表面上看没有任何问题,但是细心的同学会发现,构建该单例的一个实例时需要传入一个Context,此时传入的Context就非常关键,如果此时传入的是Activity,由于Context会被创建的实例一直持有,当Activity进入后台或者开启设置里面的不保留活动时,Activity会被销毁,但是单例持有它的Context引用,Activity又没法销毁,导致了内存泄漏。
如果此时传入的Context是ApplicationContext,由于ApplicationContext的生命周期是和app一致的,不会导致内存泄漏。但是我们不能指望使用这个单例的用户始终传入期望的Context,因此需要对这个单例设计进行调整,可以在构造函数中对mContext赋值改为this.mContext = context.getApplicationContext;当然,也可以直接不让用户传入context。

public class Singleton {
    private static Singleton mInstance = null;
    private Context mContext;
    private Singleton(Context context){
        mContext = context.getApplicationContext();
    }
    public static Singleton getInstance(Context context){
        if(mInstance == null){
            mInstance = new Singleton(context);
        }
        return mInstance;
    }

}

三、非静态内部类创建静态实例引起的内存泄漏

请看如下代码:

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(inner == null){
            inner = new Inner();
        }
    }

    private static Inner inner = null;

    private class Inner{


    }

}

上述代码中,SecondActivity 包含一个内部类InnerClass,并且在onCreate代码中创建了InnerClass的静态实例mInner,该实例和app的生命周期是一致的。在某些场景,如Activity需要频繁切换,需要不断加载大量图片的场合,是会出现上述代码的,每次Activity启动之后都会使用该单例,避免重复一些有压力的操作。但是这样会引起内存泄漏,因为非静态的内部类InnerClass会自动持有外部类SecondActivity 的引用,创建的静态实例mInner就会一直持有SecondActivity2的引用,导致SecondActivity 需要销毁的时候没法正常销毁。
怎么知道静态实例mInner持有SecondActivity 的引用呢?debug程序之后你会清晰的发现静态实例mInner确实持有外部类SecondActivity的引用。

正确的做法是吧内部了Inner改为静态内部类,那样他就不会持有SecondActivity 的引用。

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(inner == null){
            inner = new Inner();
        }
    }

    private static Inner inner = null;

    private static class Inner{


    }

}

当然,也可以把InnerClass单独抽出来作为一个内,写成单例模式,完成同样的功能,同时也可以避免内存泄漏。

四、非静态匿名内部类引起的内存泄漏
在android开发中,相信大家都会不知不觉地用到大量匿名内部类,如接受广播、点击事件、Handler消息处理等等。但是要注意,如果匿名内部类被异步线程使用,可能会引起内存泄漏。请看如下代码:

public class ThirdActivity extends AppCompatActivity {

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

        new Thread(mRunnable).start();

    }


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
}

上述代码中,mRunnable 是非静态匿名内部类,会自动持有外部类ThirdActivity 的引用,但是mRunnable被异步线程Thread使用,这样就会导致ThirdActivity 在销毁的时候没法正常销毁,从而引起内存泄漏。
正确的做法应该是把mRunnable设置为静态的,这样就不会自动持有外部类ThirdActivity 的引用,也就不会引起内存泄漏了。

五、注册/反注册未成对使用引起的内存泄漏
在andorid开发中,我们经常会在Activity的onCreate中注册广播接受器、EventBus等,如果忘记成对的使用反注册,可能会引起内存泄漏。开发过程中应该养成良好的相关,在onCreate或onResume中注册,要记得相应的在onDestroy或onPause中反注册。

六、资源对象没有关闭引起的内存泄漏
在android中,资源性对象比如Cursor、File、Bitmap、视频等,系统都用了一些缓冲技术,在使用这些资源的时候,如果我们确保自己不再使用这些资源了,要及时关闭,否则可能引起内存泄漏。因为有些操作不仅仅只是涉及到Dalvik虚拟机,还涉及到底层C/C++等的内存管理,不能完全寄希望虚拟机帮我们完成内存管理。
在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等函数,这些函数往往会通过jni调用底层C/C++的相应函数,完成相关的内存释放。

七、集合对象没有及时清理引起的内存泄漏
我们通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。如果集合是static、不断的往里面添加东西、又忘记去清理,肯定会引起内存泄漏。

使用 LeakCanary 检测 Android 的内存泄漏
内存泄漏防不胜防,通过LeakCanary工具,我们能在开发测试阶段发现绝大多数的内存泄漏。这个工具是开源的,使用也非常方便,而且能够相对准确定位出是哪里出现内存泄漏。
下面以AndroidStudio为例介绍LeakCanary的使用:
1、在build.gradle中配置leakcanary的引用
compile ‘com.squareup.leakcanary:leakcanary-android:1.4-beta2’
2、使用RefWatcher监测本该被回收的对象。
LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
在自己的Application中添加如下代码:

android 标准ota android outo_android

只需要上述两个步骤之后,LeakCanary就会自动监测内存泄漏,如果有内存泄漏在手机上面会提示,通知栏也会有通知,点击进去之后可以看到具体内存泄露的地方。debug的话,在控制台也会有相关的log输出。

当然也可以通过Studio的Heap 。通过多次手动GC查看项目内存泄漏的情况,如果多次GC,内存占用更高的话,就说明内存泄漏比较严重,良好的应用程序,多次gc后内存占用是平缓的。

总结:
1、Handler持有的引用最好使用弱引用,在Activity被释放的时候要记得清空Message,取消Handler对象的Runnable;
2、非静态内部类、非静态匿名内部类会自动持有外部类的引用,为避免内存泄露,可以考虑把内部类声明为静态的;
3、对于生命周期比Activity长的对象,要避免直接引用Activity的context,可以考虑使用ApplicationContext;
4、广播接收器、EventBus等的使用过程中,注册/反注册应该成对使用;
5、不再使用的资源对象Cursor、File、Bitmap等要记住正确关闭;
6、集合里面的东西、有加入就应该对应有相应的删除。