Android性能优化-内存篇

本文主要有以下三部分内容:
第一部分:简单介绍开发者指南上内存相关的文章。
第二部分:总结移动App性能评测与优化内存篇相关内容。
第三部分:Android内存相关好文章

开发者指南内存篇

以下是官方文档内存篇相关内容:
管理应用内存
内存管理预览
调查 RAM 使用情况
使用 Memory Profiler 查看 Java 堆和内存分配
dumpsys meminfo

管理应用内存

主要内容有:
(1)监控可用内存及内存使用:在手机有内存压力时,系统会发广播进行提示,应用根据这些
信息对内存使用作出恰当的处理,调用getMemoryInfo()方法去查询当前设备的可用内存堆内存空间。
(2)从代码角度优化内存:节省使用Service,除非sercie要去执行一个任务,否则不应该一直在后台驻留,
使用Android框架提供的优化过的数据结构,如ArrayMap替代Haskmap等等;代码抽象会
代码严重的开销,所以尽量少使用代码抽象,使用nano protobufs进行序列化,避免内存抖动,因为内存抖动会
触发更多的GC,影响手机性能,
(3)移除内存敏感的资源和库:减少APK大小,在需要使用注解时,考虑使用Dragger,Dragger不会增加不必要内存使用。
谨慎使用外部库,我们可能只需要外部库一个很小的功能,如果引入外部库可能会带来更大的内存开销。

内存管理预览

主要内容有:
(1)垃圾回收:垃圾回收的两个目标,首先是找到不再使用的对象,释放不再使用的对象,
Android里面是一个三级Generation的内存模型,不同的Generation采用不同的垃圾回收方式,GC所占用的时间和它是哪一个Generation也有关系。
(2)共享内存:不同进程之间可以共享框架代码和系统资源,应用和屏幕合成者通过匿名共享内存共享surface数据。
通过共享内存可以节省内存资源。
(3)分配和释放内存:应用内存可以根据自己的需求不变变大,但是不能超多每个应用的最大限制。
(4)限制应用内存:如果应用申请的内存超过自己最大可使用内存,这时候就会有内存溢出,应用可以通过getMemoryClass()
方法获取应用最大可使用堆的大小。
(5)切换应用:当应用在前后台切换的时候,如果遇到内存紧张,系统会根据LRU缓存去杀掉部分进程,应用在后台时使用的
内存越小,就越不容易被杀掉,这样应用在切换到前台时更快。

调查 RAM 使用情况

即使您在开发过程中遵循了管理应用的内存的所有最佳做法,您仍然可能泄漏对象或引入其他内存错误。唯一能够确定您的应用尽可能少地使用内存的方法是,利用本文介绍的工具分析应用的内存使用情况。主要内容有:
(1)解读Dalvik、ART虚拟机GC日志,主要有GC原因、垃圾回收名称、释放大小,暂停时间等等;
(2)捕捉堆转储:堆转储是应用堆中所有对象的快照。堆转储以一种名称为 HPROF 的二进制格式存储,您可以将其上传到分析工具中。
应用的堆转储包含应用堆整体状态的相关信息,以便您能够跟踪在查看堆更新时发现的问题。
(3)查看堆更新:使用 Android Monitor 在您与应用交互时查看应用堆的实时更新。实时更新提供了为不同应用操作分配的内存量的相关信息。
您可以利用此信息确定是否任何操作占用了过多内存以及是否需要调整以减少占用的内存量。
(4)分析堆转储:堆转储使用与 Java HPROF 工具中类似但不相同的格式提供。Android 堆转储的主要区别是在 Zygote 进程中进行了大量的分配。
因为 Zygote 分配在所有应用进程之间分享,所以它们对您自己的堆分析影响不太大。
(5)跟踪内存分配:具体内容可参考: 使用 Memory Profiler 查看 Java 堆和内存分配 ,Android Profiler是测量应用性能好工具主要有以下内容: 使用 CPU Profiler 检查 CPU Activity 和函数跟踪
使用 Memory Profiler 查看 Java 堆和内存分配
利用 Network Profiler 检查网络流量
(6)查看整体内存分配:使用 adb shell dumpsys meminfo <package_name|pid> [-d] 命令观察应用内存在不同类型的 RAM 分配之间的划分情况,-d 标志会打印与 Dalvik 和 ART 内存使用情况相关的更多信息,输出列出了应用的所有当前分配,单位为千字节。我们应该熟悉不同内存类型的分配,详细内容请阅读官方文档。
(7)触发内存泄漏:内存泄露越小,就需要运行更长的时间发现泄露点,可以通过横竖屏切换,应用切换来触发内存泄露。

