APP性能测试——内存占用

购买Android手机时经常可以看到6G+128GB,8G+256GB这些版本配置,这里面的6G8G表示是随机存取存储器(英语: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是比较客观的

vss_pss_rss_uss

Android 内存测试

获取设备内存信息

Linux操作系统中,/proc是一个位于内存中的伪文件系统(in-memory pseudo-file system)。该目录下保存的不是真正的文件和目录,而是一些运行时信息,如系统内存、磁盘io、设备挂载信息和硬件配置信息等。

使用命令adb shell cat /proc/meminfo查看设备的整体内存使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
λ 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命令来测试指定进程包名的内存使用详细情况,命令格式如下:

1
adb shell dumpsys meminfo [pkg or pid] 

命令执行之后如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
 λ 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 HeapNative代码分配的内存。native进程采用C/C++实现,不包含dalvik实例的linux进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的.
  • Dalvik HeapJava对象分配的占据内存

其他参数

  • Dalvik Other:类数据结构和索引占据内存。
  • Stack:栈内存
  • Ashmem:不以dalvik- 开头的内存区域,匿名共享内存用来提供共享内存通过分配一个多个进程可以共享的带名称的内存块。匿名共享内存(Anonymous Shared Memory-AshmemAndroid匿名共享内存是基于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

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文件查看到数据,然后生成图表即可。

  1. CPU数据

cpudata

  1. 内存数据

mem_datas

内存泄漏

内存泄漏(Memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存。 其实说白了就是内存空间使用完毕之后未回收。内存泄漏会因为减少可用内存的数量从而降低设备的性能。

Android 内存泄漏测试可以在APP中集成LeakCanary进行测试。

Android内存泄漏原因

使用static变量引起的内存泄漏

因为static变量的生命周期是在类加载时开始 类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中引用了Activity那么这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。

一般解决办法:
想要避免context相关的内存泄漏,需要注意以下几点:

  • 不要对activitycontext长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)
  • 如果可以的话,尽量使用关于applicationcontext来替代和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无法回收。或者说HandlerActivity退出时依然还有消息需要处理,那么这个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 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。

memory-ref-count

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)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。

memory-cycle-1

不止两对象存在循环引用问题,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现,从而造成内存泄漏。

主动断开循环引用

解决循环引用问题可以在合理的位置主动断开环中的一个引用,使得对象得以回收。如下图所示:

memory-cycle-3

内存测试

Instruments内存分析

打开Instruments 然后选择 Leaks进入主界面,选择测试设备和测试应用点击开始执行,底部菜单选择CallTree(如下图),并在底部勾选hide System Libraries隐藏系统库函数。

memeroy test

内存测试

首先点击顶部的leaks Checkes 然后点击底部Cycles & Roots,就可以看到以图形方式显示出来的循环引用。这样我们就可以非常方便地找到循环引用的对象了。

memory-leaks

延伸思考

为何iPhone设备内存小但是运行比内存更大的Android 设备更流畅?

在iOS中,应用切换到后台时其实是保留一张截屏然后关闭应用,后台的消息通知功能则通过苹果自身的服务来完成。因为后台应用是关闭状态,所以如果内存不够时可以将整个应用的状态从内存转移到手机存储中,下次打开应用时再从存储空间调回到内存。

除了某些应用必须使用后台的功能以外(例如音乐类应用在后台播放)他们都会在存储空间里乖乖坐好,内存可以完全为前台应用服务而不会被后台占用。得益于苹果采用的NVMe闪存超快的顺序读写速度,内存和存储空间中的数据可以迅速地相互传输。  

然而Android的后台应用们很多都是持续运行在内存中,为了保护自己不被系统关闭,他们还需要一直在你的后台搞事情,包括且不限于互相伤害。虽然技术上Android也可以实现类似iOS那样的后台机制,但现实情况很骨感。

Android_iOS_Memory

参考资料