Android 内存泄漏

前言

内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄露」。

如果程序发生了内存泄露,则会带来以下这些问题

应用可用的内存减少,增加了堆内存的压力

降低了应用的性能,比如会触发更频繁的 GC

严重的时候可能会导致内存溢出错误,即 OOM Error

OOM 发生在,当我们尝试进行创建对象,但是堆内存无法通过 GC 释放足够的空间,堆内存也无法再继续增长,从而完成对象创建请求的时候,OOM 发生很有可能是内存泄露导致的,但并非所有的 OOM 都是由内存泄露引起的,内存泄露也并不一定引起 OOM。

一、基础准备

如果真的想比较清楚的了解内存泄露的话,对于 Java 的内存管理以及引用类型有一个清晰的认识是必不可少的。

理解 Java 的内存管理能让我们更深一层地了解 Java 虚拟机是怎样使用内存的,一旦出现内存泄露,我们也能更加从容地排查问题。

了解 Java 的引用类型,能让我们更加理解内存泄露出现的原因,以及常见的解决方法。

具体的内容,可以看下这篇文章你真的懂 Java 的内存管理和引用类型吗?

二、Android 中内存泄露的常见场景 & 解决方案

1、单例造成的内存泄露

单例模式是非常常用的设计模式,使用单例模式的类,只会产生一个对象,这个对象看起来像是一直占用着内存,但这并不意味着就是浪费了内存,内存本来就是拿来装东西的,只要这个对象一直都被高效的利用就不能叫做泄露。

但是过多的单例会让内存占用过多,而且单例模式由于其静态特性,其生命周期 = 应用程序的生命周期,不正确地使用单例模式也会造成内存泄露。

举个例子:

publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context;    }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){            sInstance =newSingleInstanceTest(context);        }returnsInstance;    }}

上面是一个比较简单的单例模式用法,需要外部传入一个 Context 来获取该类的实例,如果此时传入的 Context 是 Activity 的话,此时单例就有持有该 Activity 的强引用(直到整个应用生命周期结束)。这样的话,即使该 Activity 退出,该 Activity 的内存也不会被回收,这样就造成了内存泄露,特别是一些比较大的 Activity,甚至还会导致 OOM(Out Of Memory)。

解决方法:单例模式引用的对象的生命周期 = 应用生命周期

publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context.getApplicationContext();    }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){            sInstance =newSingleInstanceTest(context);        }returnsInstance;    }}

可以看到在 SingleInstanceTest 的构造函数中,将 context.getApplicationContext() 赋值给 mContext,此时单例引用的对象是 Application,而 Application 的生命周期本来就跟应用程序是一样的,也就不存在内存泄露。

这里再拓展一点,很多时候我们在需要用到 Activity 或者 Context 的地方,会直接将 Activity 的实例作为参数传给对应的类,就像这样:

publicclassSample{privateContext mContext;publicSample(Context context){this.mContext = context;    }publicContextgetContext(){returnmContext;    }}// 外部调用Sample sample =newSample(MainActivity.this);

这种情况如果不注意的话,很容易就会造成内存泄露,比较好的写法是使用弱引用(WeakReference)来进行改进。

publicclassSample{privateWeakReference mWeakReference;publicSample(Context context){this.mWeakReference =newWeakReference<>(context);    }publicContextgetContext(){if(mWeakReference.get() !=null){returnmWeakReference.get();        }returnnull;    }}// 外部调用Sample sample =newSample(MainActivity.this);

被弱引用关联的对象只能存活到下一次垃圾回收之前,也就是说即使 Sample 持有 Activity 的引用,但由于 GC 会帮我们回收相关的引用,被销毁的 Activity 也会被回收内存,这样我们就不用担心会发生内存泄露了。

2、非静态内部类 / 匿名类

我们先来看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别。

class 对比static inner classnon static inner class

与外部 class 引用关系如果没有传入参数,就没有引用关系自动获得强引用

被调用时需要外部实例不需要需要

能否调用外部 class 中的变量和方法不能能

生命周期自主的生命周期依赖于外部类,甚至比外部类更长

可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。