使用 Memory Profiler 查看 Java 堆和内存分配

主要内容有:为什么分析应用内存、Memory Profiler概览、如何计算内存、查看内存分配、捕获堆转储、将对转储另存为Hprof、分析内存技巧等,具体内容请直接参阅官方文档。

dumpsys meminfo

dumpsys meminfo具体内容请阅读 dumpsys,该内容和调查 RAM 使用情况中的查看整体内存分配部分基本一致。
以上就是官方文档关于内存部分的简单介绍,更具体的内容请阅读官方文档。

移动App性能评测与优化内存篇相关内容

主要将自己认为重要的内容记录下来,部分内容会根据最新的Android版本加入一些自己的理解。虽然Android一直在不停的演进,书中描述的部分问题在最新Android手机上已经不存在,但是书中描述的一些分析方法和经验还是很值得学习的。

MAT工具使用技巧

MAT打开hprof文件之后,使用Top Consumers和Component Report功能,使用这些功能能快速定位大块内存消耗,由于虚拟机不会区分系统资料和应用自身的对象,可以采用两种方式来区分系统框架资源和应用自身对象
方法一:hprof-conv转换时添加“-z”参数;
方法二:hprof已经转换过了,在数据中寻找应用的Application类对象,可以使用OQL语句查询应用自身对象;
使用-z和OQL查询语句得到的对象集合就是应用代码分配的部分。这样就剔除了系统资源的影响。

Dilvik Heap常见问题及相关分析工具

(1)功能反复执行,Heap一直在持续增长,这种情况通常出现内存泄露,适合用LeakCanary等泄露工具进行白盒测试分析;
(2)代码执行时出现频繁的GC,Heap Alloc内存大幅波动,通常是分配了许多临时变量和数组,虽然又被回收,适合使用Heap Viewer/Allocation Tracker等工具来查看具体分配的对象;
(3)每次启动应用之后,Heap内存相对以前版本稳定增长,可能是由于新功能机代码改动引入的固定内存增长,获取Heap Dump进行多版本使用前后对比来查找增长原因。
(4)Heap Alloc变化不大,但进程Dalvik Heap Pss内存明显增加,是由于分配了大量小对象造成内存碎片的原因。

