Android内存溢出OOM:常见内存泄漏

在上一篇文章中我们对Android中内存有了一个基本的了解,在本文继续介绍有关内存溢出的相关点。当内存泄漏超过一定的界限,必然会引起内存溢出,有些内存泄漏在开发中是比较常见的,接下来通过介绍几种常见额内存泄漏情形,以便在开发过程中采取必要的措施以此防止内存泄漏。

如下是Android开发者在开发中比较常见的几种内存泄漏,并给出了相对应的防止内存泄漏的解决方式。

单例模式引起的内存泄漏

单例模式可以说在Android开发过程中使用最多的一种设计模式,所以由该模式导致的内存泄漏也是比较常见的。

在这里介绍两种由单例模式导致的内存泄漏,其实引起内存泄漏的原因都是一样的。

  • 在构造单例模式的getInstance()方法中传入了一个Context对象,但是Context是某个Activity调用者自己。
  • 在单例模式中的成员变量是一个监听器Listener或者说是一个回调Callback,但是该监听器监听器Listener或者回调Callback的实现类是一个Activity或者Fragment。

上面就是一个典型的会造成内存泄漏的单利模式示例,Context和Callback都是作为单利模式的一个成员变量传入的。如果仅仅是作为AppManager中某个方法的入参,这种情形是不会导致内存泄漏的,因为方法被调用后,方法中局部变量会被自动释放。然而作为成员变量或者属性则不同了,当调用getInstance()方法的时候如果传入一个Activity或者Fragment,那么AppManager就会持有Activity,其实传入Context与某个Activity或者Fragment实现了AppManager的Callback一样。由于单例模式的生命周期跟整个应用的生命周期一样长,所以上面介绍的两种情形都会导致AppManager持有Activity或者Fragment引用,当JVM进行垃圾回收的时候,导致Activity或者Fragment无法被GC回收,从而导致内存泄漏。

针对第一种情况,有些开发者可能会说只要Context传入的是getApplicationContext()就可以因Activity导致的内存泄漏,但是不是所有的情况下都可以将Context与getApplicationContext()相互替换的。

context_application

  • 数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。
  • 数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。
  • 数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

第二种方式中,除非我们的单例模式就是给应用的Application使用的,否则只要某个Activity或者Fragment实现了该Callback,都会导致该Activity无法被GC回收释放掉导致内存泄漏,最后导致OOM。

通过上面介绍,我们知道如果作为单例模式AppManager某个方法的入参,当方法使用之后,局部变量会自动释放,这种自动释放机制是Java语言本身就具有的。但是成员变量被释放的前提是AppManager实例对象被垃圾回收器回收后才释放,由于AppManager的实例对象是静态变量,所以必须在应用退出之后才会被释放,Context和Callback作为AppManager的成员变量,是我们在使用过程中自己设置的,如果想要在应用没有退出之前就让系统自动释放是不可能的,所以需要我们在使用后自己手动释放。在Java中将对象释放很简单,只需要赋值为null即可,这样一旦垃圾回收器执行GC时机会自动回收,所以为了在单例模式中不引起内存泄漏,只需要再添加一个clear()方法,在clear()方法中将Context或者Callback手动赋值为null,然后在Activity或者Fragment的onDestory()方法中调用一下AppManager.getInstance().clear()。

Handler引起的内存泄漏

在开发过程中Handler是最常用的用于处理子线程和主线程数据交互的工具。由于Android采用单线程UI模型,开发者无法在子线程处理UI,再者在Android4.0之后,网络请求不能放在主线程,只能在子线程请求,在子线程请求到的数据必须返回到主线程才可以用于更新UI,为此我们必须使用Handler工具将数据切换到主线程更新UI。

很多情况下开发者是直接采用下面的方式使用Handler。

这种直接采用内部类的方式处理Handler中的Message是最直接也是最简单的做法,但是这样会导致一个问题,非静态内部类会持有外部类的引用,更多内部类引起的内存泄漏在下面会继续讨论,如果Handler中发送了一个延时消息,这样会导致Activity无法被释放掉进而引起内存泄漏。

网上有许多讨论Handler引起内存泄漏的示例,被大家普遍接受的就是使用如下方式:

上述实现方式首先使用了一个静态内部类继承Handler,然后定义一个弱引用WeakReference,在构造方法中将Activity的Context存入弱引用中,这样在JVM执行GC的时候就会直接回收掉弱引用中持有的Activity。在handleMessage()方法中,我们只需要对Activity进行判空处理一下即可。如果想了解更多有关Handler导致内存泄漏,可以参看这篇文章Handler造成Activity泄漏,用弱引用真的有用么?

