移动App 性能优化评测以及优化读书笔记1---降低待机内存
1.1 内存分析常用工具
1.Android studio 自带memery Profile 进行内存观察,这样可以看到内存的消耗量,如果存在明显的内存泄漏,随着功能的反复使用,内存会出现不断升高,即使出现GC也没法降下来.可以进行录制,保存hprof文件.
2.使用SDK自带的Android Device Monitor,观察Heap堆的内存变化,配合Cause GC 按钮进行观察页面的内存动态值,遇到可疑的内存泄漏进行保存hprof .
3.收集到内存数据采用eclipse Memory Analyzer Tool (MAT)来分析.
使用MAT分析的一些技巧:
-
通常先使用Android SDK 提供的hprof-conv 转换工具,把hprof 转换成为去除系统内存使用的内存,而只留下应用代码分配的内存数据来分析.(hprof-conv 工具在Android SDK/platformtools/)
hprof-conv -z <infile><outfile> -
可以使用OQL进行筛选 : select * from instanceof java.long.Object s where s.@objectAddress>1107296256

1.2规范测试流程以及常见问题
1.2.1测试流程
1.代码:
尽量保证内存测试代码是纯净版本,不应该附加多余的Log和调试组件的,把测试的代码全部关闭.因为这些代码可能会临时分配内存,引起更多的GC,导致应用出现运行缓慢,卡顿现象.
2.测试场景:
测试场景有两类:1)属于新增功能或改动某项功能,需要对该功能进行性能测试.
2)整体性能测试.
各类场景测试的 重点:
-
包含图片显示的界面
-
网络传输的大量数据
-
需要缓存数据的场景
场景转换成用例
-
多张图片的前台进程
-
多个场景来回切换
-
长时间运行进程的内存增长
1.1.2 Dalvik Heap的常见问题
常见问题一般有下面几种:
-
随着功能反复的执行,heap内存一直在持续增长.这种情况一般出现内存泄漏,此时最合适使用LeakCanary 等工具进行白盒测试kk
-
代码执行是出现频繁的GC,Heap Alloc 内存大幅度波动.这种情况通常是分配了许多临时变量或数组,随后有迅速回收.确定具体的 场景后可以使用Heap Viewer/Allocation Tracker查看具体分配情况.
-
每次启动应用,Heap内存相比以前版本稳定增长.一般由于App待机或者使用某项功能后,可能是由于新功能引入的固定内存增长. 可以采用HeapDump 后对比多版本或功能的前后对比,找到原因.
-
Heap Alloc变化不大,但是进程Dalvik Heap Pss(Proportional Set Size)内存明显增加.是由于分配了大量小对象找出内存碎片.
1.2.4新的问题特殊的情况
注意,Heap 内存并不是应用的全部,我们在设置或者其他管理工具看到的内存大小是整个应用的全部进行内存使用量.有可能是出现在Heap部分完全没有增长,而其他部分有增长情况.
adb shell dumpsys meminfo com.xiu.app