新功能可能会分配几万到几十万字节的内存,实际增加的内存为2MB,但是Dalvik heap内存并没有增加太多(200kb),说明问题不在Dalvik里就能解决,需要我们进一步深挖,Heap内存并不是应用的全部,可以通过dumpsys查看应用整个进程的使用量,以及各部分的使用量,最后发现Dalvik heap Pss部分增加比较多。最常用观察进程内存的方法 adb shell dumpsys meminfo packge name| pid
上述描述的问题就是Dalvik heap pss内存增加了2M,Dalvik heap Alloc值增长了273kb,但Dalvik heap Free也能看出大部分增长的内存是处于空闲状态的。各种怪异的问题,常用的方法找不出原因,说明有更深层次的原因,Java代码的内存分配和释放是由虚拟机管理的,我们需要 通过虚拟机机制来探索内存增长的原因
Dalvik heap内部机制
(1)为什么DVM占用内存不释放,需要于都DVM内存分配代码(位于dalvik/vm/alloc下),目前ART虚拟机已经取代了DVM,具体代码位置没有看过。
(2)新建对象之后,由于要向对应的地址写入数据,内核开始真正分配该地址对应的4KB物理内存页面。代码在Alloc.cpp中。
(3)运行一段时间之后开始GC,GC时可能会进行trim,即将空闲的物理页面释放回系统,表现为private dirty/pss下降,相关代码在HeapSource.cpp中。
问题所在以及优化
(1)在了解DVM分配和释放内存的机制之后,根据dumpsys观察到的现象,猜测可能是页面利用率的问题,如在GC之后,大部分对象被释放,少部分留下来,导致整页的4KB内存可能只有一个小对象,但统计的时候是按4KB来计算。
(2)将MAT中的数据导出为csv格式,然后按也页面进行统计,可以查看也页面利用率统计结果图,利用率低的页面增加说明小对象碎片数量增加。
(3)取出步骤2中使用不满2KB的页面的内存块地址,重新导入MAT得到对象列表,基本可以看出那些对象造成了内存的碎片化。
(4)问题基本过程还原:生成对象过程需要很多临时变量,批量生成过程中还有空闲内存,虚拟机没有垃圾回收,完成后进行垃圾回收,清楚了所有的临时变量,留下碎片化内存,造成碎片化类似代码如下:

    private Object result[] = new Object[NUM];
    private Object test[] = new Object[NUM];
    void test(){
        for(int i = 0; i <NUM ; i ++ ){
            byte[] tmp = new byte[NUM];
            result[i] = new byte[NUM];
            test[i] = new byte[NUM];

执行以上代码之后通过MAT查看数组每个成员的内存地址,发现都是不连续的,这就到消耗很多的物理页面,增加Heap Free。造成例子中的问题(书中没有描述具体如何操作,我自己也没有尝试,感兴趣的可以试验一下,但是该问题在Android使用ART虚拟机之后就不会存在碎片化问题)。
(1)MAT是探索java堆并发现问题的好工具,能快速发现常见图片和大数组问题,但是MAT不是万能的,比如该问题隐藏在对象地址中。内存分配的最小单位是页面,大小通常为4KB;尽量不要在循环中创建很多临时变量,可以将大型循环拆开、分段、按需执行;
(2)在JVM中,虚拟机借助标记整理算法将散布的内存移动到一起,这样就不存在页面利用率的问题,但是在DVM由于使用Mark-Sweep标记清除算法,该算法不能移动对象,即没有内存整理,这样就导致了内存碎片问题,导致以上问题的产生。目前Android使用ART虚拟机取代DVM,ART使用了标记整理算法进行内存回收,所以使用ART虚拟机的Android系统就不存在内存碎片问题,DVM与ART虚拟机区别可参考JVM、DVM以及ART虚拟机简介

内存除了Dalvik Heap pss以外还有其它许多消耗内存的部分,对Dalvik heap pss优化后,可能会发现Delvik other和Mmap在内存中的比重加大,我们需要继续寻找办法对在其他部分内存进行优化,由于对这部分不熟悉,我们需要先去了解背后的原理,才能有针对性的去研究如何优化这部分内存。
从物理内存到应用,我们首先要了解系统的内存机制,搞清楚屋里内存如何被分配到各个进程,以及共享内存的机制,这些机制对内存优化有很大的帮助,根据Google提供的Android架构图可以看到Android是基于Linux内核的,因此底层内存分配和共享机制与Linux基本相同,由于Android是为移动设备设计的,Android扩充了许多内核机制和实现。对内存影响较大的是Ashmem和Binder机制,在Ashmem和COW机制基础上,Android进程最明显的内存特征是与zygote共享内存,为了加快启动速度及节约内存,Android应用进程都是由zygote fork出来,由于zygote已经载入完成的Dalvik虚拟机和Android应用框架的代码,fork出来的进程和zygote共享同一块内存,这样就节约了每个进程单独载入的时间和内存,应用进程只需要载人自己的Dalvik字节码及资料就可以运行。
一个运行的Android应用进程会包含以下几个部分:
(1)Dalvik虚拟机代码(共享内存)
(2)应用框架代码(共享内存)
(3)应用框架资源(共享内存)
(4)应用框架so库(共享内存)
(5)应用的代码(私有内存)
(6)应用的资源(私有内存)
(7)应用的so库(私有内存)
(8)堆内存、其它部分(共享/私有)。
通过dumpsys meminfo可以观察内存值,它将不同额内存消耗分类统计,通过阅读和分析dumpsys meminfo的代码(自己未阅读),可以了解Android是如何划分各部分内存的,知道dumpsys是如何统计各部分内存的。
Android底层预计Linux内核,进程内存信息和Linux一致,Dalvik heap之外的信息都能够从/proc/pid/smaps/中获取。我们可以通过 adb shell cat /proc/pid/smaps > smapsinfo.txt将smaps详细信息重定向到文本文件中进行查看。smaps中信息如下:
(1)/dev/ashmem/dalvik-heap和/dev/ashmem/dalvik-zygote归为Dalvik-heap;
(2)其它以/dev/ashmem/dilvik-开头的内存区域归为Dalvik-other
(3)文件的mmap按已知的几个扩展名分类
(4)其余归为Other mmap;
由于Android已使用ART虚拟机代替Dalvik虚拟机,暂时不知道最新的smaps信息如何对内存进行分类。
zygote内存共享机制
Pss进程实际使用的物理内存,是私有内存加上按比例分配的各进程共享内存得到的值,共享内存是zygote加载的Android框架部分,会被所有的进程分享,Dalvik pps内存=私有内存+共享内存/共享进程数,所以当一个进程结束后,它所占用的共享内存就会被其它使用,该共享库的进程所分担,所以一个进程结束,可能会导致其它进程的Pss内存增加。
优化Dex相关内存
随着代码功能的增加,代码复杂度也在不断变大,这时候会发现Dalvik heap和Dex mmap这两部分消耗的内存增大(占总内存比例变大),Dalvik other存放的是类的数据结构及关系,Dex mmap是类函数代码和常量,通常优化这部分内存,需要从代码出发,但如果我们深入理解系统,也能够找到其它方法来降低这部分的内存消耗,所以我们在优化内存时,不应该只优化堆内存,在我们搞定其它类型内存的含义以及原理之后,也是能够对其它部分的内存进行优化。虽然最新的Android版本Dalvik Other占用的内存虽然不大,但是这给优化内存提供了一种思路。简单一段代码在一个空应用执行以后,可以看到对应heap、other、dex mmap的内存增长,heap增长可以通过代码逻辑分析出来这段代码需要分配多少,也可以在mat中看到新建对象消耗的内存,当应用使用完新创建的对象后,就会将heap内存释放,但是other和dex mmap不会被释放。
一个类的内存消耗以及new一个对象的步骤
虚拟机在执行这步时会做什么那?
第一步是loadClass操作,将类信息从dex文件加载到内存中;
(1)读取.dex mmap中的class对应的数据;
(2)分配native-heap和dalvik-heap内存创建class对象;
(3)分配dalvik-linearAlloc存放class数据;
(4)分配dalvik-aux-structure存放class数据;
第二步:new instance操作,创建对象实例:
(1)执行dex mmap中的<clinit>和<init>代码;
(2)分配dalvik-heap创建class对象实例;
如果对象引用了其它类型,那还需要先按照同样的逻辑创建被引用的class,在创建一个类实例的每一步都需要消耗内存,可以大概计算一下new操作需要消耗的内存;根据虚拟机的代码能够得知class根据类成员和函数数目分配linearAlloc和aux-structure的多少,以及class本身及函数需要的字节数,我们再根据所以class总量进行平均计算得到一组数据:
第一步是loadClass操作,加载类信息;
(1).dex mmap:载入一个类需要先读取259字节的mmap
(2)dalvik-linearAlloc:在linearAlloc区域分配437字节,存放类的静态数据;
(3)dalvik-aux-structure:在aux区域分配88字节,存放各种指针
第二步:new instance操作,创建对象实例:
(1)dex mmap :为了执行类的构造函数,还需要读取252字节mmap
(2)dalvik-heap:根据类的具体内容而变化。
由于内存最小分配单位是页面,同时内存分配并不是连续分布,所以可能需要分配多个4KB页面。
Dex mmap在Android应用中作用是映射class.dex文件,Dilvik虚拟机需要从dex文件中加载类信息、字符串常量,需要在调用函数时直接从mmap内存中读取函数代码来执行,所以该部分内存是程序运行必不可少的。
.dex文件将所有的class里边所包含的信息全部整合在一个。可以使用Android SDK提供的dexdump工具来观察dex文件内容,假设代码里用到A1类后,还用到B1、C1、D1类,如果能在dex文件中将A1、B1、C1、D1类放在一起,虚拟机就只需要加载一个4KB的页面,可以减少内存使用。优化思路就是调整dex文件中数据的顺序,尽量将使用到的数据内容排列在一起。Proguard工具能够对类名进行修改,根据程序运行的逻辑将那些会互相调用的类改为同一个packag名,这样就可以使他们的数据排列在一起。
(1)优化内存时,不只有堆内存,还有其它许多类型的内存能够进行分析和优化;
(2)dex文件有很多优化空间,调整dex文件顺序,可以节约mmap的内存;
(3)引入sdk和调用新的系统API需要考虑成本,不成用的功能可能导致大量的内存消耗,这时可以考虑多进程方案,将影响内存的操作放入到临时进程执行。

内存主要组成
(1)native heap:Native代码分配的内存,虚拟机和Android框架本身也会分配;
(2)Dalvik heap:Java代码分配的对象;
(3)Dalvik Other:类的数据结构和索引;
(4)so mmap:Native代码和常量
(5)dex mmap:Java代码和常量;
内存工具
(1)Android Studio/Memory Monitor:观察Dalvik内存;
(2)dumpsys meminfo:观察整体内存;
(3)smaps:整体内存的详细组成;
(4)MAT:分析Java对并发现问题好工具;
经验总结
(1)内存分配的最小单位是页面,通常为4KB;
(2)碎片不仅仅是Dalvik内存,还包括各种mmap可能产生的内存碎片,在Android4.4引入ART虚拟机之后,ART虚拟机的垃圾收集器(MarkSweep+Semispace)会进行碎片整理,所以就不存在碎片问题;
性能优化
(1)尽量不要在循环中创建很多临时变量,可能会触发频繁的GC,导致内存抖动;
(2)性能优化不只有堆内存优化,其它类型的内存,我们需要先了解其原理之后,就可以有针对性进行分析和优化;

Android内存其它好文章