有些开发者可能有些疑问了,Handler作为Android系统刚开始时就使用的工具类,而且很多系统自带的类中都有Handler的足迹,Google不可能留给开发者这么一个大坑,每次使用Handler的时候还必须时刻想<着如果使用最简单直接的内部类方式就会导致内存泄漏。其实之所以Handler会引起内存泄漏,就是因为JVM在进行回收Activity的时候,会判断该Activity还有没有在被使用,而这时候如果Handler中还有Message未处理完成,就会导致该Activity不能及时被释放。那么如果在Activity或者Fragment生命周期的onDestroy()方法中可以将Handler没有执行完的消息清空,这样Activity就不会被引用了,垃圾回收器就可以在执行GC的时候回收Activity了,当然也就不会再发生内存泄漏的事情了。

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

非静态内部类会引起内存泄漏,当然了并不是说所有的都会引起内存泄漏,这里只是指在非静态内部类中处理比较耗时的操作容易导致内存泄漏,更多的可能涉及到异步操作。下面几个是比较常使用的用于处理异步任务的类,Handler上面已经介绍了就不再罗列出来了。

非静态内部类之所以容易引起内存泄漏是因为它持会隐式持有外部类的引用,更多内容可以参看Java”失效”的private修饰符。当我们在Activity或者Fragment中使用了上面几个比较耗时的类处理一些业务逻辑时,一旦跳过了当前页面,有可能异步任务还在执行中,这样就会导致Activity或者Fragment无法被垃圾回收器及时回收从而导致内存泄漏。

多线程并发在Android开发中是一定会涉及的,由于频繁的创建和销毁线程会大大的降低系统效率,再者就是线程本身也会占用内存,所以不建议直接使用Thread来频繁新建线程,而是使用线程池,线程池适当地调整池中工作线线程的数目,防止消耗过多的内存,也可以防止频繁的创建和销毁线程,从而提高系统运行效率。还有一点就是使用线程池可以方便的停止正在运行的线程,当界面用户已经看不到的时候可以直接调用线程池停止方法及时关闭异步任务。

Timer和AsyncTask使用方式类似,这两个类都有提供用于取消任务的cancel()方法,当不再使用的时候我们可以直接调用cancel()方法。如果说在界面不可见的时候还想在后台继续执行任务,那么这时候就建议直接新建一个单独的类或者使用静态内部类,这样就可以不必隐式持有Activity或者Fragment,当然了具体的情况还是要根据需求确定。

WebView引起的内存泄漏

现在安卓APP大多都用到了WebView+H5混合开发,在Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。即使WebView不加载任何内容,它本身也会占用很大的内存,在此基础上启动多个WebView占用的内存不会有太多变化,WebView的渲染类似一个单例模式View,但又不同于原生View,因为WebView在页面销毁之后内存仍然不会有明确减少。如下是两张截图,第一张是在Activity之上启动一个使用WebView的Activity,可以看出内存有明显的涨幅,第二张是关闭已经开启的WebView的Activity,内存虽有部分减少,但是跟没有加载WebView时相比微乎其微。

WebView_OOM01

WebView_OOM02

在网上也有许多讨论如何减少WebView导致的内存泄漏,有些建议不要在xml中设置WebView,而应该使用Java代码动态构建一个WebView,在新建WebView的时候传入getApplicationContext(),然后通过一个布局文件addView()将WebView添加进视图中。

layout = findViewById(R.id.layout);
webView = new WebView(getApplicationContext());
layout.addView(webView);

但是这种方式也不是很完美解决WebView内存泄漏的问题,如下是网上的有部分开发者遇到的问题:如果在WebView中打开链接或者你打开的页面带有flash,获得你的WebView想弹出一个dialog,都会导致从ApplicationContext到ActivityContext的强制类型转换错误,从而导致应用崩溃。这是因为在加载flash的时候,系统会首先把WebView作为父控件,然后在该控件上绘制flash,它想找一个Activity的Context来绘制他,但是你传入的是ApplicationContext。这种类型的问题我在开发过程中还没有遇到过,这应该也是确实存在的问题之一。

上面是介绍的是在构建的时候可以采取的优化错误,接下来在页面销毁的时候,可以在onDestroy()方法调用部分方法清空WebView的内容。