Andorid studio 工具查看的内存是HeapAlloc的大小,而从上图可以看出 HeapAlloc 和Dalvik Heap大小不一样,除了DalvikHeap 之外还有其他部分会消耗内存.
可以进行对各个部分的PPS 进行分析.
1.2.5 进一步挖掘Dalvik Heap内存
对于简单的问题无法回答内存递增的情况下,例如
“这个版本引入了一个简单的库,内存就涨了20M”
“这些代码只是初始化了几个对象,还没开始用呢”
“一行代码都没改,怎么会涨呢?”
通过分析,无法正常解答时需要更深入底层DVM (Dalvik Virtual Memory)机制
1.2.6 Dalvik Heap 内存机制
Dalvik 内存分配源码 dalvik/vm/alloc ,具体的内部机制:
-
DVM使用mmap 系统调用从系统分配大块内存作为java heap.根据机制,如果分类的内存尚未真正使用,就不计入privateDirty 和pss.一般处于shareDirty共享内存.
-
新键对象后,由于需要向地址写数据,一般是分配一个4KB的物理内存页面
-
在运行过程中会执行GC,有些可以回收,有些一直存在
-
在GC 过程中有可能会触发TrimHeap,即释放空闲内存表现为PrivateDirty/Pss下降.
页面利用率:
页面利用率问题(碎片问题)
由于内存分配的时候,开始会分配的比较满,经过GC后,大部分内存释放,但是有少部分留下来了.在这种情况下会产生问题,整页的4KB内存中可能只有一个小对象,但是在统计PrivateDirty/Pss是还是按4KB计算.主要原因是DVM在进行内存回收的时候是采用Mark-Sweep 方式而不是Compating GC,并没有对对象进行移动,造成内存碎片(内存空洞).
分析:
用MAT 导出csv 格式.
可以把对象按照内存地址(取高位地址&0xfffff000)分布,相同高位的对象处于相同的页面.可以得出柱状分布图.通过对比观察,如果发现 柱状短的数量多(说明页面利用率低的很多),这样就说明小对象碎片比较严重.
经验总结:
内存分配的最小单位是页面,通常4KB
对于开发人员,尽量不要在循环中创建很多临时变量.
可以将大型的循环拆散,分段或者按需执行.
1.4 进阶 内存原理
对于缓存的策略,不能简单粗暴的将所有的缓存一次生成,这样会导致大量的碎片,需要一种合适的策略来生成.
使用adb shell dumpsys meminfo com.xiu.app
总结规律如下:
通常大致了解到.Dalvik Other 和Mmap 和代码数量有关,对于越复杂的应用,这部分内存就越多.
现在需要了解背后的原理.
1.4.1 从物理内存到应用
由于linux的的底层内存分配和共享机制,android 也是符合这一原则.在Ashmem以及COW(Copy -On-Write)的机制基础上,Android进程最明显的内存特征是与zygote共享内存.因为Android进程是用zygote fork出来的,所以Android的虚拟机和Zygote进行共享,应用只需要载入自己的Dalvik字节码及资源既可以开始工作.
综上所述,一个在运行Android应用进程会包含以下几部分:
-
Dalvik虚拟机代码(共享内存)
-
应用框架的代码(共享内存)
-
应用框架的资源(共享内存)
-
应用框架的so库(共享内存)
-
应用代码(私有内存)
-
应用的资源(私有内存)
-
应用的so库(私有内存)
-
堆内存,其他部分(共享/私有内存)
下面是dumpsys meminfo工具的详细统计各部分内存值:
1.4.3 smaps
由于android 是基于linux 内核,进程内存信息和linux是一致,所以Dalvik heap之外的信息都可以在/proc/<pid>/smaps中取得.
用ps 命令可以查看 linux 系统下所有进程信息.

Annroid 中ps命令参数:-t -x,-p,-P,-c [pid|name]-t显示进程下的线程列表-x 显示进程耗费的用户时间和系统时间,单位s-P 显示调度策略,通过是bg or fg ,当获取失败将会un和er比之前打印的内容多出了一列PCY,表示进程的调度等级
-
image.png
PID:进程号 PPID:父进程号 VSIZE :进程的虚拟内存大小 RSS :进程分配到的屋里内存大小 WCHAN:程正在睡眠的内核函数名称;该函数的名称是从/root/system.map文件中获得的 NAME :进程名
通过dumpsys meminfo com.xiu.app