举个例子:

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);newMyAscnyTask().execute();    }classMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{                Thread.sleep(5000);            }catch(InterruptedException e) {                e.printStackTrace();            }return"";        }    }}

可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这时候因为 MyAsyncTask 持有 Activity 的强引用,便会导致 Activity 的内存无法被回收,这时候便会产生内存泄露。

解决方法:将 MyAsyncTask 变成静态内部类

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);newMyAscnyTask().execute();    }staticclassMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{                Thread.sleep(50000);            }catch(InterruptedException e) {                e.printStackTrace();            }return"";        }    }}

这时候 MyAsyncTask 不再持有 Activity 的强引用,即使 AsyncTask 的耗时操作还在继续,Activity 的内存也能顺利地被回收。

匿名类和非静态内部类最大的共同点就是都持有外部类的引用,因此,匿名类造成内存泄露的原因也跟静态内部类基本是一样的,下面举个几个比较常见的例子:

publicclassMainActivityextendsAppCompatActivity{privateHandler mHandler =newHandler(){@OverridepublicvoidhandleMessage(Message msg){super.handleMessage(msg);        }    };@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);// ① 匿名线程持有 Activity 的引用,进行耗时操作newThread(newRunnable() {@Overridepublicvoidrun(){try{                    Thread.sleep(50000);                }catch(InterruptedException e) {                    e.printStackTrace();                }            }        }).start();// ② 使用匿名 Handler 发送耗时消息Message message = Message.obtain();        mHandler.sendMessageDelayed(message,60000);    }

上面举出了两个比较常见的例子

new 出一个匿名的 Thread,进行耗时的操作,如果 MainActivity 被销毁而 Thread 中的耗时操作没有结束的话,便会产生内存泄露

new 出一个匿名的 Handler,这里我采用了 sendMessageDelayed() 方法来发送消息,这时如果 MainActivity 被销毁,而 Handler 里面的消息还没发送完毕的话,Activity 的内存也不会被回收

解决方法:

继承 Thread  实现静态内部类

继承 Handler 实现静态内部类,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);

3、集合类

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露,举个例子:

staticList objectList =newArrayList<>();for(int i =0; i <10; i++) {Objectobj =newObject();      objectList.add(obj);      obj =null;    }

在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。

解决方法:在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

objectList.clear();    objectList =null;

4、其他的情况

除了上述 3 种常见情况外,还有其他的一些情况

1、需要手动关闭的对象没有关闭

网络、文件等流忘记关闭

手动注册广播时,退出时忘记 unregisterReceiver()

Service 执行完后忘记 stopSelf()

EventBus 等观察者模式的框架忘记手动解除注册

2、static 关键字修饰的成员变量

3、ListView 的 Item 泄露

三、利用工具进行内存泄露的排查

除了必须了解常见的内存泄露场景以及相应的解决方法之外,掌握一些好用的工具,能让我们更有效率地解决内存泄露的问题。

1、Android Lint

Lint 是 Android Studio 提供的代码扫描分析工具,它可以帮助我们发现代码机构 / 质量问题,同时提供一些解决方案,检测内存泄露当然也不在话下,使用也是非常的简单,可以参考下这篇文章:Android 性能优化:使用 Lint 优化代码、去除多余资源

2、leakcanary

LeakCanary 是 Square 公司开源的「Android 和 Java 的内存泄漏检测库」,Square 出品,必属精品,功能很强大,使用也很简单。建议直接看 Github 上的说明:leakcanary,也可以参考这篇文章:Android内存优化(六)LeakCanary使用详解

作者:developerHaoz

链接:https://www.jianshu.com/p/65f914e6a2f8

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,624评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 790评论 0 5
  • 整理平时遇到Android内存泄漏归纳分析心得 内存泄漏:对于Java来说,就是new出来的Object 放在堆上...
    安仔夏天勤奋阅读 1,045评论 1 28
  • 今天上午妈妈带我去上轮滑课。我见到了张光耀我们一起愉快的玩耍。我们还认识了一个新的教练,他的名字叫小天教练。今天的...
    小虎宝1阅读 233评论 0 0