Android 内存优化之OOM

一、Android内存管理机制

Android系统的Dalvik虚拟机扮演了常规内存垃圾自动回收的角色,Android系统没有为内存提供交换区,它使用paging和memory-mapping的机制来管理内存,下面简要概述一些Android系统重要的内存管理基础概念。

1)内存共享

Android系统通过下面几种方式来实现内存共享:
》Android应用的进程都是从一个叫做Zygote的进程fork出来的。Zygote进程在系统启动,并载入通用的framework的代码与资源之后开始开启。为了启动一个新的进程,系统会fork Zygote进程生成一个新的进程,然后再新的进程中加载并运行应用程序的代码。这就使得大多数的RAM pages被用来分配给framework的代码,同时促使RAM资源能够在应用的所有进程之间进行共享。

》大多数static的数据被mmapped到一个进程。这不仅仅让同样的数据能够在进程之间共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code、app resource、so文件等。

》大多数情况下,Android通过显式的分配共享内存区域来实现动态RAM区域能够在不同进程之间进行共享的机制。比如:Windows Surface 在App与Screen Composition之间使用共享的内存,Cursor Buffers在ContentProvider与Clients之间共享内存。

2)分配与回收内存

》每一个进程的Dalvik Heap 都反映了使用内存占用的范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定上限。

》逻辑上讲的Heap Size和实际的物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及与其他进程共享的内存。

》Android系统并不会对Heap中空闲的内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发GC操作,从而腾出更多空闲的内存空间。在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory 的模型,最近分配的对象会存放在Young Generation区域。当这个对象在该区域停留的时间达到一定程度,它会被移动到Old Generation,最后累计一定的时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Genertion 区域的GC操作速度会比Old Generation 区域的GC操作速度更快。

这里写图片描述

每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当前对象总的大小临近这一级别内存区域的阈值时,就会触发GC操作,以便腾出空间来存放其他新的对象。

这里写图片描述

通常情况下,GC发生的时候,所有线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation 中每次GC操作时间最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Ganertion中对象的数量有关,遍历树结构查找20000个对象比起遍历50个对象自然要慢很多。

3)限制应用的内存

》为了整个系统的内存控制需要,Android系统为每一个应用程序都设置一个硬性的Dalvik Heap最大限制 阈值,这个阈值在不同设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引发OutOfMemory错误。

》ActivityManger.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法返回一个整数,表明应用的Heap Size阈值是多少MB。

4)应用切换操作

》Android系统并不会在用户切换的时候执行交换内存操作。Android会把那些不包含Foreground组件的应用进程放到LRU Cached中。例如,当用户开始启动一个应用时,系统会为它创建一个进程。但是当用户离开此应用,进程不会立即被销毁,而是被放到系统的Cached当中。如果用户后来再切换到这个应用,此进程就能够马上完整地恢复,从而实现应用的快速切换。

》如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此,当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。

》对于那些非foredground的进程,Android系统是如何判断Kill掉哪些进程的问题,请参考Processes and Threads。

二、OOM(Out Of Memory)

前面我们提到过getMemoryClass()方法可以得到Dalvik Heap 的阈值。简要地获取某个应用的内存占用情况可以参考下面的示例(更多内存查看的知识,可以参考Google官方教程: Investigating Your RAM Usage)

1)查看内存使用情况

通过命令行查看内存详细占用情况。

这里写图片描述

通过Android Studio的Memory Monitor查看内存中Dalvik Heap的实时变化。

这里写图片描述

这里写图片描述

这里写图片描述

2)发生OOM的条件

关于Native Heap、Dalvik Heap、PSS等内存管理机制比较复杂,这里就不展开详细描述。简单的说,通过不同的内存分配方式对不同的对象进行操作,会因为Android系统版本的差异而产生不同的行为,对Native Heap和Dalvik Heap以及OOM的判断条件都会有所影响。在2.X的系统上,我们常常可以看到Heap Size的total值,明显超过了通过getMemoryClass()获取到的阈值而不会发生OOM的情况。那么针对2.X与4.X的Android系统,到底如何判断会发生OOM呢?

》Android 2.X系统GC LOG的Dalvik allocated + external allocated+新分配的大小 >=getMemoryClass()值时就会发生OOM 。例如,假设有这么一段Dalvik输出的GC LOG:GC_FOR_MALLOC free 2K,13%free 32586K/37455K,exernal 8989K/10356K,paused 20ms,那么32586+8989+(新分配23975) =65550 > 64M时就会发生OOM。

》Android 4.X的系统废除了external的计数器,类似Bitmap的分配改到Dalvik的java Heap中申请。只要allocated+新分配的内存>= getMemoryClass()的时候就会发生OOM。

三、如何避免OOM

可以从四个方面着手,首先减小对象的内存占用,其次是内存对象的重复利用,然后是避免对象的内存泄漏,最后是内存使用策略优化。

减小对象的内存占用

避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。

1)使用更加轻量级的数据结构

例如,我们可以考虑使用ArrayMap/SparseMap而不是HashMap等传统数据结构。下图演示了HashMap的简要工作原理,相比起Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseMap更加高效,在于他们避免了对Key与value的自动装箱,并且也避免了装箱后的解箱。

这里写图片描述 HashMap简要工作原理

