APP性能测试——内存占用
购买Android
手机时经常可以看到6G+128GB
,8G+256GB
这些版本配置,这里面的6G
,8G
表示是随机存取存储器(英语:Random Access Memory
,缩写:RAM
;也叫主存)是与CPU
直接交换数据的内部存储器。它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时资料存储介质。
内存管理
从操作系统的角度来说,内存就是一块数据存储区域,是可被操作系统调度的资源。在多任务(进程)的操作系统中,内存管理尤为重要,操作系统需要为每一个进程合理的分配内存资源。所以可以从操作系统对内存分配和回收两方面来理解内存管理机制。
- 分配机制:为每一个任务(进程)分配一个合理大小的内存块,保证每一个进程能够正常的运行,同时确保进程不会占用太多的内存。
- 回收机制:当系统内存不足的时候,需要有一个合理的回收再分配机制,以保证新的进程可以正常运行。
Android 内存管理
内存管理机制
Android
系统是基于Linux
内核开发的开源操作系统,而linux
系统的内存管理有其独特的动态存储管理机制。不过Android
系统对Linux
的内存管理机制进行了优化,Linux
系统会在进程活动停止后就结束该进程,而Android
把这些进程都保留在内存中,直到系统需要更多内存为止。
这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。
分配机制
Android
为每个进程分配内存的时候,采用了弹性的分配方式,也就是刚开始并不会一下分配很多内存给每个进程,而是给每一个进程分配一个“够用”的量。这个量是根据每一个设备实际的物理内存大小来决定的。
随着应用的运行,可能会发现当前的内存可能不够使用了,这时候Android
又会为每个进程分配一些额外的内存大小。但是这些额外的大小并不是随意的,也是有限度的,系统不可能为每一个App
分配无限大小的内存。
Android
系统的宗旨是最大限度的让更多的进程存活在内存中,因为这样的话,下一次用户再启动应用,不需要重新创建进程,只需要恢复已有的进程就可以了,减少了应用的启动时间,提高了用户体验。
回收机制
Android
对内存的使用方式是“尽最大限度的使用”,这一点继承了Linux
的优点。Android
会在内存中保存尽可能多的数据,即使有些进程不再使用了,但是它的数据还被存储在内存中,所以Android
现在不推荐显式的“退出”应用。
因为这样,当用户下次再启动应用的时候,只需要恢复当前进程就可以了,不需要重新创建进程,这样就可以减少应用的启动时间。只有当Android
系统发现内存不够使用,需要回收内存的时候,Android
系统就会需要杀死其他进程,来回收足够的内存。但是Android
也不是随便杀死一个进程,比如说一个正在与用户交互的进程,这种后果是可怕的。所以Android
会有限清理那些已经不再使用的进程,以保证最小的副作用。
内存分类
在Linux
里面,一个进程占用的内存有不同种说法,有四种形式:
VSS
-Virtual Set Size
虚拟耗用内存RSS
-Resident Set Size
实际使用物理内存PSS
-Proportional Set Size
按比例使用的物理内存USS
-Unique Set Size
进程独自占用的物理内存
VSS
VSS
是单个进程全部可访问的地址空间,其大小可能包括还尚未在内存中驻留的部分。对于确定单个进程实际内存使用大小,VSS
用处不大。
RSS
RSS
是单个进程实际占用的内存大小,RSS
不太准确的地方在于它包括该进程所使用共享库全部内存大小。对于一个共享库,可能被多个进程使用,实际该共享库只会被装入内存一次。
PSS
PSS
不同于RSS
,它只是按比例包含其所使用的共享库大小。PSS
相对于RSS
计算共享库内存大小是按比例的。
例如:3
个进程使用同一个占用30
个内存页的共享库。 对于三个进程中的任何一个,PSS
将只包括10
个内存页。PSS
是一个非常有用的数字,因为系统中全部进程以整体的方式被统计, 对于系统中的整体内存使用是比较准确的统计。
USS
USS
是单个进程私有的内存大小,即该进程独占的内存部分。USS
揭示了运行一个特定进程在的真实内存增量大小。如果进程终止,USS就是实际被返还给系统的内存大小。
说明:
- 一般来说内存占用大小有如下规律:
VSS >= RSS >= PSS >= USS
- 实际在统计查看某个进程内存占用情况的时候,看
PSS
是比较客观的
Android 内存测试
获取设备内存信息
在Linux
操作系统中,/proc
是一个位于内存中的伪文件系统(in-memory pseudo-file system
)。该目录下保存的不是真正的文件和目录,而是一些运行时信息,如系统内存、磁盘io、设备挂载信息和硬件配置信息等。
使用命令adb shell cat /proc/meminfo
查看设备的整体内存使用情况。
1 | adb shell cat /proc/meminfo |
部分参数含义如下:
MemTotal
: 表示可供系统支配的内存,系统从开机到加载完成,操作系统内核要保留一些内存,最后剩下可供系统支配的内存就是MemTotal
。MemFree
:表示系统尚未使用的内存。MemAvailable
: 应用程序可用内存大小。系统中有些内存虽然已被使用但是可以回收的,比如
cache
可以回收,所以MemFree
不能代表全部可用的内存,这部分可回收的内存加上MemFree
才是系统可用的内存,即:MemAvailable≈MemFree+系统回收内存,它是内核使用特定的算法计算出来的,是一个估计值。它与MemFree
的关键区别点在于,MemFree
是说的系统层面,MemAvailable
是说的应用程序层面。Cached
: 缓冲区内存大小。Buffers
: 缓存区内存大小。
获取应用内存占用信息
连接设备,使用命令adb shell procrank
来获取各个应用的VSS、RSS、PSS、USS
。
1 | λ adb shell procrank |
获取指定包的内存占用情况
我们可以使用adb
命令来测试指定进程包名的内存使用详细情况,命令格式如下:
1 | adb shell dumpsys meminfo [pkg or pid] |
命令执行之后如下所示
1 | λ adb shell dumpsys meminfo com.youku.phone |
重点关注参数
一般情况下横轴仅需关注Pss Total
和Private Dirty
列。Private Dirty
表示进程独占内存。
Native Heap
:Native
代码分配的内存。native
进程采用C/C++实现,不包含dalvik
实例的linux
进程,/system/bin/
目录下面的程序文件运行后都是以native
进程形式存在的.Dalvik Heap
:Java
对象分配的占据内存
其他参数
Dalvik Other
:类数据结构和索引占据内存。Stack
:栈内存Ashmem
:不以dalvik-
开头的内存区域,匿名共享内存用来提供共享内存通过分配一个多个进程可以共享的带名称的内存块。匿名共享内存(Anonymous Shared Memory-Ashmem
。Android
匿名共享内存是基于Linux共享内存的,都是在tmpfs
文件系统上新建文件,并将其映射到不同的进程空间,从而达到共享内存的目的,只是Android在Linux的基础上进行了改造,并借助Binder+fd
文件描述符实现了共享内存的传递。Other dev
:内部driver
占用的内存.so mmap
C库代码占用的内存.jar mmap
java文件代码占用的内存.apk mmap
apk代码占用的内存.ttf mmap
ttf文件代码占用的内存.dex mmap
dex
文件代码占用内存。类函数的代码和常量占用的内存,dex mmap
是映射classex.dex
文件,Dalvik
虚拟机从dex
文件加载类信息和字符串常量等。Dex
文件有索引区和Data
区Other mmap
其它文件占用的内存
自动化获取性能数据
前面我们使用adb
命令获取CPU,内存性能数据,但是如果想批量获取性能数据,使用命令一个个查询会非常的不方便,我们可以使用Python自动化代码来自动获取性能数据,代码实现如下:
1 |
|
执行完成之后可以在在本地生成的csv
文件查看到数据,然后生成图表即可。
- CPU数据
- 内存数据
内存泄漏
内存泄漏(Memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存。 其实说白了就是内存空间使用完毕之后未回收。内存泄漏会因为减少可用内存的数量从而降低设备的性能。
Android 内存泄漏测试可以在APP中集成LeakCanary进行测试。
Android内存泄漏原因
使用static变量引起的内存泄漏
因为static
变量的生命周期是在类加载时开始 类卸载时结束,也就是说static
变量是在程序进程死亡时才释放,如果在static
变量中引用了Activity
那么这个Activity
由于被引用,便会随static
变量的生命周期一样,一直无法被释放,造成内存泄漏。
一般解决办法:
想要避免context
相关的内存泄漏,需要注意以下几点:
- 不要对
activity
的context
长期引用(一个activity
的引用的生存周期应该和activity
的生命周期相同) - 如果可以的话,尽量使用关于
application
的context
来替代和activity
相关的context
- 如果一个
acitivity
的非静态内部类的生命周期不受控制,那么避免使用它;
线程引起的内存泄漏
在Java
中线程是垃圾回收机制的根源,也就是说,在运行系统中DVM
虚拟机总会使硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。
解决办法:
- 合理安排线程执行的时间,控制线程在
Activity
结束前结束。 - 将内部类改为静态内部类,并使用弱引用
WeakReference
来保存Activity
实例 因为弱引用只要GC
发现了就会回收它 ,因此可尽快回收。
Handler的使用造成的内存泄漏
由于在Handler
的使用中,handler
会发送message
对象到 MessageQueue
中 然后 Looper
会轮询MessageQueue
然后取出Message
执行,但是如果一个Message
长时间没被取出执行,那么由于 Message
中有Handler
的引用,而Handler
一般来说也是内部类对象,Message
引用Handler
,
Handler
引用Activity
这样 使得Activity
无法回收。或者说Handler
在Activity
退出时依然还有消息需要处理,那么这个Activity
就不会被回收。
解决办法:使用静态内部类+弱引用的方式
资源未被及时关闭造成的内存泄漏
比如一些Cursor
没有及时close
会保存有Activity
的引用,导致内存泄漏
解决办法:在onDestory
方法中及时close
即可
BitMap占用过多内存
Bitmap
的解析需要占用内存,但是内存只提供8M
的空间给BitMap
,如果图片过多,并且没有及时recycle bitmap
那么就会造成内存溢出。
解决办法:及时recycle
压缩图片之后加载图片。
iOS 内存
iOS内存管理机制
iOS
内存管理的基本思想就是引用计数,通过对象的引用计数来对内存对象的生命周期进行控制,主要有两种方式:
- MRR(manual retain-release),人工引用计数,对象的生成、销毁、引用计数的变化都是由开发人员来完成。
- ARC(Automatic Reference Counting),自动引用计数,只负责对象的生成,其他过程开发人员不再需要关心其销毁,使用方式类似于垃圾回收,但其实质还是引用计数。
引用计数
引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1
,当有一个新的指针指向这个对象时,我们将其引用计数加1
。
当某个指针不再指向这个对象时,我们将其引用计数减 1
,当对象的引用计数变为 0
时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。
ARC 下的内存管理问题
ARC 能够解决 iOS 开发中 90% 的内存管理问题,但是另外还有 10% 内存管理,是需要开发者自己处理的,这主要就是与底层 Core Foundation
对象交互的那部分,底层的 Core Foundation
对象由于不在ARC
的管理下,所以需要自己维护这些对象的引用计数。
对于 ARC
盲目依赖的 iOS研发人员,可能会出现如下问题:
- 过度使用
block
之后,无法解决循环引用问题。 - 遇到底层
Core Foundation
对象,需要自己手工管理它们的引用计数时容易产生内存泄漏。
循环引用(Reference Cycle)问题
如下图所示:对象 A 和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁与依赖于对象 A 的销毁,这样就造成了我们称之为循环引用(Reference Cycle
)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。
不止两对象存在循环引用问题,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现,从而造成内存泄漏。
主动断开循环引用
解决循环引用问题可以在合理的位置主动断开环中的一个引用,使得对象得以回收。如下图所示:
内存测试
Instruments内存分析
打开Instruments
然后选择 Leaks
进入主界面,选择测试设备和测试应用点击开始执行,底部菜单选择CallTree
(如下图),并在底部勾选hide System Libraries
隐藏系统库函数。
内存测试
首先点击顶部的leaks Checkes
然后点击底部Cycles & Roots
,就可以看到以图形方式显示出来的循环引用。这样我们就可以非常方便地找到循环引用的对象了。
延伸思考
为何iPhone设备内存小但是运行比内存更大的Android 设备更流畅?
在iOS中,应用切换到后台时其实是保留一张截屏然后关闭应用,后台的消息通知功能则通过苹果自身的服务来完成。因为后台应用是关闭状态,所以如果内存不够时可以将整个应用的状态从内存转移到手机存储中,下次打开应用时再从存储空间调回到内存。
除了某些应用必须使用后台的功能以外(例如音乐类应用在后台播放)他们都会在存储空间里乖乖坐好,内存可以完全为前台应用服务而不会被后台占用。得益于苹果采用的NVMe
闪存超快的顺序读写速度,内存和存储空间中的数据可以迅速地相互传输。
然而Android的后台应用们很多都是持续运行在内存中,为了保护自己不被系统关闭,他们还需要一直在你的后台搞事情,包括且不限于互相伤害。虽然技术上Android也可以实现类似iOS那样的后台机制,但现实情况很骨感。
参考资料
- https://www.runoob.com/ios/ios-memory.html
- https://blog.devtang.com/2016/07/30/ios-memory-management/
- https://blog.csdn.net/humiaor/article/details/80826096
- https://www.cnblogs.com/kekouwen/p/10403876.html
- https://juejin.im/entry/589ff0d22f301e00694acbb3
- https://www.jianshu.com/p/0afcf1b80dcf
- https://zhuanlan.zhihu.com/p/27176914
- https://blog.csdn.net/c_z_w/article/details/85336283
- https://zhuanlan.zhihu.com/p/49829766
- https://www.zhihu.com/question/276578129/answer/931014639
- https://zhuanlan.zhihu.com/p/49829766
- https://www.orchome.com/6746
- https://zhuanlan.zhihu.com/p/26923061
- https://blog.51cto.com/xujpxm/1961072