避免内存泄漏,做一个有“洁癖”的Android开发者(基础篇)

对于众多 Android 程序员而言,在需求与应用性能之间,主要精力都会聚焦新需求的开发。随着项目复杂度的增加,应用性能越来越低,各类故障频发。程序员们奔波于各种“救火现场”,疲于奔命。本文将根据笔者在 Android 应用程序开发过程中所遇以及思考,针对内存泄漏提炼出一套可以应用于开发中的基础篇方法论,也许会让你的开发效率事半功倍。

先从管理好内存应用开始

提及Random-access memory(随机存取存储器RAM)大家并不陌生,在任何软件开发环境中它都是宝贵的资源,而对于物理内存经常受到限制的移动操作系统来说,它就更具价值了。尽管Android Runtime(ART)和Dalvik虚拟机都会执行常规的垃圾收集(GC),但这并不意味着你可以忽略你的应用分配和释放内存的时间和位置,此时你仍然需要避免引入内存泄漏。

什么是内存溢出和内存泄漏?

  • 内存溢出(Out Of Memory Error):为了允许多进程,Android为每个应用程序分配的堆大小设置了硬性限制。确切的堆大小限制,会根据设备有多少内存总量而有所不同。如果你的应用程序使用的内存已达到该限制并尝试分配更多内存时,系统就会抛出内存溢出;
  • 内存泄漏(Memory Leak):是指应用在申请内存后,无法释放已申请的内存空间,是对内存资源的浪费。最坏的情况下,内存泄漏会最终导致内存溢出。

明确内存泄漏的危害性

一次内存泄漏的危害及影响并不大,但也不能放任不管。最坏的情况下,可能导致你的 APP 由于大量的内存泄漏而将内存耗尽,进而出现闪退状况,但这并非是常态。相反,内存泄漏会消耗大量的APP内存,但却不至于内存耗尽,这时,APP 会由于内存不够分配而频繁触发GC。而GC是非常耗时的操作,会导致严重的卡顿。另外,当你的应用处于LRU列表中(即切换到后台,变为后台进程)时,由于内存泄漏而消耗了更多内存,当系统资源不足而需要回收一部分缓存进程时,你的应用被系统杀死的可能性就更大了。

Tips:

  • 为什么我们在平时开发中并不太在意的GC会导致卡顿?你需要了解GC相关知识,包括“Full GC / Minor GC”、“GC停顿”等;
  • 应用进程在整个LRU列表中消耗的内存越少,保留在列表中并且能够快速恢复的机会就越大。

常用内存泄漏检测工具——LeakCanary

LeakCanary是大家所熟知的内存泄漏检测工具,它简单易用,集成以后能在应用发生泄漏时发出警告,并显示发生泄漏的堆栈信息,新版本还会显示具体泄漏的内存大小,作为被动监控泄漏的工具非常有效,但LeakCanary功能有限,不能提供更详细的内存快照数据,并且需要嵌入到工程中,会在一定程度上污染代码,所以一般都只在build version中集成,release version中则应该去掉。

借助内存分析工具——MAT

MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看什么阻止了垃圾收集器的回收工作,并通过报表直观的查看到可能造成这种结果的对象。

除了Eclipse插件版,MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。因为DDMS生成的是Android格式的HPROF(堆转储)文件,而MAT只能识别JAVA格式的HPROF文件。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。

要调试内存,首先需要获取HPROF文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。出于篇幅考虑,此处不再讲解MAT的使用,有兴趣的读者可以参考官方文档。

如何避免写出内存泄漏的代码

  • 谨慎使用static关键字,切勿使用static修饰Activity context;
  • 注意不要让类变量直接或间接地持有Activity context引用;
  • 尽量不要在单例中使用Activity context,如果要用,不能将其作为全局变量;
  • 时刻注意内部类(尤其是Activity的内部类)的生命周期,尽量使用静态内部类代替内部类,如果内部类需要访问外部类的成员,可以用“静态内部类+弱引用”代替;内部类的生命周期不应该超出外部类,外部类结束前,应该及时结束内部类生命周期(停止线程、AsyncTask、TimerTask、Handler消息等,移除类变量或长生命周期的线程对Callback、listener等的强引用);
  • 及时注销广播以及一些系统服务的监听器;
  • 属性动画在Activity销毁前记得cancel;
  • 文件流、Cursor等资源用完及时关闭;
  • Activity销毁前WebView的移除和销毁;
  • 使用别人的方法(尤其是第三方库),遇到需要传递context时尽量使用ApplicationContext,而不要轻易使用Activity context,因为你不知道别人的代码内部会不会造成该context的泄漏。比如微信支付SDK历史版本中就曾出现过内存泄漏的问题,微信支付初始化时需要传入context,最终由WXApiImpl这个类持有了context,如果你传入的是activity context,就会被WXApiImpl泄漏。

重点知识点梳理

  • GC如何判断某个对象是否可以被回收:在垃圾回收过程中,当一个对象到GC Roots 没有任何引用链(或者说,从GC Roots 到这个对象不可达)时,垃圾回收器就会释放掉它;
  • Java的引用级别:强引用 - 软引用 - 弱引用 - 虚引用;
  • JVM宁可抛出OOM也不会去回收一个有强引用的对象;
  • GC Root:有多种方法使得一个对象成为GC Root,GC Root是由虚拟机自身保持存活的对象,所以它不会被回收,由GC Root强引用的对象也无法被回收;
  • 内部类和静态内部类:内部类的一大优势就是可以直接引用外部类的成员,这是通过隐式地持有外部类的引用来实现的;而静态内部类,由于不再隐式地持有外部类的引用,也就无法直接引用外部类的成员了;
  • 如何避免内部类造成的泄漏:为避免内部类泄漏外部类,应该使用静态内部类。但静态内部类又无法访问外部类的成员,为解决该问题,可以使用“静态内部类+弱引用”,让静态内部类持有外部类的弱引用,既不会造成泄漏,又能解决访问外部类的成员变量的问题;
  • LeakCanary如何检查是否存在内存泄漏:WeakReference + ReferenceQueue。

希望通过今天这篇文章,能够让开发者了解到什么是内存泄漏及内存泄漏给带来的影响,同时希望大家通过文内步骤依次检测及修复内存泄漏问题,从现在开始,构建高质量/高性能的应用。处理内存泄漏不仅能让你的应用有更好的用户体验,也能让你成为更好的开发者,让我们朝着这个目标努力吧!

PS:后期,公众号将推出《针对Android 内存泄漏,如何做一个有“洁癖”的开发者(实战篇)》记得届时来关注。

-END-

参考文献

  1. [Eight Ways Your Android App Can Leak Memory](http://blog.nimbledroid.com/2016/05/23/memory-leaks.html)
  2. [Android内存优化——常见内存泄漏及优化方案](https://www.jianshu.com/p/ab4a7e353076)
  3. [LeakCanary on GitHub](https://github.com/square/leakcanary)
  4. [用LeakCanary检测内存泄漏](https://academy.realm.io/cn/posts/droidcon-ricau-memory-leaks-leakcanary/)
  5. [Overview of memory management](https://developer.android.com/topic/performance/memory-overview)
  6. [Video:Memory management on Android Apps](https://www.youtube.com/watch?v=_CruQY55HOk)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值