2)避免在Android里面使用Enum

Android官方培训课程提到过”Enum require more than twice as much memory as static constants.You should strictly avoid using enums on Android. “

3)减小Bitmap对象的内存占用

Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重通常有下面两个措施:
1.inSampleSize:缩放比列,在哪图片载入到内存之前,我们首先需要计算出一个合适的缩放比例,避免不必要的大图载入。
2.decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_444/ALPHA_8,存在很大差异。

4)使用更小的图片

在涉及给到资源图片时,我们需要特别留意这张图片是否存在压缩空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException,这个问题的根本原因是发生了OOM。

内存对象的重复利用

大多数对象的复用,最终实施的方案都是利用对象池的技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用实现的逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收。如下图所示
这里写图片描述

在Android上面常用的一个缓存算法是LRU(Least Recently Use),简要操作原理图如下

这里写图片描述

1)复用系统自带资源

Android系统本身内置了很多的资源,比如字符串、颜色、图片、动画、样式、以及简单布局等这些资源都可以在应用程序中直接引用。这样做减少应用程序的自身负重,减少APK的大小,还可以在一定程度上减少内存的开销,复用性更好。但是也要留意Android系统版本的差异性,对那些不同系统版本上表现存在很大差异、不符合要求的情况,还是需要应用程序自身内置进去。

2)注意在ListView/GridView等出现大量复用子组件的视图里对ConvertView的复用,如下图所示

这里写图片描述

3)Bitmap对象的复用

在Listiew与GridView等显示大量图片的控件里,需要使用LRU的机制来缓存处理好的Bitmap,如下图所示

这里写图片描述

》利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行率。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前的那张Bitmap在Heap中所占据的pixel data 内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅需要占用屏幕所能够显示的图片数量的内存大小。如下图所示

这里写图片描述

使用inBitmap需要注意几个限制条件:

》在SDK 11 ->18之间,重用的Bitmap大小必须一致。例如给inBitmap赋值的图片大小为100*100,那么新申请的Bitmap必须也为100*100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmao大小。

》新申请的Bitmap与旧的Bitmap必须有相同的解码格式。例如大家都是8888的,如果前面的Bitmap是888,那么就不能支持4444与565格式的Bitmap了。我们可以创建一个包含多种典型可重用Bitmao的对象池,这样后续的Bitmap创建就能够找到合适的”模板”去进行重用,如下图所示

这里写图片描述

另外,在2.X系统上,尽管Bitmap是分配在Native层,但还是无法避免被计算到OOM的引用计数器里。这里提示一下,不少应用会通过反射vBitmapFactory.Options里面的inNativeAlloc来达到扩大使用内存的目的,但是如果大家都这么做,对系统整体会造成一定的负面影响,建议谨慎采纳。

4)避免在onDraw方法里面执行对象的创建

类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的GC操作,甚至是内存抖动。

5)StringBuilder

在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来代替频繁的”+”。
避免对象内存的泄漏
内存对象泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,容易出现内存抖动,从而引起性能问题,如下图所示

这里写图片描述

最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄漏的情况,更多关于LeakCanary的介绍,请看https://github.com/square/leakcanary的介绍。

1)注意Activity的泄漏

通常来说,Actiivty的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致我们Activity的泄漏:

》内部类引用导致Activity泄漏

组典型的场景Handler导致内存泄漏,如果Handler中有延迟任务或者等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生内存泄漏。此时的引用关系关链是Looper->MessageQueue->Message->Handler->Activity.为了解决这个问题,可以在UI退出之前,执行remove Handler 消息队列中的消息与runnable对象。或者使用static+WeakReference的方式来达到断开Handler与Activity之间的存在引用关系的目的。

》Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏

内部类的泄漏不仅仅会发生在Activity上,其他内部类出现的地方也要特别留意!我们可以尽量考虑使用static的内部类,同时使用WeakReference的机制来避免因为相互引用而出现内存泄漏。

2)考虑使用Application Context代替Activity Context

对于大部分非必需要使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity Context,这样就可以避免不经意的Activity泄漏。

3)注意临时Bitmap的回收

虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的,例如,临时创建的某个相对比较大的Bitmap对象,在经过变换的到新的Bitmap对象之后,应该尽快收回原始的Bitmap,这样能够更快释放原始Bitmap占用的空间。

需要特别留意的是Bitmap中的createBitmap()方法,这个函数返回的是Bitmap有可能和source bitmap是同一个,在回收的时候,需要特别检查source bitmap与return bitmap的引用是否相同,再有在不等的情况下,才能够执行source bitmap的recycle方法。

4)注意监听器的注销

在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己动手add的listener,需要记得及时remove这个listener。

5)注意缓存容器中对象泄漏

有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对对象没有及时从容器中清除,也是有可能导致内存泄漏的,例如,针对2.3的系统,如果把drawable添加到缓存容器,因为drawable与view的强引用,很容易导致Activity发生泄漏。而从4.0开始,就不存在这个问题。决解这个问题,需要对2.3系统上的缓存drawable做特殊处理,处理引用解绑的问题,避免泄漏情况。

6)注意WebView的泄漏

Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在很大差异。更严重的是标准的WebView存在内存泄漏的问题。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁从而到达内存的完整释放。

7)注意Cursor对象是否关闭
在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值