实际上上面的两幅截图对比,本身就已经采用的上面的优化方式防止WebView内存泄漏,可是通过截图也可以发现,采取的措施几乎没有起到作用。由于标准的WebView就存在内存泄露的问题,Google上面有专门讨论这个问题,更多内容可以参看WebView causes memory leak – leaks the parent Activity。所以通常根治这个问题的办法是为 WebView 开启另外一个进程,通过AIDL或者Messenger与主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

现在常用的QQ和微信就是采用的开启一个独立进程来处理WebView中的相关业务逻辑的。

图片引起的内存泄漏

在Android移动端开发中图片想来都是一个吃内存大户,Google在Android系统的每一次升级中也都试图减少由Bitmap导致的内存溢出。如下是Google官方文档的说明:

On Android 2.3.3 (API level 10) and lower, the backing pixel data for a Bitmap is stored in native memory. It is separate from the Bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. From Android 3.0 (API level 11) through Android 7.1 (API level 25), the pixel data is stored on the Dalvik heap along with the associated Bitmap. In Android 8.0 (API level 26), and higher, the Bitmap pixel data is stored in the native heap.

在Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中,并且生命周期不太可控,可能需要用户自己调用recycle()方法回收,在回收之前必须清楚的确定Bitmap已不再使用了,如果调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现”Canvas: trying to use a recycled bitmap”错误。3.0-7.1之间,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做,更多可以参看Google官方给的一个图片处理Demoandroid-DisplayingBitmaps

在8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,8.0之后图像资源的管理更加优秀,极大降低了OOM。在Android2.3.3以及以前的版本中Bitmap的像素信息也是存储的Native Memory中,这样虽然可以增加了对手机本身内存的利用率,可是JVM的垃圾收集器并不能回收Native分配的内存,需要开发人员手动调用recycle()方法回收图片内存,而且容易出问题。但是在Android 8.0引入的一种辅助自动回收native内存的一种机制NativeAllocationRegistry,它可以辅助回收Java对象所申请的native内存。

已经读取到内存中Bitmap,如何获取它们在内存中占用的内存大小呢。

通过这个方法可以发现,同样尺寸的图片在内存中占用的大小跟图片本身的大小没有关系。将相同尺寸的图片放在同一个屏幕密度的手机上面,即使一张图片大小是1M,另一张图片大小是100K,但是在内存中占用的大小仍然是相同。如果一张图片尺寸是720*1280,大小是61.37kb,如果放在手机分辨率也是720*1280的drawable-xhdpi,那么该图片占用的内存大小是多小呢?我们使用BitmapFactory的decodeResource(Resources res, int id)方法来加载图片,实际上加载在内存中大小是3600kb大小,读取的Bitmap的尺寸刚好和原图尺寸一致也是720*1280,但是内存却几乎是原图片占硬盘大小的60倍。同样如果将图片放在drawable-hdpi,图片内存大小为6401kb,图片尺寸960*1707,原图只是现在在内存中图片宽高的0.75倍。

BitmapFactory在加载一个图片时占用内存大小跟两个参数有关系inDensity和inTargetDensity,由于设备的屏幕密度是320dpi,所以inTargetDensity是320,如果将图片放在drawable-xhdpi,inDensity也是320,但是如果放在drawable-hdpi,此时的inDensity变为了240,由于将一个图片显示在某个屏幕密度的设备上就要根据屏幕密度进行缩放,原来的宽高乘以所以系数,这个缩放系数即为inTargetDensity除以inDensity,所以上的输出值也就说的通了。

除了与放置图片的的资源目录如(drawable-hdpi、drawable-xhdpi)有关系,还跟Bitmap的像素格式有关系,在加载图片时如果不做任何设置默认像素格式是ARGB_8888,这样每像素占用4Byte,而 RGB565则是 2Byte,除此之外还有ARGB_4444和ALPHA_8,虽然ARGB_4444占用内存只有ARGB8888的一半,不过图片的失真比较严重已经被官方嫌弃。而ALPHA_8只有透明度,没有颜色值,对于在图片上设置遮盖的效果的是有很有用。因此如果设置的图片不设置透明度,RGB_565是个不错选择,既要设置透明度,对图片质量要求又高的化,那就用 ARGB_8888。

