性能优化-内存优化

如题所述

第1个回答  2022-06-15

虽然Android有有优秀的内存管理机制,内存释放有垃圾收集器(GC)来回收。但内存的不合理使用还是会造成一系列的性能问题,比如短时间分配大量内存对象、内存泄漏等问题。本篇讲述如何检测内存问题和解决,希望在内存优化方面能够提供一些帮助。

首先学习Android内存管理机制,了解系统如何分配和回收内存。

Java对象在虚拟机上运行有7个阶段,也就是对象的生命周期

注意:在创建对象后,在确定不再需要使用该对象时,使对象置空,这样更符合垃圾回收标准,比如Object = null,可以提供内存使用效率。

在Android系统中,堆实际上是一块匿名共享内存,Android虚拟机并没有直接管理这块匿名共享内存,而是把它封装成一个mSpace,由底层C库来管理。

为了整个系统的内存控制需要,在Android系统为每一个应用程序都设置一个硬性的Dalvik Heap Size最大限制阈值(视设备而定)。如果应用占用内存空间接近阈值时,再尝试分配内存很容易OOM。Android系统的内存堆被划分为不同的区块,根据对数据配置对类型分配不同的区域内存,垃圾回收时,也会根据这些配置执行不同的垃圾回收处理过程,并且每一个区块都有指定的单位大小。

Android Rumtime有两种虚拟机,Dalvik和ART,他们分配的内存区域块是不同的:

其中Image Alloc和Zygote Alloc在Zygote进程和应用程序进程之间共享,而Allocation Space是每个进程都独立拥有一份。但Image Space的对象只创建一次,而Zygote Space的对象需要在系统每次启动时,根据运行情况都重新创建一遍。

整个内存分为三个区域:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。

年轻代分为三个区,一个Eden区和两个Survivor区S0和S1(S0和S1只是为了好区分,两者实质一样,角色可互换)。

年老代存放的是上面年轻代复制过来的对象,也就是在年轻代还存活的对象并且区满了复制过来的。一般来说,年老点中的对象生命周期都比较长。

用于存放静态的类和方法,以及年老代移动过来的对象。持久代对垃圾回收没有显著影响。

内存对象的处理过程如下

回收机制

系统在Young Generation和Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。

详细内容可参考我另一篇 文章

Android系统中,GC有以下三种类型:

在GC过程中,任何其他在工作的线程(包括负责绘制的线程)都可能会被暂停,一旦GC消耗的时间超过16ms的阈值,就会出现丢帧。也就是说 频繁的GC会增加应用的卡顿

如果内存在某以阶段的峰值达到了内存空间的阈值,或者频繁地发生内存峰值(毛刺现象),刚好在这个峰值时,需要申请一块较大的内存,就会由于对 内存空间不足而导致OOM异常

内存泄漏是指应用已经不会再使用的内存对象,但垃圾回收时没有把这些辨认出来,不能及时地回收,仍然一直保留在内存中,占用了一定的空间,并且最终会到GC耗时最长的Old Generation,不释放给其他对象。

内存优化主要有以下几个意义:

Memory Monitor是一款使用非常简单的图形化工具,可以很好地监控系统或应用的内存使用情况。可以快速发现内存抖动、大内存分配,甚至由于GC导致的卡顿。

(AS3.0以上的Android Profiler)

Heap Viewer的主要功能是查看不同数据类型在内存中的使用情况。通过分析这些

Allocation Tracker可以分配跟踪记录应用程序的内存分配,并列出了他们的调用堆栈,可以查看所有对象内存分配的周期。

可以先用Memory Monitor或者Heap Viewer找到内存异常的场景,然后使用Allocation Tracker分析这个场景的内存使用情况。

GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots对可达性来判断是否需要回收。GC Roots是系统选择的对象根节点,对Heap进行遍历,没有被直接或间接遍历到的引用会被GC 回收,能遍历到的能被回收。这类在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小,这种现象在Android应用中称为内存泄漏。

MAT是一个快速、功能丰富的Java heap分析工具,可以帮助开发者定位导致内存泄漏的对象,以发现大的内存对象,然后解决内存泄漏并优化。

分析内存最常用的是Histogram和Dominator Tree两个视图

(具体使用自行搜索哈哈)

上例中静态实例mTestModule会一直持有该Activity的引用,导致Activity的内存资源不能正常回收。

如果setup(Context context)传入的是Activity的Context,使得Activity被一个单例持有,mAppSettings作为静态变量,生命周期大于Activity,产生内存泄漏。

LeakCanary是一个检测内存的开源类库,可以在发生内存泄漏时告警,并且生成leak trace分析泄漏位置,同时可以提供Dump文件。

LeakCanary.install(this)会安装一个Leaks的Apk,同时也启用一个ActivityRefWatcher,用于自动监控调用Activity.onDestroy()之后泄漏的对象。

默认情况下,只对Activity进行监控,如果需要对Fragment或Service等这类组件监控,可以在Fragment onDestroy方法中,或自定义组件的周期结束回调接口加入以下实现

仅仅依靠默认的处理方式,体验不是很好,可以自定义监控结果处理。

heapDump:堆内存文件,可以拿到完成的hprof文件

result:监控到内存的状态,如是否泄漏等

leakInfo:leak trace详细信息

根据业务需求,使用合适的引用类型

考虑上面的情况,在自动装箱转化时,都会产生一个新的对象,这些对象比基础数据类型要大,这样会产生更多内存和性能开销。(int只有4字节,而Integer对象有16字节)

HashMap是一个 散列链表 ,先HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头。为了减少hash冲突,会配置一个大的数组,从内存节省的角度是非常不理想的。为了解决这个问题,Android提供了一个替代容器ArrayMap。

ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组而不是一个大数组。其中一个数组记录对象Key Hash过后的顺序列表,另外一个数组按Key的顺序记录Key-Value值,根据Key数组的顺序,交织在一起。在获取某个value时,ArrayMap会计算输入Key转换后的hash值,然后使用二分查找法对Hash数组寻找到对应的index,然后通过这个index在另外一个数组中直接访问需要的键值对。如果在第二个数组键值对中的key和前面输入的查询key不一致,就认为发生了碰撞冲突。ArrayMap会以该key为中心点,分别上下展开,逐个对比查找,直到找到匹配的值。

ArrayMap中执行插入或删除时,性能比HashMap要差一点,但如果设计对象数少,比如1000以下,不用担心这个问题。用ArrayMap能节省内存。

枚举的优点是类型安全,可读性高,但是枚举的内存开销是直接定义常量的三倍以上。官方也提醒尽量避免使用枚举类型,同时提供注解的方式检测类型安全,目前提供了int和String两者类型注解方式:IntDef和StringDef。即使用“常量定义+注解”替代枚举。

使用IntDef和StringDef需要在Gradle引入依赖

Android设备上显示图片需要把图片解码成位图格式,占用的内存只和位图的质量和大小相关。下面介绍几种减少图片内存开销的方法:

系统默认位图格式是RGB_8888占用内存较高,一般用RGB_565或RGB_4444代替。
RGB_8888占32bit、GB_565和RGB_4444都是16bit、ALPHA_8占8bit

如果内存中的图片大于屏幕需显示图片的大小,这些高分辨率图片会导致性能问题。可以通过重置这些图片大小,让它们符合实际显示大小。Bitmap的inSampleSize属性能实现位图缩放功能。

可参考 郭霖博客

本文参考书籍《Android应用性能优化最佳实践》

相似回答