APP性能测试—内存使用率
从操作系统的角度来说,内存就是一块数据存储区域,是可被操作系统调度的资源。在多任务(进程)的操作系统中,内存管理尤为重要,操作系统需要为每一个进程合理的分配内存资源。所以可以从操作系统对内存分配和回收两方面来理解内存管理机制。
分配机制:为每一个任务(进程)分配一个合理大小的内存块,保证每一个进程能够正常的运行,同时确保进程不会占用太多的内存。
回收机制:当系统内存不足的时候,需要有一个合理的回收再分配机制,以保证新的进程可以正常运行。
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
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
查看设备的整体内存使用情况。
adb shell cat /proc/meminfo
MemTotal: 3082716 kB
MemFree: 1804236 kB
MemAvailable: 2438240 kB
Buffers: 21552 kB
Cached: 752800 kB
SwapCached: 0 kB
Active: 825432 kB
Inactive: 344876 kB
Active(anon): 398388 kB
Inactive(anon): 47244 kB
Active(file): 427044 kB
Inactive(file): 297632 kB
Unevictable: 256 kB
Mlocked: 256 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 396248 kB
Mapped: 432708 kB
Shmem: 49696 kB
Slab: 64600 kB
SReclaimable: 26192 kB
SUnreclaim: 38408 kB
KernelStack: 14336 kB
PageTables: 19680 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1541356 kB
Committed_AS: 20566920 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
DirectMap4k: 18368 kB
DirectMap2M: 3127296 kB
部分参数含义如下:
MemTotal
: 表示可供系统支配的内存,系统从开机到加载完成,操作系统内核要保留一些内存,最后剩下可供系统支配的内存就是MemTotal
。MemFree
:表示系统尚未使用的内存。MemAvailable
:应用程序可用内存大小。
系统中有些内存虽然已被使用但是可以回收的,比如
cache
可以回收,所以MemFree
不能代表全部可用的内存,这部分可回收的内存加上MemFree
才是系统可用的内存,即:MemAvailable≈MemFree+系统回收内存,它是内核使用特定的算法计算出来的,是一个估计值。它与MemFree
的关键区别点在于,MemFree
是说的系统层面,MemAvailable
是说的应用程序层面。
Cached
: 缓冲区内存大小。Buffers
: 缓存区内存大小。
获取应用内存占用信息
连接设备,使用命令adb shell procrank
来获取各个应用的VSS、RSS、PSS、USS
。
λ adb shell procrank
PID Vss Rss Pss Uss cmdline
1730 2031892K 395740K 251241K 221764K com.hunantv.imgo.activity
4795 1653640K 261976K 140397K 119176K com.tal.kaoyan
694 975996K 187696K 85604K 76248K com.android.systemui
574 1308464K 178312K 77288K 67920K system_server
4915 1518732K 160100K 65918K 57488K com.tal.kaoyan:pushservice
806 929724K 142008K 44211K 36308K com.android.settings
292 1127072K 125732K 32235K 23864K zygote
1842 904940K 120480K 28330K 21852K com.android.launcher3
2588 1521168K 131268K 25032K 14032K com.hunantv.imgo.activity:QS
789 895800K 110356K 24897K 19572K com.android.phone
2378 1304108K 135616K 24510K 12020K com.hunantv.imgo.activity:pushcore
2318 1284692K 116828K 16963K 7500K com.hunantv.imgo.activity:pushservice
682 889216K 97264K 15347K 11120K com.android.inputmethod.latin
1278 874704K 94340K 14498K 10120K android.process.acore
4966 887688K 93944K 13354K 8856K com.android.packageinstaller
280 120504K 14336K 12390K 12352K /system/bin/local_opengl
4754 1282184K 105700K 11988K 4620K com.hunantv.imgo.activity:ww
986 874004K 83028K 10501K 7280K com.android.deskclock
4690 1199892K 76916K 9015K 6232K com.android.gallery3d
301 71636K 17780K 8707K 7900K media.extractor
1044 866616K 74928K 6947K 4128K com.genymotion.genyd
5078 872856K 78192K 6554K 2324K com.android.webview:webview_service
1032 867468K 73080K 6177K 3428K com.genymotion.systempatcher
4722 864988K 73320K 5588K 2788K com.genymotion.superuser
1656 864388K 74560K 5216K 2244K com.android.defcontainer
951 863916K 71788K 4867K 2056K android.ext.services
4706 863972K 70828K 4805K 2136K com.android.musicfx
302 78232K 14136K 4496K 3096K /system/bin/mediaserver
4737 863776K 69620K 4455K 1836K com.svox.pico
293 42380K 10328K 4098K 3708K /system/bin/audioserver
获取指定包的内存占用情况
我们可以使用adb
命令来测试指定进程包名的内存使用详细情况,命令格式如下:
adb shell dumpsys meminfo [pkg or pid]
命令执行之后如下所示
λ adb shell dumpsys meminfo com.youku.phone
Applications Memory Usage (in Kilobytes):
Uptime: 1253283 Realtime: 1253283
** MEMINFO in pid 2040 [com.youku.phone] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 165059 165008 0 0 244480 207583 36896
Dalvik Heap 83625 83428 0 0 95519 79135 16384
Dalvik Other 10853 10852 0 0
Stack 120 120 0 0
Ashmem 600 144 0 0
Other dev 53 0 28 0
.so mmap 58881 3056 48540 0
.jar mmap 6 0 4 0
.apk mmap 23139 124 14948 0
.ttf mmap 626 0 404 0
.dex mmap 81423 28 42536 0
.oat mmap 6730 0 1436 0
.art mmap 2821 2516 0 0
Other mmap 8932 12 6272 0
Unknown 49054 49052 0 0
TOTAL 491922 314340 114168 0 339999 286718 53280
App Summary
Pss(KB)
------
Java Heap: 85944
Native Heap: 165008
Code: 111076
Stack: 120
Graphics: 0
Private Other: 66360
System: 63414
TOTAL: 491922 TOTAL SWAP PSS: 0
Objects
Views: 2680 ViewRootImpl: 2
AppContexts: 18 Activities: 2
Assets: 16 AssetManagers: 4
Local Binders: 134 Proxy Binders: 47
Parcel memory: 62 Parcel count: 250
Death Recipients: 1 OpenSSL Sockets: 4
SQL
MEMORY_USED: 1425
PAGECACHE_OVERFLOW: 872 MALLOC_SIZE: 1120
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 32 14 0/17/1 /data/user/0/com.youku.phone/databases/video-download2.db
4 32 79 10/19/7 /data/user/0/com.youku.phone/databases/video-download2.db (1)
4 20 29 3/16/2 /data/user/0/com.youku.phone/databases/accs.db
4 36 97 3/18/4 /data/user/0/com.youku.phone/databases/ut-abtest-v1.db
4 308 18 1/15/2 /data/user/0/com.youku.phone/databases/data_cache.db
4 308 50 2/15/3 /data/user/0/com.youku.phone/databases/data_cache.db (2)
4 100 25 1/17/2 /data/user/0/com.youku.phone/databases/ut.db
Asset Allocations
zip:/data/app/com.youku.phone-1/base.apk:/assets/Trebuchet_MS_Bold.ttf: 7K
zip:/data/app/com.youku.phone-1/base.apk:/assets/yk_iconfont.ttf: 12K
zip:/data/app/com.youku.phone-1/base.apk:/assets/Trebuchet_MS_Bold.ttf: 7K
zip:/data/app/com.youku.phone-1/base.apk:/assets/Trebuchet_MS_Bold.ttf: 7K
zip:/data/app/com.youku.phone-1/base.apk:/assets/iconfont_detail_page.ttf: 2K
zip:/data/app/com.youku.phone-1/base.apk:/assets/Trebuchet_MS_Bold.ttf: 7K
zip:/data/app/com.youku.phone-1/base.apk:/assets/fonts/ykf_iconfont.ttf: 27K
zip:/data/app/com.youku.phone-1/base.apk:/assets/fonts/new_danmaku_iconfont.ttf: 3K
zip:/data/app/com.youku.phone-1/base.apk:/assets/player_icon_font/iconfont.ttf: 13K
重点关注参数
一般情况下横轴仅需关注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自动化代码来自动获取性能数据,代码实现如下:
import csv
import os
import time
class Monitoring(object):
def __init__(self, count,pkg):
self.pkg=pkg #包名
self.counter = count #统计次数
self.cpudata = [("timestamp", "cpustatus")] #cpu性能数据
self.memdata = [("timestamp", "memstatus")] #内存性能
def getCurrentTime(self):
'''获取当前的时间戳'''
currentTime = time.strftime("%H:%M:%S", time.localtime())
return currentTime
def getCurrentDate(self):
'''获取当前日期'''
datetime=time.strftime("%Y-%m-%d %H_%M_%S")
return datetime
def monitoring_cpu(self):
'''cpu监控'''
result = os.popen(" adb shell top -m 100 -n 1 -d 1 -s cpu | findstr " +str(self.pkg)) #获取cpu性能指标数据
res=result.readline().split(" ") #根据返回数据进行分割
print(res)
if res==['']: # 返回数据为空时处理
print('no data')
else:
cpuvalue=list(filter(None, res))[4] #获取cpu数据
currenttime = self.getCurrentTime()
print("current time is:"+currenttime)
print("cpu used is:" + cpuvalue)
self.cpudata.append([currenttime, cpuvalue])
def monitoring_memeroy(self):
'''获取内存数据'''
result = os.popen(" adb shell procrank | findstr " + str(self.pkg)) # 获取内存性能指标数据
res = result.readline().split(" ") # 根据返回数据进行分割
print(res)
mem_kb = list(filter(None, res))[3][:-1] #获取Pss值并去掉最后一个字符K
mem_mb=round((float(mem_kb) / 1024), 2) #转化为MB
currenttime = self.getCurrentTime()
print("current time is:" + currenttime)
print("memory used is:" + str(mem_mb))
self.memdata.append([currenttime, mem_mb])
def get_cpu_datas(self):
'''连续获取cpu性能数据'''
while self.counter > 0:
self.monitoring_cpu()
self.counter = self.counter - 1
time.sleep(2)
def get_memeroy_datas(self):
'''连续获取内存性能数据'''
while self.counter > 0:
self.monitoring_memeroy()
self.counter = self.counter - 1
time.sleep(3)
def SaveDataToCSV(self,data_type):
'''
存储性能测试数据
:param data_type:
:return:
'''
now=self.getCurrentDate()
if data_type=='cpu':
csvfile = open('./cpustatus_'+now+'.csv', 'w', encoding='utf8', newline='')
writer = csv.writer(csvfile)
writer.writerows(self.cpudata)
csvfile.close()
elif data_type=='mem':
csvfile = open('./memstatus_' + now + '.csv', 'w', encoding='utf8', newline='')
writer = csv.writer(csvfile)
writer.writerows(self.memdata)
csvfile.close()
else:
print('data_type error!')
if __name__ == '__main__':
m = Monitoring(20,'com.youku.phone')
m.get_cpu_datas()
m.SaveDataToCSV('cpu')
m.get_memeroy_datas()
m.SaveDataToCSV('mem')
执行完成之后可以在在本地生成的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那样的后台机制,但现实情况很骨感。