dumpsys 统计的各个内存块的pss,shared_dirty,private_dirty,按照以下的原则进行归类:
-
/dev/ashmem/dalvik-heap 和/dev/ashmem/dalvik-zygote ==>Dalvik Heap
-
其他以/dev/ashmem/dalvik- 开头的区域 ==> Dalvik Other
-
/dev/Ashmem 下所有不以dalvik- 开头的区域 ==>Ashmem
-
/dev下的其他内存区域 ==>Other dev
-
,如[stack],[malloc],Unknown等 ==>其他部分
Dalvik :
在上述我们直到Dalvik 包括Dalvik heap,和Dalvik other.
Dalvik heap : Dalvik-heap 和Dalvik-zygote .堆内存,所有的java对象实例都放在这里.
Dalvik Other :
*LinearAlloc
*Accounting
*Code_Cache
1.LinearAlloc 包括 dalvik-LinearAlloc.线性分配器,虚拟机存放载入类的函数信息,随着dex数量增加而增加,注明的 65535个函数的限制就是从这里来的.
- Accounting 包括 /dev/ashmem/dalvik-aux-structure,/dev/ashmem/dalvik-bitmap-2,/dev/ashmem/dalvik-card-table .这些主要是作为标记和指针表使用. Dalvik-aux-structure随类及方法数增大而增加,dalvik-bitmap 随着dalvik-heap 增大而增大.
3.Code_cache 包括/dev/ashmem/dalvik-jit-code-cache .jit编译代码后的缓存,随着代码复杂度增加而变大.
因为堆内部的内存分配往往是应用消耗内存最多的地方,所以有效的方法就是减少Dalvik-heap中的创建对象,减少代码量.
由于这部分的内存增长取决与代码复杂度,因此通常情况下没有简单直接的方法降低他们内存的消耗.
mmap
系统会将一些文件mmap到内存中,对各个文件进行mmap的时机以大小比较复杂,dex_mmap 是主要的内容.
1.4.3 zygote 共享内存机制
对于meminfo 输出的Heap Size/alloc /Free 部分的数值,这些数值是Dalvik 虚拟机统计的内存堆的使用量,但是这些值是如何对应到Pss内存中,以及Heap Alloc 和Heap Pss 相差不远,但是又不一样.他们之间的关系如下:
Heap Alloc统计是由虚拟机分配的所有应用实例的内存,所以会把应用从zygote共享的部分也算进去,于是Heap Alloc的值总是比实际物理内存使用值要大.
Pss表示进程的实际使用物理内存.是有私有内存+共享内存的按比例分到的值.
Dalvik Pss=Private dirty+(Shared Dirty/共享内存进程数)
1.4.4 多进程应用
当一个进程结束后他所占用的共享库内存将会被其他仍然使用的进程所分担,共享库消耗的物理内存并不会减少.实际上对所有共享使用了这个库的应用,Pss都会增加,因为分担的共享内存增加了.对于一般的应用只共享了zygote 进程的Android框架等基础部分,通常手机使用的进程树达到几十 至上百个.所以某个进程结束后,其他进程内存增加的情况并不明显.
但是对于多进程的应用来说,由于多进程之间会共享很多内容,包括代码,资源,so库 等,因此单个进程结束后对其他进程影响比较明显.
由此可见,在统计多进程应用内存优化时候需要综合考虑,以免优化了一个进程内存,却造成其他进程的内存增长.
1.5 案例: 优化dex 相关内存
Dalvik Mmap 和Dalvik Other会随着代码复杂度增加而增大.而这两部分的内存将接近总内存的一半.在对Dalvik Heap做了优化之后,继续研究这部分内存有何优化.
Dalvik Other存放的是类的数据结构和关系.而Dalvik mmap是类的函数代码和常亮.通常情况下,减少这部分内存,需要精简代码,精简无用代码,或者将功能插件化.如果深入理解系统还有其他的方法降低这部分内存消耗.
1.5.1 从class 对象说起
1.5.2一个类的内存消耗
首先在代码中使用一个类,例如一下代码:
Foo f=new Foo();
虚拟机在执行到这步时会做什么呢?
第一步: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实例
计算下new 操作需要消耗的内存:
1.5.3 dex mmap
dex mmap 在android的作用是映射classes.dex文件Dalvik虚拟机需要从dex文件中加载类信息\字符串常量,还需要在调用函数时候直接从mmap内存中读取函数代码来执行,所以该部分内存是程序运行必不可少的.
在一个实例,可以发现dex 的文件排列顺序导致内存有可能被浪费.原因是因为dex文件在生成是是按字母顺序排列的,由于4KB页面加载的原因,实际运行是会加载许多相邻的但不会被用到的数据.例如在代码中用了A1类,虚拟机需要加载包含A1类的数据页面,但由于A1只有1KB,那么加载的4KB页面中还会有A2,A3,A4类,总共占4KB内存.
假设我们的代码中在用到A1类后,还会用到B1,C1,D1 类,那么如果能在dex文件中将A1,B1,C1,D1的类放在一起,虚拟机只需要加载 一个4kB页面,不仅减少了内存使用,对程序运行的速度也有好处.因此调整的思路就是优化dex文件的数据的顺序,将能够用到的数据紧密排列在一起.