一般在开发中在处理图片加载使用的是第三方图片加载框架,只要可以熟练使用,基本上我们不必担心图引起的内存泄漏问题。但是如果在某些场景需要自己处理Bitmap,记住使用BitmapFactory的Option,大图片一定要使用inJustDecodeBounds进行缩放,因为设置inJustDecodeBounds的属性为true的时候,我们解码的时候不分配内存但是却可以知道图片的宽高。另外在设置inSampleSize时,建议设置的图片的宽高以不超过原图2倍宽高为宜。

系统管理类引起的内存泄漏

这里所指的一些类是指通过getSystemService()方法获取的类,一般情况下建议使用上文所说的getApplicationContext()获取,因为这些类是系统服务类,但是常用的LayoutInflater除外。因为这些常用的系统服务类都是单利模式的,如果在整个应用的生命周期之内都必须使用的话,使用getApplicationContext()也不必担心内存溢出。但是某些类是在某个页面或者某种特殊场景下才会用到,一旦使用过有可能系统并不会主动释放资源,很有可能导致内存泄漏。比如InputMethodManager在某种类型的手机上回出现内存泄漏,ClipboardManager以及SensorManager在使用之后可能忘记解绑了,都有会导致内存泄漏。

网上有关InputMethodManager引起内存泄漏的讨论也挺多的,有些开发者认为可以不必关心这点,因为InputMethodManager对象并不是完全归前一个Activity持有,只是暂时性的指向了它,InputMethodManager的对象是被整个APP循环的使用。另外,InputMethodManager是通过单例实现的,不会造成内存的叠加,如果使用了leakCancy一直检测出来可以直接屏蔽掉。但是尽管如此,InputMethodManager确实有内存泄漏的情况,如下给出了解决内存泄漏的代码,代码参考自InputMethodManager内存泄露现象及解决

如果某些特定页面使用了ClipboardManager以及SensorManager等系统服务类,一定要在页面销毁的时候注释解绑服务,防止造成不必要的内存泄漏。

动画引起的内存泄漏

动画的使用虽然可以提高用户体验,可是使用不当也非常容易造成内存溢出。这里主要介绍两种情况,一种是帧动画引起内存溢出,某些特效需要多张图片,并且单张图片尺寸有比较大,使用系统提供的方法很容易就会导致内存溢出。帧动画将图片全部读出来后按顺序设置给ImageView,利用视觉暂留效果实现了动画。一次拿出这么多图片,而系统都是以Bitmap位图形式读取的。而动画的播放是按顺序来的,大量Bitmap就排好队等待播放然后释放。在网上也给出了许多解决方式,出发点基本是开发人员自己写一个实现图片顺序循环加载的逻辑达到帧动画同样的效果。但是个人认为这里应该从设计方面考虑,动效设计必须考虑不同平台的流畅性可用性,不能影响到产品性能。

另外一种就是页面在用户不可见时没有及时停止动画导致内存泄漏。由于动画从开始到结束时需要一定的时间的,但是有可能用户还没等到动画执行结束就已经跳过该页面,这时候应该及时停止动画。如果动画是在View中定义的,在View不可见时注意停止动画,如果是在Activity中,注意在onDestroy()或者onStop()方法中停止动画。

其它

资源未关闭造成的内存泄漏。资源性对象比如Cursor、Stream、MediaRecorder、File文件等往往都用了一些缓冲。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。另外需要注意的一点就是,如果数据库频繁开启关闭连接,这样也很影响性能,而且容易导致StackOverFlowError。

集合中的元素在使用过之后要及时清理。如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。

Protocol buffers是由Google为序列化结构数据而设计的,一种语言无关,平台无关,具有良好的扩展性。类似XML,却比XML更加轻量,快速,简单。如果你需要为你的数据实现序列化与协议化,建议使用nano protobufs。关于更多细节,请参考protobuf readme的”Nano version”章节。

如果应用需要在后台使用service,除非它被触发并执行一个任务,否则其他时候Service都应该是停止状态。当你启动一个Service,系统会倾向为了保留这个Service而一直保留Service所在的进程。这使得进程的运行代价很高,因为系统没有办法把Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。建议使用 IntentService,它会在处理完交代给它的任务之后尽快结束自己。

除此之外,在使用BroadcastReceiver的时候要注意注销掉广播,应用内广播建议使用LocalBroadcastManager。ListView和GridView一定要注意复用convertView并结合ViewHolder。谨慎使用第三方库以及注解等等,本篇文章就介绍到这里,后续会再另起一篇博文介绍出现内存溢出OOM后如何使用工具检查内存泄漏。