Android 内存泄漏分析与解决

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!另外,本系列文章知识可能需要有一定Android开发基础和项目经验的同学才能更好理解,也就是说该系列文章面向的是Android中高级开发工程师。


前言

上一篇我们主要上了一个实例来把读者带进自定义ViewGroup的大门,只是带进大门,自定义View的内容还有很多,我之后碰到一些好的自定义View的话一定还来这里分享。本篇内容我们来分析App运行过程中出现的内存泄漏及如何解决。


内存泄漏概念及其影响

内存泄漏通俗的讲是一个本该被回收的对象却因为某些原因导致其不能回收。我们都知道对象是有生命周期的,从生到死,当对象的任务完成之后,由Android系统进行垃圾回收。我们知道Android系统为某个App分配的内存是有限的(这个可能根据机型的不同而不同),当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,最终导致OOM(OutOfMemory)使程序崩溃。

内存泄漏检查工具介绍

早在使用Eclipse的时候我们就知道了MAT性能分析工具,使用MAT当然能检查内存泄漏,不过使用稍微有些麻烦,我这里介绍另一个工具,同时呢,我们也抛弃了Eclipse,拥抱Android Studio。这个工具名叫LeakCanary。为什么要使用这个工具呢,当然因为其简单,傻瓜式操作。这个工具是在Github开源的,是Square公司出品的,不是有一句话嘛,Square出品必属精品,https://github.com/square/leakcanary我们可以方便的引用它

In your build.gradle:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
 }

In your Application class:

public class ExampleApplication extends 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 MainActivity extends Activity{
    private static final String TAG = "MainActivity";

    private static Context sContext;
       
    private static View sView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //这里直接把当前Activity赋值给了静态变量sContext
        sContext = this;
        //这种写法和上面的类似
        sView = new View(this);

    }
}

上面这种方法估计小学生都知道会造成内存泄漏,原因是当MainActivity对象完成任务需要回收时,却有一个静态变量引用它(静态变量的生命周期与Application相同),造成内存泄漏。我们使用LeakCanary分析就是如下图

Android 内存泄漏分析与解决

当我们的App发生内存泄漏时会在通知栏显示通知,点击该通知可得到内存泄漏的详细信息,或者点击上图中的Leaks图标获得App运行过程中所有的内存泄漏,上面例子中得到的内存泄漏信息如下图所示

Android 内存泄漏分析与解决

单例模式造成的内存泄漏

上面的内存泄漏太明显,估计大家都不会这样写,但是单例模式就不一样了,我们往往会忽略掉错误使用单例模式而造成的泄漏。比如说我们常在开发中用到的dp转px,px转dp等往往会封装成一个单例类。如下

public class DisplayUtils {
    private static volatile DisplayUtils instance = null;
    private Context mContext;
    private DisplayUtils(Context context){
        this.mContext = context;
    }
    public static DisplayUtils getInstance(Context context){
        if (instance != null){
            synchronized (DisplayUtils.class){
                if (instance !=null){
                    instance = new DisplayUtils(context);
                }
            }
        }

        return instance;
    }

    public int dip2px(float dpValue) {
        final float scale = mContext.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}

然后我们去调用它

public class SingleActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_single);
        //这里我们把当前SingleActivity传入
        DisplayUtils.getInstance(this).dip2px(5);
    }
}

就这样内存泄漏产生了,我们可以看图。

Android 内存泄漏分析与解决

这个图和上面的内存泄漏的图很相像。但是我们常常忽略了这种内存泄漏,是因为我们没有直接使用静态变量指向传递进来的参数,解决办法要保证Context和AppLication的生命周期一样,修改后代码如下:

public class DisplayUtils {
    private static volatile DisplayUtils instance = null;
    private Context mContext;
    private DisplayUtils(Context context){
        //这里变化了,把当前Context指向个应用程序的Context
        this.mContext = context.getApplicationContext();
    }
    public static DisplayUtils getInstance(Context context){
        if (instance != null){
            synchronized (DisplayUtils.class){
                if (instance !=null){
                    instance = new DisplayUtils(context);
                }
            }
        }

        return instance;
    }

    public int dip2px(float dpValue) {
        final float scale = mContext.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}

非静态内部类创建静态实例造成的内存泄漏

我们在程序中基本上不能避免使用ListView或者RecyclerView,谈到这些列表展示的类,那么我们的Adapter基本上也是不可缺少,我们在优化ListView的Adapter的时候会使用ViewHolder(RecyclerView本身已经做了优化),我们在使用ViewHolder的使用建议使用静态内部类。那么为什么会由此建议呢?这就是我们下面要谈到的。非静态内部类创建静态实例可能造成的内存泄漏

public class NonStaticActivity extends AppCompatActivity {
    private static Config sConfig;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_non_static);
        //Config类并不是静态类,
        sConfig = new Config();
    
    }
    
    class Config {
    
    }

}

造成内存泄漏的原因是内部类会隐式持有外部类的引用,这里的外部类是NonStaticActivity,然而内部类sConfig又是static静态变量其生命周期与Application生命周期一样,所以在NonStaticActivity关闭的时候,内部类静态实例依然持有对NonStaticActivity的引用,导致NonStaticActivity无法被回收释放,引发内存泄漏。
解决办法就是把内部类生命为静态内部类,与外部类解耦。,这也是在使用ViewHolder的使用建议使用静态内部类的原因。

WebView造成的内存泄漏

对于使用Android的WebView造成的内存泄漏。我在此建议使用https://github.com/delight-im/Android-AdvancedWebView,使用这个优化后的WebView,按照提示进行操作。

Handler造成的内存泄漏

我在我的项目中使用了handler,此时mHandler会隐式地持有一个外部类对象引用这里就是MainActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

public class HandlerActivity extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;

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

        mTextView = (TextView) findViewById(R.id.text);//模拟内存泄露
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("test");
            }
        }, 5 * 60 * 1000);

    }


}

用LeakCanary可以看到类似下图

Android 内存泄漏分析与解决

解决办法是 在HandlerActivity onDestroy里面移除消息队列中所有消息和所有的Runnable。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    mHandler = null;
}

其他原因造成的内存泄漏

造成内存泄漏的原因有很多,我们这里只是列举了其中比较典型的几种,当然还有好多原因会造成内存泄漏,比如资源开启但是未关闭、多线程等等等等。但是我们有LeakCanary这个利器哈。

本篇总结

本篇只是稍微介绍了下LeakCanary以及几种常见的内存泄漏,内存泄漏以及内存性能优化是个持久的过程。我这里只是向你们介绍其中一种方法。编程无止境,性能优化也是。

下篇预告

好了,我们下一篇介绍正篇Android的消息机制Looper、Handler、MessageQueue,Message


此致,敬礼

相关推荐