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分析就是如下图

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

单例模式造成的内存泄漏

上面的内存泄漏太明显,估计大家都不会这样写,但是单例模式就不一样了,我们往往会忽略掉错误使用单例模式而造成的泄漏。比如说我们常在开发中用到的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);
    }
}

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

这个图和上面的内存泄漏的图很相像。但是我们常常忽略了这种内存泄漏,是因为我们没有直接使用静态变量指向传递进来的参数,解决办法要保证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可以看到类似下图

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

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

其他原因造成的内存泄漏

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

本篇总结

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

下篇预告

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


此致,敬礼

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android先生

Android小技巧: 这里涵盖了所有实现 “一键退出 App” 的方法

即 需要2个步骤 才可 完成 一键退出 App 需求。下面,我将根据这两个步骤进行功能实现讲解。

622
来自专栏7号代码

Android网络与数据存储——File存储(实现SD卡文件浏览器)

AndroidManifest.xml中manifest标签下有一个属性android:installLocation,用于指定应用程序安装在什么地方,该属性有...

2463
来自专栏技术小黑屋

Android内存泄漏检测利器:LeakCanary

到这里你就可以检测到Activity的内容泄露了。其实现原理是设置Application的ActivityLifecycleCallbacks方法监控所有Act...

1212
来自专栏学海无涯

Android开发之连续点击返回键退出程序

简介 在很多程序中,都有这样一个功能,就是在主界面,连续点击返回键会退出程序。它一般是这样显示的:第一次按下提示你 再按一次退出程序 ,如果此时立马点击返回键会...

3426
来自专栏Android源码框架分析

SharePreference原理及跨进程数据共享的问题

SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。为什么这么说呢?因为SharedPrefere...

1736
来自专栏IT大咖说

Oracle中最容易被忽略的那些实用特性

内容来源:2017 年 04 月 08 日,ITPUB管理版版主吕海波在“DBGeeK+PG数据库技术沙龙(4月杭州站)”进行《Oracle中最容易被忽略的那些...

1416
来自专栏分享达人秀

ListView列表数据源——Adapter

在上一节一起了解了ListView的简单使用,那么本节继续来学习与ListView有着千丝万缕的Adapter。 一、了解MVC模式 在开始学习...

35710
来自专栏码农笔录

Android全能开源项目xUtils3开发教程、简单封装

1492
来自专栏AndroidTv

【Android】再来一篇Fragment的懒加载(只加载一次哦)

2017-7-14更新: 目前有人使用后出现了诸如首次打开显示空白界面,但点击有反应;或来回切换又变空白界面的问题。这些问题我暂时还不知道该怎么解决,后期有时...

4677
来自专栏Android先生

(新瓶旧酒)谷歌官方MVP项目学习--浅入源码

项目的目的是通过展示各种架构app的不同方式来帮助开发者解决架构问题。项目中通过不同的架构概念及方式实现了功能相同的app。你可以用示例来当做参考,或是干脆拿来...

951

扫码关注云+社区