Android的内存优化是性能优化中很重要的一部分,而避免OOM又是内存优化中比较核心的一点。本文是个人工作中的总结和参考了一些其他人的博客,主要用来记录资料复习。
1、java,android内存分配与回收机制;
2、android内存泄露常见原因与OOM;
3、内存分析工具MAT 和 studio Monitor
一、java , android内存分配与回收机制:
1、静态存储、栈区、堆区
Java/Android 程序运行时的内存分配有三种策略,分别是静态的,栈式的和堆式的,对应的三种存储策略使用的内存空间主要分别是静态存储区(方法区)、堆区和栈区:
静态存储区(方法区)
内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在,它主要是用来存放静态数据、全局 static 数据和常量;
栈区
在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限;
堆区
亦称为动态内存分配,Java/Android 程序在适当的时候使用 new 关键字申请所需要大小的对象内存,然后通过 GC 决定在不需要这块对象内存的时候回收它,但是由于我们的疏忽导致该对象在不需要继续使用的之后,GC 仍然没办法回收该内存区域,这就代表发生了内存泄漏。
2、堆区和栈区的区别:
在函数中定义的一些基本类型的变量和对象的引用变量(也就是局部变量的引用)都是在函数的栈内存分配的,当在一段代码块中定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被重新使用;堆内存用于存放所有由 new 创建的对象(内容包括该对象其中的所有成员变量)和数组,在堆中分配的内存是由 GC 来管理的,在堆中产生了一个对象或者数组后,还可以在栈中生成一个引用指向这个堆中对象的内存区域,以后就可以通过栈中这个引用变量来访问堆中的这个引用指向的对象或者数组。
3、 Dalvik到ART的进化
Android在4.4之前一直使用的Dalvik虚拟机作为App的运行VM的, 4.4中引入了ART作为开发者备选, 5.0起正式将ART作为默认VM了。其中 Dalvik采用的是JIT技术,在应用程序启动时,JIT通过进行连续的性能分析来优化程序代码的执行,,在程序运行的过程中,,Dalvik在不断的进行将字节码编译成机器码的工作。而ART 取自 Android RunTime. Android用其取代Dalvik,主要目的就是为了提升运行性能。所以,ART相比Dalvik有几个关键的提升:
引入AOT(ahead-of-time)预编译技术
在安装apk的过程中, ART会使用dex2oat程序所有的字节码预编译成了机器码. 应用程序运行过程中无需进行实时的编译工作, 只需要进行直接调用. 故而提高了应用程序的运行效率.
提高GC效率
由原来的两次GC暂停减少为一次.
以较少的GC时间回收最近分配的, 短命的对象.
提升GC工程学, 使并发GC更及时.
压缩GC, 以减少后台内存使用和内存碎片.
4、Dalvik分配内存的过程
Dalvik 虚拟机实现了一个 dvmAllocObject 函数,每当 Dalvik 虚拟机需要为对象分配内存时,就会调用函数 dvmAllocObject,例如,当 Dalvik 虚拟机的解释器遇到一个 new 指令时,它就会调用函数 dvmAllocObject;
函数 dvmAllocObject 调用函数 dvmMalloc 从 Java 堆中分配一块指定大小的内存给新创建的对象使用,如果分配成功,那么接下来就先使用宏 DVM_OBJECT_INIT 来初始化新创建对象的成员变量 clazz,使得新创建的对象可以与某个特定的类关联起来,接着再调用函数 dvmTrackAllocation 记录当前的内存分配信息,以便通知 DDMS。函数 dvmMalloc 返回的只是一块内存地址,这是没有类型的,但是由于每一个 Java 对象都是从 Object 类继承下来的,因此函数 dvmAllocObject 可以将获得的没有类型的内存块强制转换为一个 Object 对象;
dvmMalloc 函数接着调用到了另一个函数 tryMalloc ,真正执行内存分配操作的就是这个 tryMalloc 函数,dvmMalloc 函数操作如果分配内存成功,则记录当前线程成功分配的内存字节数和对象数等信息;否则的话,就记录当前线程失败分配的内存字节数和对象等信息,方便通过 DDMS 等工具对内存使用信息进行统计,同时会调用函数 throwOOME 抛出一个 OOM 异常;
5、Dalvik进行GC的过程
Google IO 2011 大会的图就很好的展示了Android 4.4 版本之下的回收策略
图中的每个圆节点代表对象的内存资源,箭头代表可达路径,当一个圆节点和 GC Roots 存在可达的路径时,表示当前它指向的内存资源正在被引用,虚拟机是无法对其进行回收的(图中的黄色节点);反过来,如果当前的圆节点和 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 的时候将其内存回收掉。具体点来说,Java/Android 的内存垃圾回收机制是从程序的主要运行对象(如静态对象/寄存器/栈上指向的内存对象等,对应上面的 GC Roots)开始检查调用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链组成无法回收的对象集合,而剩余其他的孤立对象(集)就作为垃圾被 GC 回收。GC 为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
6、ART 的GC过程
ART 为新创建对象分配内存的过程和 Dalvik VM 几乎是一样的,区别仅仅在于垃圾收集的方式和策略不一样。 ART 运行时为从 DEX 字节码翻译得到的 Native 代码提供的一个函数调用表中,有一个 pAllocObject 接口是用来分配对象的,当 ART 运行时以 Quick 模式运行在 ARM 体系结构时,上述提到的 pAllocObject 接口由函数 art_quick_alloc_object 来实现,art_quick_alloc_object 是一段汇编代码,最终经过一系列的调用之后最终会调用 ART 运行时内部的 Heap 对象的成员函数 AllocObject 在堆上分配对象(具体的过程:ART运行时为新创建对象分配内存的过程分析),其中要分配的大小保存在当前 Class 对象的成员变量 object_size_ 中。(具体参考博客:http://blog.csdn.net/self_study/article/details/61919483)
7、ART相对Dalivk的优势
在 Android 4.4 版本以及之后就使用了 ART 运行时,在安装的时候就将应用翻译成机器码执行,效率比起以前的 Dalvik 虚拟机更高,但是缺点就是安装之后的应用体积变大和安装的时间会变长,不过相对于优点来说,这点缺点不算什么。ART 运行时与 Dalvik 虚拟机一样,都使用了 Mark-Sweep 算法进行垃圾回收,因此它们的垃圾回收流程在总体上是一致的,但是 ART 运行时对堆的划分更加细致,因而在此基础上实现了更多样的回收策略。不同的策略有不同的回收力度,力度越大的回收策略每次回收的内存就越多,并且它们都有各自的使用情景,这样就可以使得每次执行 GC 时,可以最大限度地减少应用程序停顿。
二、内存泄露以及OOM原因及解决办法:
1、 内存泄露
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,容易出现内存抖动,从而引起性能问题(如图15所示)。
图15
最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况,更多关于LeakCanary的介绍,请看这里(中文使用说明)。另外也可以使用传统的MAT工具查找内存泄露,请参考这里(便捷的中文资料)。
1)注意Activity的泄漏
通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的Activity泄漏:
内部类引用导致Activity的泄漏
最典型的场景是Handler导致的Activity泄漏,如果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()方法,如图16所示:
图16 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对象的及时关闭。
2、避免OOM
减小对象的内存占用
避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。
1)使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。图8演示了HashMap的简要工作原理,相比起Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与value的自动装箱(autoboxing),并且避免了装箱后的解箱。
关于更多ArrayMap/SparseArray的讨论,请参考《Android性能优化典范(三)》的前三个段落。
2)避免在Android里面使用Enum
Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考《Android性能优化典范(三)》,所以请避免在Android里面使用到枚举。
3)减小Bitmap对象的内存占用
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重,通常来说有以下2个措施:
inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4)使用更小的图片
在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。
内存对象的重复利用
大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用的实现逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收(如图9所示)。
图9 对象池技术
在Android上面最常用的一个缓存算法是LRU(Least Recently Use),简要操作原理如图10所示。
图10 LRU简要操作原理
1)复用系统自带的资源
Android系统本身内置了很多的资源,比如字符串、颜色、图片、动画、样式以及简单布局等,这些资源都可以在应用程序中直接引用。这样做不仅能减少应用程序的自身负重,减小APK的大小,还可以在一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异、不符合需求的情况,还是需要应用程序自身内置进去。
2)注意在ListView/GridView等出现大量重复子组件的视图里对ConvertView的复用,如图11所示。
图11
3)Bitmap对象的复用
在ListView与GridView等显示大量图片的控件里,需要使用LRU的机制来缓存处理好的Bitmap,如图12所示。
图12
利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率(注:3.0以及4.4以后存在一些使用限制上的差异)。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小,如图13所示。
图13 利用inBitmap的高级特性提高Android在Bitmap分配与释放执行效率
使用inBitmap需要注意几个限制条件:
在SDK 11 -> 18之间,重用的Bitmap大小必须是一致的。例如给inBitmap赋值的图片大小为100-100,那么新申请的Bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小。
新申请的Bitmap与旧的Bitmap必须有相同的解码格式。例如大家都是8888的,如果前面的Bitmap是8888,那么就不能支持4444与565格式的Bitmap了。我们可以创建一个包含多种典型可重用Bitmap的对象池,这样后续的Bitmap创建都能够找到合适的“模板”去进行重用,如图14所示。
图14
另外,在2.x的系统上,尽管Bitmap是分配在Native层,但还是无法避免被计算到OOM的引用计数器里。这里提示一下,不少应用会通过反射vBitmapFactory.Options里面的inNativeAlloc来达到扩大使用内存的目的,但是如果大家都这么做,对系统整体会造成一定的负面影响,建议谨慎采纳。
4)避免在onDraw方法里面执行对象的创建
类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。
5)StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。
三、内存分析工具MAT 和 studio Monitor
(参考博客:https://www.jianshu.com/p/080473ae050b)
网友评论