一、 内存泄漏与内存溢出(OOM)

1. 内存泄露

  • 垃圾回收器无法回收原本应该被回收的对象,这个对象就引发了内存泄露。
  • 内存泄露的危害: (1)过多的内存泄露最终会导致内存溢出(OOM)(2)内存泄露导致可用内存不足,会触发频繁GC,不管是Android2.2以前的单线程GC还是现在的CMS和G1,都有一部分的操作会导致用户线程停止(就是所谓的Stop the world),从而导致UI卡顿。

2. 内存溢出(OOM)

  • Android为每个进程设置Dalvik Heap Size阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果APP想要分配的内存超过这个阈值,就会发生OOM。
  • ActivityManager.getMemoryClass()可以查询当前APP的Heap Size阈值,单位是MB。
  • 在3.x以前,Bitmap分配在Native heap中,而在4.x之后,Bitmap分配在Dalvik或ART的Java heap中。
  • Android 2.x系统,当dalvik allocated + native allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生OOM,也就是说在2.x系统中,考虑native heap对每个进程的内存限制。
  • Android 4.x系统,废除了native的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生OOM(art运行环境的统计规则还是和dalvik保持一致),也就是说在4.x系统中,不考虑native heap对每个进程的内存限制,native heap只会收到本机总内存(包括RAM以及SWAP区或分页文件)的限制。

二、 LeakCanary内存检测工具配置

LeakCanary在GitHub上地址:https://github.com/square/leakcanary

在app的build.gradle中加入:

 

dependencies {
    //leakcanary内存检测三方库
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}

 

在项目的Application中加入(没有则创建):

 

@Override
    public void onCreate() {
        super.onCreate();
        //初始化内存检测工具
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
        // Normal app init code...
    }
}

 

三、 内存泄漏检测案例

非静态内部类引起的内存泄漏

导致内存泄漏的错误代码:

 

public class LeakActivity extends AppCompatActivity {

    private static User mUser;
        ……
    /**
     * 非静态内部类引起的内存泄漏
     */
    private void innerClassLeak() {
        mUser = new User();

    }

    class User {
        private String userName;
    }
}

 

内存泄漏检测工具界面截图:

android 如何处理内存溢出 安卓 内存溢出_Android


分析结果:
从下往可以看出导致内存泄漏是LeakActivity中private static User mUser;导致。导致该Activity销毁时释放不了.

解决后代码:

 

private static User mUser;
        ……
    /**
     * 非静态内部类引起的内存泄漏
     */
    private void innerClassLeak() {
        mUser = new User();

    }

    static  class User {
        private String userName;
    }
}

 

 

将非静态类加入static修饰变为静态类,或者去除 private static User mUser; 中static修饰符

Thread引起的内存泄漏

导致内存泄漏的错误代码:

/**
 * 线程引起内存泄漏
 */
private void threadLeak() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            //耗时操作
            SystemClock.sleep(10000);//睡眠10s,模拟耗时操作

        }
    }).start();
}

 

 

 

内存泄漏检测工具界面截图:

android 如何处理内存溢出 安卓 内存溢出_内存泄漏_02


分析结果:
可以看出内存泄漏的原因是LeakActivity中有个任务导致,当次Activity销毁时,此线程任务未完成,导致此Activity无法释放。

解决后代码:

/**
 * 线程引起内存泄漏
 */
private void threadLeak() {
    new MyThread().start();
}

static class MyThread extends Thread{
    @Override
    public void run() {
        //耗时操作
        SystemClock.sleep(10000);//睡眠10s,模拟耗时操作
        super.run();
    }
}

 

 

 

自定义线程池工具类来开启此线程也可以。

Handler引起的内存泄漏

导致内存泄漏的错误代码:

Handler handler= new Handler();
/**
 * Handler导致内存泄漏
 */
private void HandlerLeak() {
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            handler.postDelayed(this,1000);//每隔1000毫秒,循环发送消息
        }
    },1000);
}

 

 

 

内存泄漏检测工具界面截图:

android 如何处理内存溢出 安卓 内存溢出_android 如何处理内存溢出_03


分析结果:
从下到上可以看出LeakActivity中消息任务导致内存泄漏,此Activity关闭时消息任务还未关闭,导致此Activity未能销毁。

解决后代码:

Handler handler= new Handler();
/**
 * Handler导致内存泄漏
 */
private void HandlerLeak() {
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            handler.postDelayed(this,1000);//每隔1000毫秒,循环发送消息
        }
    },1000);

}

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);//remove正在执行的消息任务
}

 

 

 

当Activity销毁时调用关闭所有消息任务

单例Toast引起内存溢出

导致内存溢出的错误代码:

/**
 * 静态Toast引起的内存泄漏
 */
private void ToastLeak() {
    ToastUtils.showToast(this,new Date().getTime()+"");
}

private static Toast toast;

public static  void showToast(Context context, String msg){
    if(toast==null)
    toast = Toast.makeText(context,"",Toast.LENGTH_SHORT);

    toast.setText(msg);
    toast.show();
}

 

 

 

内存泄漏检测工具界面截图:

android 如何处理内存溢出 安卓 内存溢出_Android_04


分析结果:
从下至上看出LeakActivity 中Toast(这里使用了静态Toast) 的Context导致LeakActivity无法销毁,内存泄漏

解决后代码:

/**
 * 静态Toast引起的内存泄漏
 */
private void ToastLeak() {
    ToastUtils.showToast(this,new Date().getTime()+"");
}

private static Toast toast;

public static  void showToast(Context context, String msg){
    if(toast==null)
    toast = Toast.makeText(context.getApplicationContext(),"",Toast.LENGTH_SHORT);

    toast.setText(msg);
    toast.show();
}

 

 

 

这里使用的是应用全局的Context。
什么是Context:https://possiblemobile.com/2013/06/context/

四、如何避免内存泄漏

  • 使用轻量的数据结构:使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
  • 不要轻易使用Enum。
  • Bitmap压缩处理:使用知名的第三方开源库处理。
  • 不要使用String进行字符串拼接:频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。
  • 谨慎使用static对象:static对象的生命周期过长,应该谨慎使用。
  • Context持有导致内存泄漏:Activity中的context被传递到其他实例中,这可能导致自身被引用而发生泄漏。