文章目录
  1. 1. 统计方法
    1. 1.1. dumpsys meminfo
      1. 1.1.1. Total RAM:
      2. 1.1.2. Free RAM:
      3. 1.1.3. Used RAM:
      4. 1.1.4. ZRAM:
      5. 1.1.5. Lost RAM:
      6. 1.1.6. Tuning:
      7. 1.1.7. dumpsys meminfo pid:
    2. 1.2. procrank
      1. 1.2.1. 进程列表:
      2. 1.2.2. ZRAM:
      3. 1.2.3. RAM:
    3. 1.3. /proc/pid/maps
    4. 1.4. /proc/pid/smaps
    5. 1.5. /proc/meminfo
    6. 1.6. PageMap
    7. 1.7. ion
    8. 1.8. Kernel used
    9. 1.9. Kernel reserved
    10. 1.10. MAT
    11. 1.11. malloc_debug
    12. 1.12. 工具小结
      1. 1.12.1. 内存总计
      2. 1.12.2. 应用内存分析
  2. 2. 优化思路
    1. 2.1. Kernel
      1. 2.1.1. Kernel used
      2. 2.1.2. Kernel reserved
      3. 2.1.3. 快速回收
    2. 2.2. Android
      1. 2.2.1. 删除无用模块
      2. 2.2.2. 精简模块内容
      3. 2.2.3. 资源瘦身
      4. 2.2.4. 虚拟机参数调优
      5. 2.2.5. lmk 参数调优
    3. 2.3. 多媒体
  3. 3. 参考资料

统计方法

做优化相关的工作,最重要的就是要有可以量化的指标。所以我们先要知道哪些可以衡量系统占用内存的方法和工具。结合自身工作的一些经验和网上的一些资料,下面先介绍一些查看内存占用情况的方法和工具:

dumpsys meminfo

dumpsys meminfo 是 Android 中最常用的查看内存的一个命令。它是 AMS(ActivityManagerService)中一个 binder 接口,具体源码见参考资料,它会显示当前的内存情况:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
Applications Memory Usage (in Kilobytes):
Uptime: 73832068 Realtime: 103320267
Total PSS by process:
50,992K: system (pid 2309)
41,119K: com.android.systemui (pid 3382)
24,075K: com.android.launcher3 (pid 3993 / activities)
9,564K: com.android.phone (pid 3734)
8,738K: com.android.calendar (pid 12956)
8,711K: com.android.deskclock (pid 12994)
8,196K: android.process.media (pid 4888)
8,195K: com.android.inputmethod.latin (pid 4161)
8,049K: com.google.android.permissioncontroller (pid 5523)
8,037K: mediaserver (pid 1985)
6,730K: media.swcodec (pid 1995)
6,621K: com.android.providers.calendar (pid 5453)
6,099K: com.android.email (pid 5328)
5,487K: com.android.settings (pid 5480)
5,216K: com.android.traceur (pid 5501)
5,088K: com.android.se (pid 4696)
5,050K: zygote (pid 1928)
5,040K: com.android.keychain (pid 6191)
5,001K: surfaceflinger (pid 1951)
4,895K: audioserver (pid 1948)
4,426K: media.extractor (pid 1983)
2,656K: cameraserver (pid 1976)
2,464K: media.codec (pid 1989)
2,320K: init (pid 1)
2,232K: android.hardware.audio@2.0-service (pid 1933)
2,166K: logd (pid 1773)
2,074K: android.hardware.graphics.composer@2.2-service (pid 1939)
1,858K: media.metrics (pid 1984)
1,756K: drmserver (pid 1977)
1,667K: vold (pid 1778)
1,645K: netd (pid 1929)
1,603K: rild (pid 1993)
1,578K: adbd (pid 8775)
1,567K: ueventd (pid 1400)
1,385K: idmap2d (pid 1978)
1,258K: android.hardware.graphics.allocator@2.0-service (pid 1938)
1,236K: init (pid 1397)
1,234K: installd (pid 1980)
1,210K: storaged (pid 1987)
1,171K: statsd (pid 1986)
1,126K: keystore (pid 1981)
1,115K: apexd (pid 1910)
1,011K: android.hardware.keymaster@4.0-service-aw (pid 1859)
1,010K: init (pid 1398)
978K: hwservicemanager (pid 1775)
954K: android.hardware.wifi@1.0-service-lazy (pid 3344)
928K: mediadrmserver (pid 1982)
923K: android.system.suspend@1.0-service (pid 1932)
904K: gatekeeperd (pid 1996)
889K: gpuservice (pid 1949)
877K: incidentd (pid 1979)
865K: android.hardware.sensors@2.0-service (pid 1945)
833K: android.hardware.configstore@1.1-service (pid 1936)
832K: android.hardware.health@2.0-service (pid 1940)
831K: android.hardware.cas@1.1-service (pid 1935)
829K: android.hardware.gatekeeper@1.0-service (pid 1937)
820K: android.hardware.bluetooth@1.0-service (pid 1934)
818K: android.hardware.power@1.0-service (pid 1943)
802K: android.hardware.usb@1.0-service (pid 1946)
801K: android.hardware.light@2.0-service (pid 1941)
789K: android.hardware.memtrack@1.0-service (pid 1942)
787K: android.hardware.boot@1.0-service (pid 1858)
775K: android.hardware.radio.config@1.1-service (pid 1944)
742K: wificond (pid 1988)
736K: awlogd (pid 2000)
725K: displayservice (pid 4587)
667K: crda.uevent (pid 1990)
642K: servicemanager (pid 1774)
629K: ip6tables-restore (pid 2044)
629K: dumpsys (pid 28152)
609K: lmkd (pid 1950)
590K: android.hidl.allocator@1.0-service (pid 1931)
577K: ashmemd (pid 1947)
574K: iptables-restore (pid 2043)
572K: sh (pid 13796)
561K: vndservicemanager (pid 1776)
559K: sh (pid 1801)
543K: tee_supplicant (pid 1860)
539K: radio_monitor (pid 1992)
519K: dom2reg (pid 1991)
489K: tombstoned (pid 1997)
485K: kmsgd (pid 2002)
Total PSS by OOM adjustment:
99,103K: Native
8,037K: mediaserver (pid 1985)
6,730K: media.swcodec (pid 1995)
5,050K: zygote (pid 1928)
5,001K: surfaceflinger (pid 1951)
4,895K: audioserver (pid 1948)
4,426K: media.extractor (pid 1983)
2,656K: cameraserver (pid 1976)
2,464K: media.codec (pid 1989)
2,320K: init (pid 1)
2,232K: android.hardware.audio@2.0-service (pid 1933)
2,166K: logd (pid 1773)
2,074K: android.hardware.graphics.composer@2.2-service (pid 1939)
1,858K: media.metrics (pid 1984)
1,756K: drmserver (pid 1977)
1,667K: vold (pid 1778)
1,645K: netd (pid 1929)
1,603K: rild (pid 1993)
1,578K: adbd (pid 8775)
1,567K: ueventd (pid 1400)
1,385K: idmap2d (pid 1978)
1,258K: android.hardware.graphics.allocator@2.0-service (pid 1938)
1,236K: init (pid 1397)
1,234K: installd (pid 1980)
1,210K: storaged (pid 1987)
1,171K: statsd (pid 1986)
1,126K: keystore (pid 1981)
1,115K: apexd (pid 1910)
1,011K: android.hardware.keymaster@4.0-service-aw (pid 1859)
1,010K: init (pid 1398)
978K: hwservicemanager (pid 1775)
954K: android.hardware.wifi@1.0-service-lazy (pid 3344)
928K: mediadrmserver (pid 1982)
923K: android.system.suspend@1.0-service (pid 1932)
904K: gatekeeperd (pid 1996)
889K: gpuservice (pid 1949)
877K: incidentd (pid 1979)
865K: android.hardware.sensors@2.0-service (pid 1945)
833K: android.hardware.configstore@1.1-service (pid 1936)
832K: android.hardware.health@2.0-service (pid 1940)
831K: android.hardware.cas@1.1-service (pid 1935)
829K: android.hardware.gatekeeper@1.0-service (pid 1937)
820K: android.hardware.bluetooth@1.0-service (pid 1934)
818K: android.hardware.power@1.0-service (pid 1943)
802K: android.hardware.usb@1.0-service (pid 1946)
801K: android.hardware.light@2.0-service (pid 1941)
789K: android.hardware.memtrack@1.0-service (pid 1942)
787K: android.hardware.boot@1.0-service (pid 1858)
775K: android.hardware.radio.config@1.1-service (pid 1944)
742K: wificond (pid 1988)
736K: awlogd (pid 2000)
725K: displayservice (pid 4587)
667K: crda.uevent (pid 1990)
642K: servicemanager (pid 1774)
629K: ip6tables-restore (pid 2044)
629K: dumpsys (pid 28152)
609K: lmkd (pid 1950)
590K: android.hidl.allocator@1.0-service (pid 1931)
577K: ashmemd (pid 1947)
574K: iptables-restore (pid 2043)
572K: sh (pid 13796)
561K: vndservicemanager (pid 1776)
559K: sh (pid 1801)
543K: tee_supplicant (pid 1860)
539K: radio_monitor (pid 1992)
519K: dom2reg (pid 1991)
489K: tombstoned (pid 1997)
485K: kmsgd (pid 2002)
50,992K: System
50,992K: system (pid 2309)
55,771K: Persistent
41,119K: com.android.systemui (pid 3382)
9,564K: com.android.phone (pid 3734)
5,088K: com.android.se (pid 4696)
24,075K: Foreground
24,075K: com.android.launcher3 (pid 3993 / activities)
8,195K: Perceptible
8,195K: com.android.inputmethod.latin (pid 4161)
62,157K: Cached
8,738K: com.android.calendar (pid 12956)
8,711K: com.android.deskclock (pid 12994)
8,196K: android.process.media (pid 4888)
8,049K: com.google.android.permissioncontroller (pid 5523)
6,621K: com.android.providers.calendar (pid 5453)
6,099K: com.android.email (pid 5328)
5,487K: com.android.settings (pid 5480)
5,216K: com.android.traceur (pid 5501)
5,040K: com.android.keychain (pid 6191)
Total PSS by category:
60,569K: .so mmap
37,173K: Native
23,108K: Dalvik
21,030K: .art mmap
20,667K: .jar mmap
20,112K: Unknown
15,498K: .dex mmap
13,589K: .oat mmap
11,040K: .apk mmap
5,781K: Other mmap
5,432K: Dalvik Other
1,680K: Stack
882K: Other dev
520K: Ashmem
512K: .ttf mmap
0K: Cursor
0K: Gfx dev
0K: EGL mtrack
0K: GL mtrack
0K: Other mtrack
Total RAM: 486,028K (status normal)
Free RAM: 168,297K ( 62,157K cached pss + 78,284K cached kernel + 27,856K free)
Used RAM: 292,956K ( 238,136K used pss + 54,820K kernel)
hmm: Lost RAM: 65,107K, totalPss: 300,293K, totalSwapPss: 62,700K
ZRAM: 22,368K physical used for 68,664K in swap ( 364,516K total swap)
Tuning: 128 (large 512), oom 97,812K, restore limit 32,604K (low-ram)

上面是一个 512M Android 10 设备中的 dumpsys meminfo 的输出。

Total RAM:

这个数值是当前系统的所有内存大小。它是读取 /proc/meminfo 节点中的 MemTotal 获取的(/proc/meminfo 这个节点的信息我们后面再说)。它一般并不等于机器的物理内存大小,例如这里物理内存是 512M。那是因为 linux kernel 都会有预留内存(reserved),这个后面再说。物理内存 - kernel reserved = 系统实际内存总和。

Free RAM:

这个数值是当前系统可用内存大小。它是后面括号内那3项之和:

    (1).cached pss: 是 Total PSS by OOM adjustment 里面 Cached 分类的总和。OOM score 是 Android 根据进程的类型给 lmk 打的分数,lmk 从高分开始回收进程。其中 Cached 的是 OOM 900 以上的,一般都是切到后台的进程,表示可以随时回收。Rss(resident set size)表示常驻内存的大小,但是由于不同的进程之间会共享内存,所以用 Rss 表示进程内存会偏大。而 Pss(Proportional Set Size)把共享内存的 Rss 进行了平均分摊,所以一般使用 Pss 来表示进程的内存大小。而这里把 OOM 900 以上的所有进程的 Pss 算做 free 内存,我认为并不准确。Android 的应该是认为 OOM 900 以上的进程能随时被 lmk 回收,所以认为这部分可以算作 free 内存。但是其实这些进程中包含进程的已经申请并且分配了的内存,所以这个算法我认为并不能很好的代表 free 内存。这也是我并不推荐参考这个 Free RAM 数值来衡量当前系统的可用内存的原因。

    (2).cached kernel: 是 /proc/meminfo 中下面几项计算出来的: Buffers + Cached + SReclaimable - Mapped。
        Buffers: 是 kernel 中块设备(block)申请的内存大小。
        Cached: 这部分是文件缓存,例如进程加载的 so,jar,ttf,dex 等资源。它们的原始数据都在磁盘中,在内存紧缺的时候可以很方便将占用的内存交换出来使用。
        SReclaimable: 是 Slab 中的可回收的部分。Slab 是 linux 中的一种内存分配机制。一般都是 kernel 模块使用的。在 Android 7 的时候 dumpsys meminfo 还是将所有的 Slab 计算到 Free RAM 中的,在 10 中改成可以回收的部分了。
        Mapped: 是 Cached 中已经被 mapped 的部分。Cached 中有一部分是被 unmapped 的,但是没有马上释放。可以理解 Mapped 就是进程中的一些文件资源已经被 load 进内存里面的。

    (3).free: 是 /proc/meminfo 下的 MemFree。

从上面的统计内容来看,和一般 linux 理解的 free RAM 并不一样,一般的 linux 认为 Free RAM = free + Cached。也就是说 Cached 在内存充足的情况,缓存了进程运行所需要的资源,能加快进程的运行(对于app启动速度尤为明显);当内存不足的情况,可以快速的交换出来。linux 认为这可以算作可用内存,所以 linux 的 Cached 一般都比较大,真正的 free 内存都很少。而 dumpsys meminfo 则认为 lmk 可以随时回收的 Pss 是可用内存,而 load 进内存的文件 Cached 则不算。以我目前的认知,我认为 free + Cached 代表 Free RAM 能有更好的体验。并且 google 的 cts 测试统计可用内存也是按 free + Cached 统计的,但是某些下游客户比较执着于 dumpsys meminfo 的 Free RAM 统计算法。

Used RAM:

这个数值是当前系统已经内存的大小。它也是后面括号内那2项之和:

    (1).used pss: 是 Total PSS by OOM adjustment 除了 Cached 分类之外的进程的 Pss 之和。
    (2).kernel: 分2部分:第一部分是 /proc/meminfo 中: Shmem + SUnreclaim+ PageTables + KernelStack:
        Shmem: Shared memory 即 kernel 中的共享内存,tmpfs 也会被统计为 Shmem。
        SUnreclaim: Slab 中的不可回收部分。
        PageTables: kernel 中用来转化虚拟地址和物理地址的。
        KernelStack: 每个用户线程都会在 kernel 有个内核栈(kernel stack)。kernel stack 虽然属于用户态线程,但是用户态无法访问,只有 syscall、异常等进入到内核态才会调用到。所以这部分内存是内核代码使用的。

第二部分是 VM_ALLOC_USED,它是 kernel 中的模块通过 vmalloc 函数申请的内存,通过统计 /proc/vmallocinfo 中 “pages=” 数值之和得到(注意这里统计的单位是页面,一般Android上跑的 linux 一个页面都是 4Kb)。

这部分,kernel 统计还算合理,used pss 把 OOM 900 以上的进程的 Pss 全部剔除(前面算作 Free RAM 了),理由还是和前面一样,我认为是不合理的。

ZRAM:

zram 是 linux 的一项内存压缩技术,Android 里面用来当 Swap 用。当配置开启了 Zram,AMS 会把一些优先级低线程标记为可以放入 Swap 分区(例如说一些 Cached 进程)。这个 Swap 是基于内存的(Zram 支持写回磁盘,但是Android没支持),线程数据放入 Swap 的时候,会进行压缩(可以配置压缩算法),然后把之前占用的内存就可以给其他线程使用了。当再次使用这个线程数据的时候,再从 Swap 解压换回正常内存。这样能在小内存设备上挤出更多的内存空间给前台进程使用。

    (1).22,368K physical used for: 是读取 /sys/block/zram0/mm_stat 统计的。mm_stat 一共有8个数值,含义可以参看参考资料里面的 kernel 官方文档。这里读取的是第三个数值,也就是 mem_used_total。这里代表是实际物理内存使用的大小(经过压缩算法)。
    (2).68,664K in swap: 是 /proc/meminfo 里面 SwapTotal - SwapFree。代表 swap 里存放实际内存的大小(解压之后的) 。
    (3).364,516K total swap: 是 /proc/meminfo 里面的 SwapTotal。可以由方案配置。A50 配置的 zram size 是整个内存的 75% (在方案下的 fstab.sun8iw15p1 里有配置),我截图的是 512M 的(整个内存是除去预留内存的实际可用内存),486,028k * 0.75 = 364, 521k,差不多。

上面整体来看,就是使用了 22M(22,368K)内存,压缩存放了 67M(68,664K)的数据,压缩比例大概是 67% 。

Lost RAM:

这一项其实没什么实际意义,它是: Total RAM - (totalPss - totalSwapPss) - free - cached kernel - kernel - zram total:

    (1).TotalPss - totalSwapPss: totalPss 就是上面所有进程 Pss 之和。但是为什么要减去 totalSwapPss。每个进程的 SwapPss 在 /proc/pid/smaps 里面可以统计到(smaps 里面每一段 vma 里面都有 SwapPss)。拿来举例的设备是开启了 ZRAM 的,dumpsys meminfo 统计进程的 Pss 是计算了 swapPss 在里面的。所以其实按真正内存使用来看,是要减去 SwapPss。
    (2).free: Free RAM 里面的 free
    (3).cached kernel: Free RAM 里面的 cached kernel
    (4).kerenl: Used RAM 里面的 kernel
    (5).zram total: ZRAM 里面的 physical used for

其实上面的公式可以简单化为: Lost RAM = Total RAM - Free RAM - Used RAM - zram physical used + totalSwapPss 。totalSwapPss 从统计原理来看,应该等于 ZRAM 项里面的 “in swap” ,但是计算了一下发现有点偏差。用 totalPss 去减 procrank 统计的 totalPss(procrank 进程的 pss 是直接通过 /proc/pid/map 来统计的,没有加 swapPss,所以比 dumpsys meminfo 的小一些)来计算 totalSwapPss 也还是有点偏差,有可能是前面2次命令的时间间隔带来的进程数据统计偏差,也有可能是本来这么算就有点偏差。不过用 dumpsys meminfo 的 totalPss - procrank 的 totalPss 的偏差会比较小。所以我偏向于用这种方式计算 totalSwapPss (当然也可以在自己在 AMS 的源码里面打 totalSwapTotal 打印出来,这样就没偏差了)。

Tuning:

这里的值都是一些属性配置的值:

    (1).128: dalvik.vm.heapgrowthlimit属性取值,单位为MB
    (2).large: dalvik.vm.heapsize属性取值,单位为MB
    (3).oom: ProcessList中 mOomMinFree 数组最后一个元素取值
    (4).restore limit: ProcessList中 mCachedRestoreLevel 变量取值,原生设计是 oom 的 1/3
    (5).low-ram: ro.config.low_ram=true 时判断显示 low-ram,ro.config.low_ram=false 且 config_avoidGfxAccel 为 false 时显示 high-end-gfx

dumpsys meminfo pid:

不加 pid 参数是统计整个系统所有进程的内存信息,如果后面接 pid 可以详细显示单个进程的内存信息。例如说下面这个是 dump systemui 的 pid(3382):

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
Applications Memory Usage (in Kilobytes):
Uptime: 73902242 Realtime: 103390441
** MEMINFO in pid 3382 [com.android.systemui] **
hmm: 10128 8676 1451, 10716 5358 5358
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 6570 6544 0 1859 10128 8676 1451
Dalvik Heap 5720 5720 0 87 10716 5358 5358
Dalvik Other 1480 1480 0 40
Stack 56 56 0 0
Ashmem 8 0 0 0
Other dev 37 0 36 0
.so mmap 4097 128 492 173
.jar mmap 2850 0 524 0
.apk mmap 4231 0 3828 0
.ttf mmap 340 0 252 0
.dex mmap 8027 16 7996 4
.oat mmap 2110 0 64 0
.art mmap 2077 1508 0 51
Other mmap 165 28 64 0
Unknown 1015 1008 0 176
TOTAL 41173 16488 13256 2390 20844 14034 6809
App Summary
Pss(KB)
------
Java Heap: 7228
Native Heap: 6544
Code: 13300
Stack: 56
Graphics: 0
Private Other: 2616
System: 11429
TOTAL: 41173 TOTAL SWAP PSS: 2390
Objects
Views: 606 ViewRootImpl: 4
AppContexts: 16 Activities: 0
Assets: 12 AssetManagers: 0
Local Binders: 180 Proxy Binders: 62
Parcel memory: 13 Parcel count: 55
Death Recipients: 4 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 0
PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0

你会发现就算只间隔了2个命令的时间,详细统计出来的 Pss 都会和整体统计的有些小差别。因为程序每时每刻都在运行变化,所有总会优点小差别。

单独 dump pid 和统计方法和整体 dump 是一样的,就是说整理 dump 里面所有进程的 Pss 的统计之和就是单独 dump 的方法统计出来的,只是整体的只是取了 Pss 而已。是通过统计 /proc/pid/smaps 里面的项目得到上面这些信息(统计核心代码在 android_os_Debug.cpp 里面的 android_os_Debug_getDirtyPagesPid() 和 load_maps())。详细说明见 /proc/pid/smaps(/proc/pid/maps)章节说明。

    (1). 我们先来说横着的列:
        Pss Total: /proc/pid/smaps vma 里面 Pss 之和
        Private Dirty: /proc/pid/smaps vma 里面 Private_Dirty 之和
        Private Clean: /proc/pid/smaps vma 里面 Private_Clean 之和
        SwapPss Dirty: /proc/pid/smaps vma 里面 SwapPss 之和(这是开了 Zram 的,如果没开就是 Swap 之和,同时显示会变为 Swap Dirty)
        Heap Size: Native Heap 是 mallinfo() 里面 usmblks 的值。mallinof() 这个是一个 glic 函数,用来获取当前 native heap 的信息,返回的是一个结构体,里面的字段可以 man mallinfo 查看。usmblks 是 heap 能申请的最大值。Dalvik Heap 是 java 类 Runtime 里面的 totalMemory() 函数获取的,是当前虚拟机的 heap 大小,这值会动态调整,初始大小和调整策略由 dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize 属性控制(这里不展开说)。
        Heap Alloc: Native Heap 是 mallinfo() 里面 uordblks 的值,表示当前 native heap 已经申请的内存大小。Dalvik Heap 是 Runtime 里面 totalMemory() - freeMemory() 的值。
        Heap Free: Native Heap 是 mallinfo() 里面 fordblks 的值,表示当前 native heap 空闲内存的大小。Dalvik Heap 是 Runtime 里面 freeMemory() 的值,表示虚拟机 heap 空闲内存大小。虚拟机 heap 的空闲内存小到一定程度会有一定程度的策略扩大 heap 的 total size,策略的阀值由上面说的属性控制。

​    (2). 接下来再说竖着的行:只有 Pss Total、Private Dirty、Private Clean、SwapPss Dirty 区分竖直的分类(Heap Size、Heap Alloc、Heap Free 只区分 Native Heap 和 Dalvik Heap 前面已经说过了)
        Natvie Heap: /proc/pid/smaps 中: 所有 [heap]、[anon:libc_malloc] 的 vma 中各自字段之和。例如说 Natvie Heap 的 Pss Total 就是所有的 [heap]、[anon:libc_malloc] 的 vma 中 Pss 之和。从 vma 的名字来看,应该是 natvie malloc 申请的内存。
        Dalvik Heap: /proc/pid/smaps 中所有以 [anon:dalvik-alloc space、[anon:dalvik-main space、[anon:dalvik-large object space、[anon:dalvik-free list large object space、[anon:dalvik-non moving space、[anon:dalvik-zygote space 开头的 vma 中各字段之和。从 vma 的名字来看,应该是 java 虚拟机申请的内存。
​        Dalvik Other: /proc/pid/smaps 中 vma 中一些以 anon:dalvik 开头的字段之和(大部分是除了上面 Dalvik Heap 之外的),有点多我就不一一列的,详细的可以自己去 android/framework/base/core/jni/android_os_Debug.cpp 里面的 load_maps() 函数里面看。这里看 vma 名字像是虚拟机的一些引用计数、字节码缓存之类的。
        Stack: /proc/pid/smaps 中所有以 [stack 开头的 vma 各字段之和。应该是栈申请的内存,一般也不是很大,几十k这样。
        Ashmem: /proc/pid/smaps 中所有以 /dev/ashmem 开头的 vma 各字段之和。应该是前面说的进程间的共享内存。
        Other dev: /proc/pid/smaps 中所有以 /dev 开头的 vma 各字段之和。应该是一些设备驱动映射的内存。
        .so mmap: /proc/pid/smaps 中所有以 .so 结尾的 vma 各字段之和。是 load so 映射的内存。
        .jar mmap: /proc/pid/smaps 中所有以 .jar 结尾的 vma 各字段之和。是 load jar 映射的内存。
        .apk mmap: /proc/pid/smaps 中所有以 .apk 结尾的 vma 各字段之和。是 load apk 映射的内存。
        .ttf mmap: /proc/pid/smaps 中所有以 .ttf 结尾的 vma 各字段之和。是 load ttf 映射的内存。
        .dex mmap: /proc/pid/smaps 中所有以 .odex、.dex、.vdex、结尾的 vma 各字段之和。应该是加载虚拟机字节码、预编译的字节码映射的内存。
        .oat mmap: /proc/pid/smaps 中所有以 .oat 结尾的 vma 各字段之和。应该是加载虚拟机预编译字节码映射的内存。好像是 system 的 jar dex2oat 出来的是 oat,其他的是 odex。
        .art mmap: /proc/pid/smaps 中所有以 .art、art] 结尾的 vma 各字段之和。这个好像是 odex 的一些索引,又叫启动镜像。也是虚拟机预编译出来的内容加载到内存里面所占用的。
        Other mmap: 除了上面分类, vma 中有名字的各字段之和。
        Unknown: 除了上面分类, vma 中没有名字的各字段之和。
        Total: 分别等于各自列上的项目之和,Pss Total 还需要额外加上 SwapPss Dirty 这一列的求和。

    (3).App Summary:
        Java Heap: Dalvik Heap 的 Private Dirty + .art mmap 的 (Private Dirty + Private Clean)。Android 认为这些代表进程的虚拟机占用的内存。
        Native Heap: Native Heap 中的 Private Dirty。Android 认为这个数值代表进程的 native 占用的内存。
        Code: .so mmap、.jar mmap、.apk mmap、ttf mmap、.dex mmap、oat mmap 的 (Private Dirty + Private Clean )”。Android 认为这些是一些资源类(例如字节码文件、图片、字体等)占用的内存。
        Stack: Stack 中 Private Dirty 的值。
        Graphcis: 这个是 GPU 里面 texture、buffer 占用的内存,需要 gpu 驱动支持才行。0 的话代表 gpu 驱动还没支持。
        Private Other: TOTAL(Private Dirty + Private Clean) - Java Heap - Native Heap - Code - Stack - Graphcis
        System: TOTAL(Pss) - TOTAL(Private Dirty + Private Clean)
        TOTAL: TOTAL(Pss)
        TOTAL SWAP PSS: TOTAL(SwapPss Dirty)

    (4).OBjects: 这些统计的是进程里面一些 Android Java 对象的引用信息,对于排查 Java 内存泄漏比较有用。这里不细说,其实看名字大概也能猜到是哪些对象。

    (5).SQL: 应该是 sql 申请的内存,占时没研究过,不过一般这部分占用内存都不高。

procrank

procrank 是一个 native 的 bin 小工具,在 shell 中敲命令执行(在 userdebug 中 root 才能显示全,user 下会因为没权限,只能显示 total 数值。因为 shell 组没权限去读 /proc/pid/maps )。它的显示内容如下:

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
PID Vss Rss Pss Uss Swap PSwap USwap ZSwap cmdline
2309 2052916K 85976K 46063K 38164K 8376K 5642K 5456K 1838K system_server
3382 1170900K 80940K 38878K 29684K 7176K 2519K 2196K 820K com.android.systemui
3993 1114268K 63652K 23538K 15212K 5920K 819K 464K 266K com.android.launcher3
3734 1058588K 33280K 8938K 6820K 6268K 857K 476K 279K com.android.phone
4161 1040096K 33340K 7546K 5324K 6372K 899K 512K 292K com.android.inputmethod.latin
1995 48688K 9988K 6725K 6612K 44K 44K 44K 14K media.swcodec
1985 75200K 13192K 6644K 5232K 1480K 1480K 1480K 482K /system/bin/mediaserver
4888 1031384K 32052K 5705K 3432K 8216K 2742K 2356K 893K android.process.media
5523 1032036K 31788K 5267K 2784K 8580K 3008K 2608K 980K com.google.android.permissioncontroller
12956 1028588K 37652K 5103K 1012K 9480K 3904K 3500K 1272K com.android.calendar
12994 1032108K 37636K 4783K 728K 9720K 4199K 3804K 1367K com.android.deskclock
1948 52824K 8820K 4349K 4084K 596K 596K 596K 194K /system/bin/audioserver
1983 33136K 10440K 4245K 3124K 252K 252K 252K 82K media.extractor
1928 1705140K 27260K 3612K 1712K 7984K 1706K 676K 556K zygote
1951 64340K 9012K 3532K 3032K 1516K 1516K 1516K 493K /system/bin/surfaceflinger
4696 1026632K 22872K 2998K 1672K 8564K 2308K 1280K 752K com.android.se
5453 1027204K 28024K 2759K 636K 9624K 4097K 3704K 1334K com.android.providers.calendar
1989 35368K 6640K 2482K 2308K 8K 8K 8K 2K media.codec
1933 27316K 5420K 2232K 2124K 24K 24K 24K 7K /vendor/bin/hw/android.hardware.audio@2.0-service
5480 1070120K 23592K 2013K 632K 9308K 3721K 3316K 1212K com.android.settings
5328 1035528K 24700K 1890K 388K 9980K 4467K 4076K 1455K com.android.email
1976 34248K 5968K 1881K 1724K 812K 812K 812K 264K /system/bin/cameraserver
1773 16328K 3964K 1859K 1824K 316K 316K 316K 102K /system/bin/logd
5501 1030772K 22184K 1844K 548K 9220K 3597K 3184K 1171K com.android.traceur
6191 1025652K 23060K 1827K 472K 9112K 3465K 3044K 1129K com.android.keychain
1939 50688K 5924K 1720K 1508K 388K 388K 388K 126K /vendor/bin/hw/android.hardware.graphics.composer@2.2-service
1 27940K 3340K 1595K 1236K 744K 744K 744K 242K /system/bin/init
1993 19328K 4680K 1593K 1444K 24K 24K 24K 7K /vendor/bin/hw/rild
1778 19408K 4404K 1511K 1436K 172K 172K 172K 56K /system/bin/vold
8775 27672K 3728K 1441K 1356K 124K 124K 124K 40K /system/bin/adbd
1929 30720K 4032K 1222K 1156K 436K 436K 436K 142K /system/bin/netd
1910 15576K 2476K 1108K 940K 16K 16K 16K 5K /system/bin/apexd
1987 17048K 4000K 1065K 980K 160K 160K 160K 52K /system/bin/storaged
1986 18768K 4032K 1025K 916K 160K 160K 160K 52K /system/bin/statsd
1981 15684K 3932K 1024K 948K 116K 116K 116K 37K /system/bin/keystore
1980 18456K 3588K 1009K 944K 240K 240K 240K 78K /system/bin/installd
1859 12740K 3960K 999K 932K 24K 24K 24K 7K /vendor/bin/hw/android.hardware.keymaster@4.0-service-aw
1400 11880K 2568K 980K 680K 588K 588K 588K 191K /system/bin/ueventd
3344 13124K 3920K 967K 888K 0K 0K 0K 0K /vendor/bin/hw/android.hardware.wifi@1.0-service-lazy
30353 18348K 2972K 950K 912K 0K 0K 0K 0K procrank
1984 22596K 5052K 946K 776K 948K 948K 948K 308K media.metrics
1996 14000K 3680K 897K 824K 20K 20K 20K 6K /system/bin/gatekeeperd
1979 13100K 3632K 861K 800K 28K 28K 28K 9K /system/bin/incidentd
1936 12296K 3836K 839K 752K 8K 8K 8K 2K /vendor/bin/hw/android.hardware.configstore@1.1-service
1945 12740K 3820K 838K 740K 40K 40K 40K 13K /vendor/bin/hw/android.hardware.sensors@2.0-service
1938 16316K 4896K 834K 696K 452K 452K 452K 147K /vendor/bin/hw/android.hardware.graphics.allocator@2.0-service
1934 13172K 3824K 833K 760K 0K 0K 0K 0K /vendor/bin/hw/android.hardware.bluetooth@1.0-service
1937 11064K 3772K 809K 744K 32K 32K 32K 10K /vendor/bin/hw/android.hardware.gatekeeper@1.0-service
1935 12912K 3940K 807K 712K 40K 40K 40K 13K /vendor/bin/hw/android.hardware.cas@1.1-service
1944 12500K 3804K 787K 676K 0K 0K 0K 0K /vendor/bin/hw/android.hardware.radio.config@1.1-service
1943 11164K 3816K 780K 704K 52K 52K 52K 16K /vendor/bin/hw/android.hardware.power@1.0-service
1946 12088K 3688K 770K 708K 44K 44K 44K 14K /vendor/bin/hw/android.hardware.usb@1.0-service
1940 11144K 3732K 769K 708K 76K 76K 76K 24K /vendor/bin/hw/android.hardware.health@2.0-service
1932 16448K 3428K 761K 636K 172K 172K 172K 56K /system/bin/hw/android.system.suspend@1.0-service
2000 35216K 2920K 743K 712K 0K 0K 0K 0K /system/bin/awlogd
1775 12096K 3388K 735K 636K 256K 256K 256K 83K /system/bin/hwservicemanager
4587 13076K 3580K 734K 668K 8K 8K 8K 2K /system/bin/displayservice
1988 11928K 3232K 723K 676K 28K 28K 28K 9K /system/bin/wificond
1977 21816K 4568K 715K 576K 1072K 1072K 1072K 349K /system/bin/drmserver
2044 8216K 2556K 634K 608K 0K 0K 0K 0K /system/bin/ip6tables-restore
1950 8764K 2996K 619K 576K 0K 0K 0K 0K /system/bin/lmkd
1941 11032K 3536K 606K 544K 208K 208K 208K 67K /vendor/bin/hw/android.hardware.light@2.0-service
1978 18612K 4300K 584K 476K 828K 828K 828K 269K /system/bin/idmap2d
2043 8204K 2516K 579K 552K 0K 0K 0K 0K /system/bin/iptables-restore
13796 7828K 2620K 577K 452K 0K 0K 0K 0K -/system/bin/sh
1949 12684K 3608K 564K 500K 344K 344K 344K 112K /system/bin/gpuservice
1397 12392K 1880K 556K 436K 688K 688K 688K 224K /system/bin/init
1947 9904K 2812K 530K 496K 56K 56K 56K 18K /system/bin/ashmemd
1942 11024K 3464K 529K 468K 272K 272K 272K 88K /vendor/bin/hw/android.hardware.memtrack@1.0-service
1858 11036K 3432K 528K 468K 272K 272K 272K 88K /vendor/bin/hw/android.hardware.boot@1.0-service
1990 10744K 3028K 524K 460K 152K 152K 152K 49K /vendor/bin/crda.uevent
1982 13640K 3656K 512K 436K 432K 432K 432K 140K /system/bin/mediadrmserver
1991 8900K 3044K 510K 448K 20K 20K 20K 6K /vendor/bin/dom2reg
1774 9468K 2600K 482K 448K 168K 168K 168K 54K /system/bin/servicemanager
1997 8248K 2464K 479K 452K 16K 16K 16K 5K /system/bin/tombstoned
2002 8540K 2724K 478K 444K 16K 16K 16K 5K /system/bin/kmsgd
1860 9888K 2964K 419K 372K 132K 132K 132K 43K /vendor/bin/hw/tee_supplicant
1801 7828K 2392K 407K 284K 156K 156K 156K 50K /system/bin/sh
1992 10232K 2956K 325K 276K 224K 224K 224K 72K /vendor/bin/hw/radio_monitor
1931 10320K 2876K 309K 264K 292K 292K 292K 95K /system/bin/hw/android.hidl.allocator@1.0-service
1776 10036K 2688K 281K 240K 288K 288K 288K 93K /vendor/bin/vndservicemanager
1398 11880K 1396K 125K 8K 892K 892K 892K 290K /system/bin/init
------ ------ ------ ------ ------ ------ ------
241304K 178772K 150872K 64930K 57624K 21151K TOTAL
ZRAM: 22368K physical used for 68664K in swap (364516K total swap)
RAM: 486028K total, 27368K free, 788K buffers, 172428K cached, 1644K shmem, 40944K slab, 195M free+cached

进程列表:

procrank 每个进程的内存信息是通过读取 /proc/pid/maps 统计的,这点和 dumpsys meminfo 不太一样(具体代码见: libmeminfo/procmeminfo.cpp 的 ReadMaps() 和 ReadVmaStats())。dumpsys meminfo 统计的进程的 Pss 是通过 /proc/pid/smaps 统计的,会比 procrank 的大一些,包含了 Zram 里面的 SwapPss。来看一下先说横着的列:

    Vss(Virtual Set Size): /proc/pid/maps 中每一段 vma 的 size 之和。size 可以通过 vma 的 (结束地址 - 开始地址) x page_size 计算出来(一般 page_size 是 4kb,代码里面可以通过 unistd.h 里面的 getpagesize() 获取)。这里的 vma 有一些是没有映射到内存里面的(或者在 Swap 里面的),像是 page_count = 0 的就是没映射的,也就是没有被任何进程引用(注意和下面 Rss 的区别)。page_count 可以通过 /proc/pid/maps 中 vma 的虚拟地址,在 /proc/pid/pagemap 里面获取到内存页面的 PFN(Page Frame Number),然后再在 /proc/kpagecount 里就能读到页面被引用的计数了(具体的可以看 procrank 的源码或是参考资料里面的 pagemap 的相关资料)。再加上进程里面有很多共享库和资源(特别是 Android apk 是从 Zygote fork 出来的),所以 Vss 一般都很大,所以没啥参考价值。具体的怎么索引的和页面表的标志位含义可以看附录参考资料里面的 利用/proc/pid/pagemap将虚拟地址转换为物理地址pagemap和VSS/USS/PSS/RSS的计算
    Rss(Resident Set Size): /proc/pid/maps 中 page_count > 0 的 vma size 之和。 和 Vss 相比, 多了page_count > 0 这个过滤,表示这段 vma 至少被一个进程引用(被映射到内存当中)。所以 Rss 表示进程实际占用内存的情况(包含共享资源)。
    Pss(Proportional Set Size): /proc/pid/maps 中每一段 vma size / page_count 。和 Rss 相比,Pss 把多进程共享资源部分的内存按比例均分了。所以 Pss 表示进程实际占用内存的情况(共享资源按比例均分)。一般可以拿 Pss 来衡量进程内存占用情况。
    Uss(Unique Set Size): /proc/pid/maps 中 page_count = 1 的 vma size 之和。和 Pss 相比,Uss 除去了共享资源的内存。所以 Uss 表示进程私有内存占用的情况。
    Swap: 和 dumpsys meminfo 统计的类似,当开启了 Zram 就能统计到 Swap。这里的是通过 linux 的接口判断出 /proc/pid/maps 中哪些 vma 是在 Swap 分区里面的(同样是在上面的函数里面判断的)。这里统计到的值是包含了进程共享资源内存的 Swap 的,和 Vss 类似。
    PSwap: 和 dumpsys meminfo 里面的 SwapPss 一样,按 page_count 均分了。和 Pss 类似。
    USwap: Swap 里面 page_count = 1 的。和 Uss 类似。
    ZSwap: PSwap x zram 压缩率。 zram 压缩率 = zram_size / (SwapTotal- SwapFree)。zram_size 是前面介绍的 /sys/block/zram0/mm_stat 第三个数值,SwapTotal 和 SwapFree 是 /proc/meminfo 下面的数值。
    TOTAL: 上列表的简单求和。

ZRAM:

这里的 Zram 信息和 dumpsys meminfo 是一样的。

RAM:

这里信息都是直接从 /proc/info 下读取同名的数值(名字有点点差别)。另外 free+cached 的数值是我们平台自己加上去的。

/proc/pid/maps

每个进程都会有这个文件节点,表示进程的内存映射信息。例如说我们 cat 一下 systemui 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
12c00000-12cc0000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
12cc0000-12e40000 ---p 00000000 00:00 0 [anon:dalvik-main space (region space)]
12e40000-12f00000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
12f00000-12f80000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
12f80000-13280000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
13280000-14140000 ---p 00000000 00:00 0 [anon:dalvik-main space (region space)]
14140000-22c00000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
6fef2000-700cb000 rw-p 00000000 fd:03 1476 /system/framework/arm/boot.art
700cb000-70185000 rw-p 00000000 fd:03 1455 /system/framework/arm/boot-core-libart.art
70185000-701ad000 rw-p 00000000 fd:03 1467 /system/framework/arm/boot-okhttp.art
701ad000-701e2000 rw-p 00000000 fd:03 1452 /system/framework/arm/boot-bouncycastle.art
701e2000-701ee000 rw-p 00000000 fd:03 1449 /system/framework/arm/boot-apache-xml.art
701ee000-7087f000 rw-p 00000000 fd:03 1461 /system/framework/arm/boot-framework.art
7087f000-708ab000 rw-p 00000000 fd:03 1458 /system/framework/arm/boot-ext.art
708ab000-70966000 rw-p 00000000 fd:03 1470 /system/framework/arm/boot-telephony-common.art
70966000-70970000 rw-p 00000000 fd:03 1473 /system/framework/arm/boot-voip-common.art
70970000-70980000 rw-p 00000000 fd:03 1464 /system/framework/arm/boot-ims-common.art
70980000-70983000 rw-p 00000000 fd:03 1446 /system/framework/arm/boot-android.test.base.art
70983000-70a37000 r--p 00000000 fd:03 1477 /system/framework/arm/boot.oat
70a37000-70c5a000 r-xp 000b4000 fd:03 1477 /system/framework/arm/boot.oat
70c5a000-70c5b000 rw-p 00000000 00:00 0 [anon:.bss]
70c5b000-70c5d000 r--s 00000000 fd:03 1491 /system/framework/boot.vdex

上面只是截图了前面一部分,因为是整个进程的内存页面,所以整个文件会比较大(和进程复杂度有关,systemui 挺复杂的)。里面每一行代表一个 vma(virtual memory areas:虚拟内存区域)。下面我们来看看 vma 每一列代表什么含义:

    12c00000-12cc0000: 这个是该虚拟内存段的开始和结束地址。通过地址 x page_size 可以计算出该段内存的大小。
    rw-p: 该段内存的权限。前3位分别是:读、写、执行,最后一位 p 代表私有,s 代表共享。
    00000000: 该虚拟内存段起始地址在对应的映射文件中以页为单位的偏移量,对匿名映射,它等于0或者vm_start/PAGE_SIZE。
​    00:00: 文件的主设备号和次设备号。对匿名映射来说,因为没有文件在磁盘上,所以没有设备号,始终为00:00。对有名映射来说,是映射的文件所在设备的设备号。上面有映射号的都是一些文件资源。
    0: 被映射到虚拟内存的文件的索引节点号,通过该节点可以找到对应的文件,对匿名映射来说,因为没有文件在磁盘上,所以没有节点号,始终为00:00。
    [anon:dalvik-main space (region space)]: 被映射到虚拟内存的文件名称。后面带(deleted)的是内存数据,可以被销毁。对有名来说,是映射的文件名。对匿名映射来说,是此段虚拟内存在进程中的角色。dumpsys meminfo 是通过这个字段来进行分类统计的。

/proc/pid/smaps

smaps 是 map 的扩展,它显示的信息更加详细,还是拿 systemui 的来举例:

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
12c00000-12dc0000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)]
Name: [anon:dalvik-main space (region space)]
Size: 1792 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 480 kB
Pss: 480 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 480 kB
Referenced: 480 kB
Anonymous: 480 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
VmFlags: rd wr mr mw me ac
70c5b000-70c5d000 r--s 00000000 fd:03 1491 /system/framework/boot.vdex
Size: 8 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 8 kB
Pss: 2 kB
Shared_Clean: 8 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 8 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
VmFlags: rd mr me ms

这里也是截图了其中2段 vma。

    Size: vma 空间的大小。可以由地址计算出来。等于 Vss,进程有时候 malloc 了一大块内存,但是并没有真正使用。但是还是会计算这块内存的大小。
    KernelPageSize: kernel page size 的大小,一般是 4kb。
    MMUPageSize: MMU page size 大小,一般等于 KernelPageSize。
    Rss: 前面解释了。
    Pss: 前面也解释了。
    Shared/Private: 该 vma 是私有的还是共享的。对照前面的权限标志位的最后一位,确实 p 的 Shared 有数值,并且不等于 Pss(被均分了);而 s 的就是 Private 的有数值,并等于 Pss(因为是私有的)。
    Dirty/Clean: 在页面被淘汰的时候,就会把该脏页面回写到交换分区(换出,swap out)。有一个标志位用于表示页面是否dirty。
    Referenced: 当前页面被标记为已引用或者包含匿名映射。
    Anonymous: 匿名映射的物理内存,这部分内存不来自于文件的内存大小。
    AnonHugePages 统计的是Transparent HugePages (THP)。我暂时没理解是啥意思(网上抄的说明)。
    Shared/Private_Hugetlb: 由hugetlbfs页面支持的内存使用量,由于历史原因,该页面未计入“ RSS”或“ PSS”字段中。 并且这些没有包含在Shared/Private_Clean/Dirty 字段中。(网上抄的说明)。
    Swap: 开启 Zram 后,Swap 到 Zram 分区的大小。这里是包含了共享资源部分内存的。
    SwapPss: Swap 共享部分按比例均分。
    Locked: 常驻物理内存的大小,这些页不会被换出。
    VmFlags: 表示与特定虚拟内存区域关联的内核标志。标志如下:

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
rd - readable
wr - writeable
ex - executable
sh - shared
mr - may read
mw - may write
me - may execute
ms - may share
gd - stack segment growns down
pf - pure PFN range
dw - disabled write to the mapped file
lo - pages are locked in memory
io - memory mapped I/O area
sr - sequential read advise provided
rr - random read advise provided
dc - do not copy area on fork
de - do not expand area on remapping
ac - area is accountable
nr - swap space is not reserved for the area
ht - area uses huge tlb pages
ar - architecture specific flag
dd - do not include area into core dump
sd - soft-dirty flag
mm - mixed map area
hg - huge page advise flag
nh - no-huge page advise flag
mg - mergable advise flag

/proc/meminfo

前面的 dumpsys meminfo 和 procrank 中 total 统计的部分的基础就来源于这个节点的数值:

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
MemTotal: 486028 kB
MemFree: 26124 kB
MemAvailable: 181240 kB
Buffers: 788 kB
Cached: 172652 kB
SwapCached: 6428 kB
Active: 145524 kB
Inactive: 159336 kB
Active(anon): 61084 kB
Inactive(anon): 74240 kB
Active(file): 84440 kB
Inactive(file): 85096 kB
Unevictable: 2892 kB
Mlocked: 2892 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 486028 kB
LowFree: 26124 kB
SwapTotal: 364516 kB
SwapFree: 295852 kB
Dirty: 20 kB
Writeback: 0 kB
AnonPages: 132388 kB
Mapped: 108676 kB
Shmem: 1644 kB
Slab: 41300 kB
SReclaimable: 13752 kB
SUnreclaim: 27548 kB
KernelStack: 5792 kB
PageTables: 14332 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 607528 kB
Committed_AS: 11034652 kB
VmallocTotal: 507904 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
CmaTotal: 8192 kB
CmaFree: 248 kB

选一些重点的讲一些:

    (1).MemTotal: 总共系统可用的物理内存。你会发现这个并不等于实际贴在机器上的内存大小。是因为还有一部分内存要作为 linux kernel 的保留内存(reserved)。保留内存的详细信息后面再说。
    (2).MemFree: 这个就是真正意义上没有被使用的内存了。这值在 linux 系统上会比较低,其实有一部分内存是各个进程正在使用的,还有一大部分是 Cached。linux 的设计本意就是尽量多的使用内存,来保证进程间切换的平滑性(多任务)。
    (3).MemAvailable: 这个值会比 MemFree 多一些,kernel 根据一些可以回收的内存(例如 cached、buffer 之类的)估算出一个能用的。这个值我目前没发现有啥用处。
    (4).Buffers: 块设备(block)申请的内存。
    (5).Cached/Mapped/AnonPages: 进程的内存页面分为2种: file-backed pages(与文件对应的内存页)和 anonymous pages(匿名页)。file-backed pages 是指例如说进程的代码段、so、jar 库之类的,映射的都是 file-backed,而进程的堆、栈是不与文件相对应的,就属于匿名页。file-backed pages 在内存不足的时候可以直接写回硬盘的文件(叫 page-out),而不需要用到交互区(swap);而匿名页在内存不足的时候就只能写到硬盘的交换区里(叫 swap-out)。
​        AnonPages: 是前面提到的 anonymous pages。它是进程的私有堆栈。
        Mapped: 是前面提到的 file-backed pages。
​        Cached: Cached 是 Mapped 的超集,除了包含 Mapped 之外,Cached 还包括了 unmapped 的页面。进程中的 file-backed pages 被 unmapped 后不会马上回收,而是当做缓冲计算在 Cached 当中。但是进程中的 anonymous pages 一旦进程退出,则会马上回收。
    (6).SwapCached: 这个好像是 Swap 分区的 page cached。SwapCached 并没有包含在 Cached 里面。
    (7).Active/Inactive/Active(anon)/Inactive(anon)/Active(file)/Inactive(file): linux kernel 有个 LRU 的页面回收算法。LRU list 包括:LRU_INACTIVE_ANON(Inactive(anon))、LRU_ACTIVE_ANON(Active(anon))、LRU_INACTIVE_FILE(Inactive(file))、 LRU_ACTIVE_FILE(Active(anon))、LRU_UNEVICTABLE(Unevictable)。Active 就是对应正在使用的内存页面,一般这种是无法回收的。而 Inactive 则是最近没在使用的页面,一般这种是可以回收(reclaimed)的。而 Active(anon/file)则分别代表了正在使用的 anonymous pages 和 file-backed pages。Inactive(anon/file) 则是最近没在使用的 anonymous pages 和 file-backed pages。从上面定义来看一般: Active(anon) + Inactive(anon) = AnonPages ,但是发现并不相等,一个会受 Shmem 状态的影响,一个是有点小偏差。同理 Active(file) + Inactive(flie) 与不完全等于 Cached,一个也是受 Shmem 状态影响,一个是 Activie(flie) + Inactivie(file) 还包含 Buffers。
    (8).Unevictable: 是不能被 page-out/swap-out 的页面。
    (9).SwapTotal/SwapFree: 前面说了,配置了 Zram 后,swap 分区的总大小和空闲大小。
    (10).Shmem: 包括 shared memory 和 tmpfs。android 上一般也不大。
    (11).Slab/SReclaimable/SUnreclaim: Slab 是 linux kernel 上的一种内存分配管理机制,一般是内核模块使用。SReclaimable 和 SUnreclaim 一个是可以回收的,一个是不能回收的 Slab 内存,而 Slab 则这2个加一起。dumpsys meminfo 把 SUnreclaim 算成是 kernel Used 。
    (12).KernelStack: 前面有提到过,是内核的调用堆栈。内核栈是常驻内存的,既不包括在 LRU lists 里,也不包括在进程的 RSS/PSS 内存里,所以可以认为它是 kernel 消耗的内存。dumpsys meminfo 把 KernelStack 也计算在 Kernel Used 里面
    (13).PageTables: Page Table用于将内存的虚拟地址翻译成物理地址,随着内存地址分配得越来越多,Page Table会增大。请把Page Table与Page Frame(页帧)区分开,物理内存的最小单位是 page frame,每个物理页对应一个描述符(struct page),在内核的引导阶段就会分配好、保存在 mem_map[] 数组中,mem_map[] 所占用的内存被统计在 dmesg 显示的 reserved (前面提到的 kernel 预留内存)中,/proc/meminfo 的 MemTotal 是不包含它们的。而Page Table的用途是翻译虚拟地址和物理地址,它是会动态变化的,要从 MemTotal 中消耗内存。dumpsys meminfo 把 PageTables 计算在 Kernel Used 里面。
    (14).VmallocUsed: kernel 模块使用 vmalloc 申请的内存。它也是算 kernel 使用的内存的,dumpsys meminfo 把 VmallocUsed 也计算在 Kernel Used 里面。然而在 Android 上 /proc/meminfo 这里的 VmallocUsed 统计不到数值。dumpsys meminfo 是通过 /proc/vmallocinfo 统计的。/proc/vmallocinfo 里不仅有 vmalloc 的,还有 ioremap(这个是IO地址映射的)、vmap 的,这2个是不占内存的。vmalloc 的每一行有 “pages=x” 的页面数量信息,所以直接计算 pages 的总数,然后 x page_size 就能统计到 VmallocUsed 了(这也是前面 dumpsys meminfo kernel used 那里我说计算 pages= 的理由)。
    (15).CmaTotal/CmaFree: 配置的 cma 总大小和 free 大小。一般有 iommu 的方案 cma 都会比较小。

其他一些数值不是很重要就不细说了。

PageMap

PageMap 是一个 github 上的小工具(github地址),可以统计到每个进程匿名页、文件缓存等信息。可以认为是 /proc/meminfo 的 pid 版本。它的源码也比较简单,就一个 PageMap.c 文件,统计的方法是:

    (1). 遍历 /proc/pid/maps 中的所有 vma(也就是每一行)。

    (2). 通过 vma 的虚拟地址在 /proc/pid/pagemap 里面找到对应的 PFN,这是一个 64 bit 的值,含义 kernel 说明文档如下:

1
2
3
4
5
6
7
8
9
* Bits 0-54 page frame number (PFN) if present
* Bits 0-4 swap type if swapped
* Bits 5-54 swap offset if swapped
* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
* Bit 56 page exclusively mapped (since 4.2)
* Bits 57-60 zero
* Bit 61 page is file-page or shared-anon (since 3.5)
* Bit 62 page swapped
* Bit 63 page present

    (3). 通过 PFN 可以在 /proc/kpagecount 里面得到对应页面的引用计数。

    (4). 通过 PFN 可以在 /proc/kpageflag 里得到对应页面的标志位,从而判断出页数的属性(这个标志也是个 64 位数值,可以通过位来判断),通过页面属性累计各种信息(例如匿名页、文件缓存等):

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
0. LOCKED
1. ERROR
2. REFERENCED
3. UPTODATE
4. DIRTY
5. LRU
6. ACTIVE
7. SLAB
8. WRITEBACK
9. RECLAIM
10. BUDDY
11. MMAP
12. ANON
13. SWAPCACHE
14. SWAPBACKED
15. COMPOUND_HEAD
16. COMPOUND_TAIL
17. HUGE
18. UNEVICTABLE
19. HWPOISON
20. NOPAGE
21. KSM
22. THP
23. BALLOON
24. ZERO_PAGE
25. IDLE

其实上面的过程和前面 procrank 统计 Vss/Rss/Pss 是类似的。但是这个工具跑在全志的平台上统计需要稍微改一下:

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
diff --git a/PageMap.c b/PageMap.c
old mode 100644
new mode 100755
index d52dedb..c80bcd1
--- a/PageMap.c
+++ b/PageMap.c
@@ -288,7 +288,7 @@ int dumppid(struct global *globals)
uint64_t pfn;
uint64_t swapfile;
uint64_t swapoff;
- int present,swapped;
+ int present,swapped,file_or_shareanon,soft_dirty;
unsigned int shift;
unsigned int pagesize;
@@ -383,8 +383,11 @@ int dumppid(struct global *globals)
// Unpack common bits
present = (entry & 0x8000000000000000LL) >> 62;
swapped = (entry & 0x4000000000000000LL) >> 61;
- shift = (entry & 0x1f80000000000000LL) >> 55;
- pagesize = (1<<shift);
+ file_or_shareanon = (entry & 0x2000000000000000LL) >> 60;
+ soft_dirty = (entry & 0x80000000000000LL) >> 54;
+ //shift = (entry & 0x1f80000000000000LL) >> 55;
+ //pagesize = (1<<shift);
+ pagesize = 4096;
if(!present && !swapped){
// Page not present in physical ram or swap
@@ -409,6 +412,10 @@ int dumppid(struct global *globals)
pfn = entry & 0x007fffffffffffffLL;
if(globals->verbose && !skip){
printf(", Present (pfn %016" UINT64FMT "x)", pfn);
+ if (file_or_shareanon)
+ printf(", file/shareAnon");
+ if (soft_dirty)
+ printf(", pte-soft-dirty");
}
if(globals->map && !skip) printf("P");
@@ -473,6 +480,7 @@ int dumppid(struct global *globals)
if(hdpageflags & (1<<12)) stats.anon += pagesize;
if(hdpageflags & (1<< 2)) stats.refd += pagesize;
}
+ printf(",%s", item);
}

然后可以用编译服务器的 arm-gcc 编译:

1
2
3
4
ming85 ~ $ which arm-none-linux-gnueabi-g++
/arm/arm-2010.09/bin/arm-none-linux-gnueabi-g++
arm-none-linux-gnueabi-g++ PageMap.c -Wall -Wextra -fPIC -fno-inline -g -O2 --static -o pgmap_my

编译出来的 bin 文件 push 到 /data/local/tmp/(chmod +x 加下执行权限),用下面就可以统计指定进程的详细信息了: /data/local/tmp/pgmap_my -v -p pid > /sdcard/pgmap.txt 。统计是打印信息,所以需要重定向到一个文本里面,而且是一个页面一个页面的信息,还需要脚本二次统计:

1
2
3
4
5
6
7
8
9
10
11
12
==================== (region space)] [rw-p] [ 768kB] ====================
0000000012c00000-0000000012c00fff [ 4kB], Present (pfn 0000000000059145), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c01000-0000000012c01fff [ 4kB], Present (pfn 000000000004fb0d), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c02000-0000000012c02fff [ 4kB], Present (pfn 0000000000057f79), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c03000-0000000012c03fff [ 4kB], Present (pfn 000000000005c536), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c04000-0000000012c04fff [ 4kB], Present (pfn 00000000000553ae), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c05000-0000000012c05fff [ 4kB], Present (pfn 00000000000555db), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c06000-0000000012c06fff [ 4kB], Present (pfn 000000000004e044), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c07000-0000000012c07fff [ 4kB], Present (pfn 0000000000057b57), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c08000-0000000012c08fff [ 4kB], Present (pfn 0000000000046809), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c09000-0000000012c09fff [ 4kB], Present (pfn 0000000000059b61), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]
0000000012c0a000-0000000012c0afff [ 4kB], Present (pfn 0000000000047f0d), RefCnt 1, Flags [UPTODATE LRU ACTIVE MMAP ANON SWAPBACKED],(region space)]

每个页面的页面属性(Flags 那里的),引用计数(RefCnt)会打印出来。例如说我们比较关心的就是进程的匿名页面和缓存信息,可以用下面脚本对得到的文本进行统计:

1
2
3
4
5
6
7
8
9
10
11
cat $1 | grep Swapped: | awk '{print $1,$2/4}'
echo
echo "anon inactive lru: `cat $1 |grep -v ACTIVE| grep ANON |grep LRU | wc -l`"
echo "anon active lru: `cat $1 |grep ANON| grep ACTIVE |grep LRU | wc -l`"
echo "anon undirty lru: `cat $1 |grep ANON| grep -v DIRTY |grep LRU | wc -l`"
echo
echo "file inactive lru: `cat $1 | grep -i 'file' | grep -v ACTIVE|grep LRU | wc -l` "
echo "file active lru: `cat $1 | grep -i file | grep ACTIVE |grep LRU | wc -l`"
echo "file unique inactive lru: `cat $1 | grep -i 'file' | grep -v ACTIVE|grep LRU | grep "RefCnt 1," | wc -l` "
echo "file unique active lru: `cat $1 | grep -i file | grep ACTIVE |grep LRU | grep "RefCnt 1," | wc -l`"
echo "file unique undirty lru: `cat $1 | grep -i file | grep -v DIRTY|grep LRU | grep "RefCnt 1," | wc -l`"

这个脚本需要 linux 环境运行(window 可以用 cygwin),也可以直接 push 到设备上跑: sh /data/local/tmp/x1.sh /sdcard/pgmap.txt :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在 Swap 分区中的页面,下面的数据单位都是页面(一个页面为4kb)
Swapped: 3343
// anon 是匿名页面
anon inactive lru: 2525
anon active lru: 1000
// undirty 是非脏页面
anon undirty lru: 3525
// file 是文件缓存
file inactive lru: 1514
file active lru: 1445
// unique 是引用计数为1的页面,undirty 是非脏页面
file unique inactive lru: 508
file unique active lru: 484
file unique undirty lru: 969

ion

ion 是 google 推出的 Android 的一种内存管理机制,一般用于多媒体、Camera、显示模块。它可以在用户空间的进程间共享,或者内核的模块之间共享。我个人认为 ion 的内存没有计算在前面的进程内存里面(Pss)或者 kernel 模块里面(/proc/meminfo)。我们先来看一下 ion 的内存怎么看,Android 10 上面有一个节点可以看到: cat /sys/kernel/debug/ion/heaps/sys_user:

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
client pid size
----------------------------------------------------
init 1397 4096
init 1397 4096
init 1397 77824
ion_disp2 1 4915200
----------------------------------------------------
orphaned allocations (info is from last known client):
allocator@2.0-s 1938 81920 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 81920 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 118784 0 1
allocator@2.0-s 1938 118784 0 1
allocator@2.0-s 1938 118784 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 8192 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 8503296 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 20480 0 1
allocator@2.0-s 1938 2494464 0 1
allocator@2.0-s 1938 81920 0 1
----------------------------------------------------
total orphaned 24346624
total 29347840
deferred free 0
----------------------------------------------------
0 order 8 highmem pages uncached 0 total
3 order 8 lowmem pages uncached 3145728 total
0 order 4 highmem pages uncached 0 total
48 order 4 lowmem pages uncached 3145728 total
0 order 0 highmem pages uncached 0 total
128 order 0 lowmem pages uncached 524288 total
0 order 8 highmem pages cached 0 total
0 order 8 lowmem pages cached 0 total
0 order 4 highmem pages cached 0 total
0 order 4 lowmem pages cached 0 total
0 order 0 highmem pages cached 0 total
0 order 0 lowmem pages cached 0 total

上面列出了所有进程(ion 的 client)申请的 ion 的情况(上面单位是 byte)。上面主要分为2部分:

    (1). init 占用的。这里并不多,也就才 4M 左右。上面 pid 1 和 1397 分别是2个 init 进程(好像从 android 某个版本开始就有2个 init 进程了)。一般 init 申请 ion 是用来做一些平滑显示用的。

    (2). 1938 从上面的进程列表可以看到是 android.hardware.graphics.allocator@2.0-service 。这个是 android 的里面的一个 hal 模块,用来管理 buffer queue 的(以前的 android 版本好像叫 gralloc)。procrank 可以看到这个进程就算是算 Rss 也才 4.8M,但是从 ion 这边看到的它申请的 ion 内存却有 23M。所以我认为前面计算进程 的 Pss 没有统计 ion。而且网上有个补丁:android: ion: include system heap size in proc/meminfo ,这么看原生的 /proc/meminfo 也是没统计 ion 的。gralloc 申请的 23M 内存大多数都是 SurfaceFlinger 用了,可以使用 dumpsys SurfaceFlinger 查看:

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
Display 0 HWC layers:
-------------------------------------------------------------------------------------------
Layer name
Z | Window Type | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB)
-------------------------------------------------------------------------------------------
com.android.systemui.ImageWallpaper#0
rel 0 | 2013 | CLIENT | 0 | 0 0 1024 600 | 0.0 0.0 1318.0 772.0
- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - -
com.android.launcher3/com.android.launcher3.Launcher#0
rel 0 | 1 | CLIENT | 0 | 0 0 1024 600 | 0.0 0.0 1024.0 600.0
- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - -
StatusBar#0
rel 0 | 2000 | CLIENT | 0 | 1000 0 1024 600 | 0.0 0.0 24.0 600.0
- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - -
NavigationBar0#0
rel 0 | 2019 | CLIENT | 0 | 0 0 48 600 | 0.0 0.0 48.0 600.0
- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - -
h/w composer state:
h/w composer enabled
layer | handle | format |bl|space|TR|pAlp| crop or color | frame |zOrder|hz|ch|id|duto
----------------------------------------------------------------------------------------------------------------------------------------------------
0xa813050c| 0xa81270e0| 1| 1| 0| 0|1.00|[ 0.0, 0.0,1318.0, 772.0]|[ 0, 0,1024, 600]| 0|-1|-1|-1|FORCE_GPU
0xa81306ec| 0xa8127540| 1| 2| 0| 0|1.00|[ 0.0, 0.0,1024.0, 600.0]|[ 0, 0,1024, 600]| 1|-1|-1|-1|FORCE_GPU
0xa813032c| 0xa81271c0| 1| 2| 0| 0|1.00|[ 0.0, 0.0, 24.0, 600.0]|[1000, 0,1024, 600]| 2|-1|-1|-1|FORCE_GPU
0xa8130a0c| 0xa8127460| 1| 2| 0| 0|1.00|[ 0.0, 0.0, 48.0, 600.0]|[ 0, 0, 48, 600]| 3|-1|-1|-1|FORCE_GPU
0xa81301ec| 0xa8127230| 5| 2| 0| 0|1.00|[ 0.0, 0.0,1024.0, 600.0]|[ 0, 0,1024, 600]| 65536| 0| 1| 0|HWC_LAYER
disp:0 cur:1662-1661 ker:1662
----------------------------------------------------------------------------------------------------------------------------------------------------
GL:29030400 GCL:2457600 GC:2457600 GR:0 DR:0 RM(C):8947848(0) RP(C):4147200(0)
memmalloc:9408
Allocated buffers:
0xa80255b0: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0x1a00 | FramebufferSurface
0xa8025620: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0x1a00 | FramebufferSurface
0xa8025690: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0x1a00 | FramebufferSurface
0xa8027370: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0xb02 | com.android.launcher3/com.android.launcher3.Launcher#0
0xa80273e0: 75.00 KiB | 24 ( 32) x 600 | 1 | 1 | 0xb02 | StatusBar#0
0xa80274c0: 75.00 KiB | 24 ( 32) x 600 | 1 | 1 | 0xb02 | StatusBar#0
0xa80276f0: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0xb02 | com.android.launcher3/com.android.launcher3.Launcher#0
0xa80277d0: 75.00 KiB | 24 ( 32) x 600 | 1 | 1 | 0xb02 | StatusBar#0
0xa80278b0: 114.00 KiB | 600 ( 608) x 48 | 1 | 1 | 0xb02 | NavigationBar0#0
0xa8027990: 8300.00 KiB | 1318 (1328) x 1600 | 1 | 1 | 0xb02 | com.android.systemui.ImageWallpaper#0
0xa8027a00: 114.00 KiB | 600 ( 608) x 48 | 1 | 1 | 0xb02 | NavigationBar0#0
0xa8027a70: 112.50 KiB | 48 ( 48) x 600 | 1 | 1 | 0xb02 | NavigationBar0#0
0xa8027ae0: 2400.00 KiB | 1024 (1024) x 600 | 1 | 1 | 0xb02 | com.android.launcher3/com.android.launcher3.Launcher#0
Total allocated (estimate): 23265.50 KB

gralloc 的 23M 都是 SurfaceFlinger 的 BufferQueue。一个 BufferQueue 一般对应一个显示图层(Layer)。一般普通的 Layer 对应的 BufferQueue 是3重缓冲,所以有3块内存块(壁纸的好像有点特殊只有一个)。除了显示的 Layer 的 BufferQueue 之外,还有一个 FramebufferSurface 的 BufferQueue,这个是用来做 GPU 合成的。所以说显示的 Layer 越多,就越占内存(BufferQueue 越多),而且也越占带宽(无论是 GPU 合成还是 HWC 合成,多一个图层就多一次合成操作)。

Kernel used

目前我还是认可 dumpsys meminfo 中 kernel used 的统计方式:Shmem + SUnreclaim+ PageTables + KernelStack + VmallocUsed

Kernel reserved

前面有说无论是 /proc/meminfo 还是 free 看到的可用内存都会比物理内存小。这其中差的内存就属于 Kernel reserved。这部分内存有一些是一些驱动专用内存,一些是内核代码段,一些是内存页表。有几种方法可以看到 reserved 的信息:

    (1). 开机启动的 dmesg 打印(一般可以接串口打印看到):

1
2
Memory: 476056K/524288K available (10240K kernel code, 1419K rwdata, 3040K rodata, 1024K init, 423K bss, 40040K reserved, 8192K cma-reserved, 0K highmem)

    (2). cat /sys/kernel/debug/memblock/reserved:

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
0: 0x40004000..0x40007fff, size:16K
1: 0x40020000..0x40020fff, size:4K
// 这个应该是包含了 kernel code、atf 相关的内容,地址连续所以只显示一块
2: 0x40100000..0x411cdc1b, size:17207K
3: 0x42000000..0x420bc5b4, size:753K
4: 0x44000000..0x4401a0d3, size:104K
// 这个应该是存放内存页面表结构的
5: 0x48000000..0x48ffffff, size:16384K
6: 0x5f291000..0x5f334fff, size:656K
7: 0x5f335a80..0x5f337a7f, size:8K
8: 0x5f337aa0..0x5f7ff03b, size:4893K
9: 0x5f7ff040..0x5f7ff07b, size:0K
10: 0x5f7ff080..0x5f7ff0f7, size:0K
11: 0x5f7ff100..0x5f7ff10f, size:0K
12: 0x5f7ff140..0x5f7ff14f, size:0K
13: 0x5f7ff180..0x5f7ff183, size:0K
14: 0x5f7ff1c0..0x5f7ff1c3, size:0K
15: 0x5f7ff200..0x5f7ff59a, size:0K
16: 0x5f7ff5c0..0x5f7ff95a, size:0K
17: 0x5f7ff980..0x5f7ffd1a, size:0K
18: 0x5f7ffd2c..0x5f7ffd49, size:0K
19: 0x5f7ffd4c..0x5f7ffd80, size:0K
20: 0x5f7ffd84..0x5f7ffde4, size:0K
21: 0x5f7ffde8..0x5f7ffe38, size:0K
22: 0x5f7ffe3c..0x5f7ffece, size:0K
23: 0x5f7ffed0..0x5f7ffeea, size:0K
24: 0x5f7ffeec..0x5f7fff06, size:0K
25: 0x5f7fff08..0x5f7fff22, size:0K
26: 0x5f7fff24..0x5f7fff3e, size:0K
27: 0x5f7fff40..0x5f7fff9f, size:0K
// 这个是 cma
28: 0x5f7fffb0..0x5fffffff, size:8192K

第一种方法看到的可用内存 476056K 会比 /proc/meminfo 的少一些。是因为这里统计的的可用内存有一部分在 boot 阶段还未释放,所以启动后的 /proc/meminfo 的可用内存会比这里统计的高一些。这里的一些内核代码段和init的一些占用,加起来也不等于后面的 40040K reserved,而且 reserved 也不光光只有这些。

所以还是第二种方法好一些,而且还能看到具体的地址分布。这个在优化 reserved 内存的时候相关负责的研发可以根据地址分布推断出是哪里占用 reserved 内存,从而进行裁剪、优化。第二种方法累积求和和第一种的 reserved 差不多(需要把最后那个 8192K 剪掉,这个是 cma 大小。cma 系统是可以使用的,不应该计算在 reserved 里面)。reserved(40025kb) + memoryTotal(486028kb) 会比 512Mb(例子中是 512 的样机) 大一点点,可能多多少少哪里又有点统计重复了。上面的注释是根据全志平台的 kernel 推断出来的。

MAT

MAT(MemoryAnalyzer)是一个分析 java heap 的工具,可以分析 java heap 中对象的引用情况。一般来说这个工具是用来分析 java 内存泄漏的,但是这个工具同时也可以拿来分析内存占用情况。我之前写过一篇 MAT 的使用文章可以参考一下:Android 应用内存泄漏问题分析

MAT 可以在官网下载到:MAT下载地址。使用方法就是抓取 dump heap 文件,让用 MAT 打开来分析。dump heap 可以使用 AndroidStudio 抓,也可以使用 sdk 里面的 monitor.bat 抓,但是我更喜欢用命令行抓:

1
2
3
4
5
6
7
8
9
#
# 给进程发 10 信号量,是强制进行 gc,在抓之前排除一些弱引用的干扰
kill -10 pid
# am 的 dumpheap 命令把 java heap dump 出来,并保存到指定路径(一般 /data/local/tmp 会有权限)
am dumpheap pid /data/local/xx.heap
# 把 dump 的 heap pull 到 pc
adb pull /data/local/xx.heap
# hprof-conv 是 sdk platform-tools 下面的一个转化工具,转化之后的 hprof 文件就可以用 MAT 打开了
hprof-conv xx.heap xx.hprof

MAT 打开 hprof 文件后,一般选 Top Consumers(占内存最高的对象和Class):

最占内存的对象排行榜

这张图显示的是 java 单个对象占用内存最高的排行榜(这里是 dump system_server 的)。从这里看到基本上没什么大的对象(一般来说像 Bitmap 这种就是大的对象),单个对象最大的也就才 500k。

最占内存的对象排行榜

这行图是显示占内存最多的类的排行榜。从这里可以看到都是数量巨大小对象占了内存。所以不是很好简单的针对优化。例如说如果有 Bitmap 这种大对象的就可以看看是不是可以剪裁掉。这里结合 system_server 应该是里面包含了很多服务对象导致,可以考虑删掉一些不需要的服务模块来减少一部分对象,来节省一些内存。

malloc_debug

上一章的 MAT 是分析 java 内存的,这一章介绍的 malloc_debug 就是分析 native 内存的。它是 libc 的一个调试开关,打开之后 c 库会跟踪并记录每一个 malloc 的调用堆栈。下面是它的源码和说明:

android/bionic/libc/malloc_debug
android/bionic/libc/malloc_debug/README.md

调试普通 app 和 platform apk 是不太一样的,这里我们介绍 platform apk 的调试方法,因为我们这里是用来分析内存占用的。这个工具又分为:

​    (1).中途抓取: 让应用先启动,然后发送一个信号量打开 malloc_debug 开关。这个适用于排查一些内存泄漏场景,并不适合我们这里分析系统模块的内存占用情况。
​    (2).从开始抓取: 这个需要先把 malloc_debug 打开,然后再让应用运行,这样就能完整的记录下来每一个 native 的 malloc 申请,这样才能分析内存占用情况。

所以我们这里介绍的是基于 platform apk 从头开始抓取的用法(注意这里需要使用 userdebug 或是 eng 固件,user 固件是无法调试的):

1
2
3
4
5
6
7
8
9
10
11
#
# stop 先终止所有的 android 进程
stop
# 下面2个属性就打开 malloc_debug 的开关:
# libc.debug.malloc.program: 是配置需要开启 malloc_debug 的进程名字,由于 android 的 apk 都是
# 由 zygote fork 出来的,所以只能让 zygote 进程开启 malloc_debug,zygote 进程就是 app_process
# backtrace: 是记录 malloc 堆栈的深度,记录的堆栈越深越耗时,默认是 16
setprop libc.debug.malloc.program app_process
setprop libc.debug.malloc.options backtrace=8
# 配置了上面属性开关后,重新让 android 系统进程启动
start

这里说明一下:让 app_process(zygote) 开启了 malloc_debug ,也就是让所有的 apk 进程开启了 malloc_debug ,这会导致 system_server 启动变得很慢,很容易造成卡死,然后触发 WatchDog 自动重启,导致抓取失败。有几个方面可以改善一下:

    (1). 尽量选择内存大的机器来抓取,这个并不影响分析结果的。因为记录每一个 malloc 的堆栈,会额外占用内存。最少1G以上的,512M 的就不要抓取了,基本无法启动。
    (2). 所以上面 backtrace 配置成 8,默认的 16 更加难以成功启动。
    (3). 注意观看 logcat,如果有报由于什么卡住了导致重启,就先把这个相关的功能先注释掉(例如说开启了 malloc_debug ,awbms 老是报错,我就先把 awbms 注释掉了)。

然后剩下的就是靠运气了不停的重试了,我在 A50 Android10 1G 的设备有一次成功启动了。启动了之后使用下面命令在抓取:

1
2
3
4
5
6
7
8
9
10
11
12
#
# 抓取命令同样是 am dumpheap ,只不过抓 native 需要加上 -n
am dumpheap -n pid /data/local/xx_heap.txt
# adb pull 拉出来
adb shell pull /data/local/xx_heap.txt
# 在 sdk 的路径下面有一个转化脚本: development/scripts/native_heapdump_viewer.py
# 这是一个 python 脚本,打开有使用说明:
# --html: 表示要生成 html 报表,主要后面要自己重定向到 html 文件
# --symbols: 这个是你抓取固件,编译出来的符号表,在 out/target/product/xx/symbols/ 下面,
# 所以说要抓取分析的话,需要保留好编译固件的中间文件
python native_heapdump_viewer.py --html --symbols xx xx_heap.txt > xx_heap.html

dumpheap -n 出来的 txt 就已经包含了每个 malloc 的调用堆栈,但是都是一些函数地址,需要自己去 maps 文件的代码段,去符号表里用 addr2line 反编译来看。这里就不介绍具体方法了(网上也有),sdk 里面这个 python 脚本就是帮我们转化了(所以他也是需要符号表的)。例如说转化出来的一个报表是(抓的是 Android 10 systemui 的):

1
2
3
4
5
Native allocation HTML viewer
Click on an individual line to expand/collapse to see the details of the allocation data
13945660 100.00% 22197 app
2122038 100.00% 16699 zygote

点开 app 开具体情况,zygote 的是父进程的:

systemui native heap

systemui 的 native 堆栈一共 13.2MB(13945660 单位是字节)。下面的对象是按占用内存大小来排序的。排在第一个是一个 8MB 的 Bitmap decode 的时候申请的,占了 native heap 60% 的大小。这里我把堆栈配成了8,好像不是很直观的看到是哪里调用到的。但是 systemui 里面 8M 大小的 Bitmap,之前 dumpsys SurfaceFlinger 的时候看到 ImageWallpaper 申请的 Layer buffer 刚好是 8M。所以可以猜想到这个应该是 ImageWallpaper 加载的 Bitmap 申请的内存。然后去查看了一下相关代码,正是 SystemUI load ImageWallpaper 的 Bitmap 申请的内存。所以一张壁纸不光占用了显示 buffer(图层),同时加载的 Bitmap 也很占内存。如果不要壁纸的话,把图层和 Bitmap 都干掉,这里以 A50 Android10 1280x720 为例,可以节省 16M 的内存。

工具小结

内存总计

上面说了那么多,我们试着来看看统计一下系统各个模块内存的占用情况。我以上面的列举的数据为例:样机物理内存 512Mb,减去大概 40Mb 的 reserved,系统可用内存为: 486028 Kb。根据上面的理论知识,系统总共内存应该等于下面各个部分之和:

    (1). 所有进程的 Pss 之和:可使用 dumpsys meminfo 的中的统计。但是这个是包含了 Zram SwapPss 的,如果用这个,那应该减去 SwapPss 的部分(我认为可以使用 ZRAM 里面的 68,664K in swap - 22,368K physical used)。如果使用 procrank 的 Pss 统计之和,这个是不包含 SwapPss 的,所以应该把 Zram 22368K physical used 这部分加上。
    (2). 缓存中 unmapp 的部分:这部分没有统计在进程 Pss 里面,可以使用 /proc/meminfo: Cached - Mapped
    (3). 空闲内存:/proc/meminfo: free。
    (4). Kernel Used:可以按照 dumpsys meminfo 的统计。
    (5). Kernel Cached:也可以按照 dumpsys meminfo 的统计,但是这个计算公式把 (2) 那里也算上了。
    (6). gralloc 申请的:前面有统计方法。
    (7). cma:前面有统计方法。

其实仔细看上面这些部分,可以等效等于 dumpsys meminfo 中的 Free RAM + Used RAM - SwapPss + gralloc + cma。按这个计算:

168,297K + 292,956K - (68,664K - 22,368K) + (29347840 / 1024)K + 8192K = 451809Kb

和系统记录的 486028 kb 还有点差距。可能是哪里统计有漏洞(我也没一一去看统计代码,网上一些资料说这些统计是点有遗漏的),也有可能是例子这个样机还没支持 dumpsys meminfo GL(A50 Android 10),GPU 那部分内存没统计上(也不确定 GPU 驱动的是不是包含在哪个里面了)。然后其实进程 Pss 之和还可以用 procrank 的,前面也有说,再加上 Zram physical used 的。然后其他的内容去 /proc/meminfo 里面自己算。计算出来的结果和上面直接加 dumpsys meminfo 的差不多。那目前来说我感觉这样也挺接近可用内存了。

然后我去拿了一台支持 dumpsys meminfo GL 的样机(A100 Android 10),通过上面的计算公式计算,发现已经和可用内存很接近了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. dumpsys meminfo:
Total RAM: 991,364K (status normal)
Free RAM: 239,195K ( 121,859K cached pss + 85,892K cached kernel + 31,444K free)
Used RAM: 728,265K ( 591,361K used pss + 136,904K kernel)
Lost RAM: 56,974K
ZRAM: 11,908K physical used for 50,128K in swap ( 743,516K total swap)
// 2. cat /sys/kernel/debug/ion/heaps/sys_user:
----------------------------------------------------
total orphaned 33128448
total 41689088
// 3. cat /proc/meminfo:
CmaTotal: 8192 kB
CmaFree: 5224 kB

239,195K + 728,265K - (50,128K - 11,908K) + (41689088/1024)K + 8192K = 978144Kb

我拿的 oppo X20(4G,Android 8.1)也试了下,发现也挺接近了的(cat ion 没权限,可以用 dumpsys SurfaceFlinger 代替,在没有多媒体播放或是 camera 的情况下,误差不会特别大)。那应该这个计算方式还算靠谱吧。

应用内存分析

上面 dumpsys meminfo 还是 procrank 看到的 apk 进程占用的 Pss 都挺大的,但是其实单独 dumpsys meminfo pid 就会发现一般进程大部分都是一些文件缓存占用比较大(例如 Code、资源、库文件等)。可以单独看 Dalvik Heap 和 Native Heap,然后还能通过 MAT 和 malloc_debug 进一步分析是是哪些对象申请了内存。MAT 抓出来的 java heap 和 dumpsys meminfo pid 里面的 Dalvik Heap 中的 heap size 算是比较接近,但是还是有些差距,可能统计的内容有点不一样。malloc_debug 抓出来的 native heap 和 dumpsys meminfo pid 里面的 Native Heap 的 Total Pss 还是挺接近的,比 MAT 的误差小很多。

某些时候还能直接使用 /proc/pid/maps 或是 /proc/pid/smaps 直接分析,也可以使用 PageMap 具体查看应用的匿名内存页和文件缓存页情况。

优化思路

Kernel

Kernel used

按第一章的 kernel used 拆分,目前我的认知来说:

    (1). 可以优化 Slab 的占用
    (2). 可以去掉一些无用的,但是却加载了的 ko 模块

不过这个需要 kernel 的同事来分析,目前暂时还没接触到案例。以后接触到再补充。

Kernel reserved

kernel reserved 的大小直接决定了开机后 MemTotal 的大小。从第一章的拆分来看:

​    (1).精简代码段: 可通过 kernel 的 config 去掉一些无用的模块,这样可以节省一部分 kernel 代码段的占用。一般也是需要 kernel 的同事进行确认。例如说 R818 Android 10 1G 方案通过 menuconfig 去掉无用模块,代码段节省了 2M。

    (2).删除 optee 部分内容: 这里就是第一章分析的 atf 那部分。如果是非安全系统,是几乎可以把整个 atf 部分删掉的(好像需要留1、2M吧)。如果是安全系统,那么根据需求可以删去一部分,例如说 R818 Android 10 1G 方案就删掉了 gatekeeper 和 keymaster(具体删掉方案得找相关负责人弄),atf 部分节省了 14M。

快速回收

这个是在研究竞品系统上发现的一个方法,原理是快速(强制)让 kernel 回收后台应用的匿名页面(anon)和文件缓存(file),从而达到增大 free+cache 的目的。kernel 原生有个 /proc/sys/vm/drop_caches 接口(echo 1 是释放 cache,echo 2 是释放 slab,echo 3 = 1+2),但是效果不是特别明显(很多占用的 cache 其实释放不掉)。kernel 同事仿造这个思路在 5.4 的 kernel 新增了一个接口(目前还处于调试阶段,我提前拿到补丁在平板上试了一下):/proc/sys/vm/reclaim ,用法如下:

1
2
3
4
#
# pid: 要回收的进程,可以自己制定一些策略,强制回收哪些进程(例如可以参考一下 oom adjust score)
# 1: 代表回收 anon,2 代表回收 file, 3 = 1+2
echo pid 1 > /proc/sys/vm/reclaim

在平板上以切换到后台的 Settings 为例,效果如下:

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
// 强制回收前:
// dumpsys meminfo:
** MEMINFO in pid 1615 [com.android.settings] **
Pss Private Private Swap Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 4105 4048 0 0 5640 5540 4334 1205
Dalvik Heap 2889 2832 0 0 3904 3480 2610 870
Dalvik Other 613 428 0 0 1268
Stack 508 508 0 0 512
Ashmem 2 0 0 0 16
Other dev 44 0 44 0 260
.so mmap 2768 212 0 0 32600
.jar mmap 2112 0 108 0 19236
.apk mmap 5052 0 4380 0 7236
.ttf mmap 63 0 0 0 248
.dex mmap 678 16 644 0 732
.oat mmap 1128 0 0 0 12956
.art mmap 3101 2456 8 0 13828
Other mmap 112 32 0 0 1136
Unknown 408 404 0 0 596
TOTAL 23583 10936 5184 0 23583 9020 6944 2075
// PageMap 统计:
// 1G 设备内存充足,占时没有用到 Zram,所以 Swap 是 0
Swapped: 0
anon inactive lru: 0
anon active lru: 6913
anon undirty lru: 6913
file inactive lru: 3435
file active lru: 14458
file unique inactive lru: 664
file unique active lru: 624
file unique undirty lru: 1286

使用 echo 3 进行强制 anon + file 回收后:

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
// dumpsys meminfo:
** MEMINFO in pid 1615 [com.android.settings] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 48 48 0 3583 64 5056 4123 932
Dalvik Heap 0 0 0 2141 0 3458 2594 864
Dalvik Other 0 0 0 427 0
Stack 84 84 0 424 88
Other dev 12 0 12 0 180
.so mmap 146 12 0 238 4284
.jar mmap 83 0 0 0 1480
.apk mmap 1 0 0 0 32
.dex mmap 0 0 0 16 0
.oat mmap 82 0 0 0 1352
.art mmap 53 0 0 3032 1016
Other mmap 30 24 0 8 532
Unknown 0 0 0 408 8
TOTAL 10816 168 12 10277 10816 8514 6717 1796
// PageMap:
Swapped: 3343
anon inactive lru: 2525
anon active lru: 1000
anon undirty lru: 3525
file inactive lru: 1514
file active lru: 1445
file unique inactive lru: 508
file unique active lru: 484
file unique undirty lru: 969

可以看到强制回收后,在后台 Settings 的 Pss 大量减少,通过 PageMap 看到匿名页面和缓存页面大量被换入 Swap 分区。如果对所有后台进程都进行这个操作,那么 free+cache 将会大量增加(经过测试 512M 方案大概可以增加 40-50M)。

但是这个方案也是有弊端的,因为它的是通过牺牲了后台进程的缓存,来换取前台进程的可用内存。当再次切换到后台进程的时候经过强制回收后的进程,Activity 速度明显慢了一个档次。在平板上将 Settings 切换到后台,对比正常温启动和强制回收后的温启动2次启动的时间:

1
2
3
4
5
#
# 正常温启动 Settings:
ActivityTaskManager: Displayed com.android.settings/.Settings: +412ms
# 强制回收后温启动 Settings:
ActivityTaskManager: Displayed com.android.settings/.Settings: +1s416ms

可以看到,强制回收后,由于失去了缓存的作用,温启动速度大幅度慢于正常的温启动。所以这方案仅适用于内存十分低下的设备(例如说 512)。io 很慢的 nand 设备也要慎用,因为强制回收后,不光是要从 Swap 解压内存,某些还是需要重新从磁盘 load 数据的,这会进一步放大 nand 设备 io 瓶颈问题。

Android

删除无用模块

这个是最简单,也是最有效的优化手段。随着 Android 版本的更新,功能增多和模块框架化的同时,进程会越来越多和臃肿。用 dumpsys meminfo 看看,结合具体产品看看,哪些 Pss 大,又用不到的模块就可以想办法删掉。其中有不少的模块都是可以直接删掉的,有一些可能需要改改代码,这个需要具体模块具体分析。在 R818 Android10 1G 方案上我列了一个表,删掉了 68 个 模块(有很多是主题 Overlay 只是能省一点 emmc 而已,并不能省运行内存)。删模块的时候最好一个一个的删,删除之后测试一下一些产品的基本功能是否正常,再进行下一个会比较好。

删一些系统自带的模块,可能无法避免要去修改 build/core 下面系统的编译脚本,这里介绍一个办法,可以巧妙的删除系统自带模块,而又不用修改系统编译脚本:

Android 的 .mk 文件里面有一个 LOCAL_OVERRIDES_PACKAGES 的配置,就是可以用当前的模块覆盖已有的模块。我们只需要在 vendor 下面新建一个空壳的模块(例如说 apk),然后把想要删除的原生模块给 override 掉就变相把这些原生模块给删掉了。例如说空壳 .mk 可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
#
###############################################################################
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := PackageOverride
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := platform
LOCAL_PRODUCT_MODULE := true
LOCAL_OVERRIDES_PACKAGES := $(GLOBAL_REMOVED_PACKAGES)
#LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_SDK_VERSION := current
include $(BUILD_PACKAGE)

其中 GLOBAL_REMOVED_PACKAGES 就是我们自定义的全局变量,可以在方案的 .mk 中配置这个变量,把想要删除的原生模块加到这个变量里面,例如说下面就是 R818 Android 10 1G 方案配置删除的原生模块:

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
# Remove Some unuse system package to save memory.
GLOBAL_REMOVED_PACKAGES := \
MmsService \
Telecom \
TelephonyProvider \
TeleService \
SecureElement \
CalendarProvider \
Calendar \
Contacts \
Email \
DeskClock \
QuickSearchBox \
ExactCalculator \
Calculator \
WallpaperPicker2 \
Browser2 \
Gallery2 \
BookmarkProvider \
Camera2 \
CtsShimPrebuilt \
CtsShimPrivPrebuilt \
DocumentsUI \
BasicDreams \
EasterEgg \
Music \
MusicFX \
WallpaperCropper \
WallpaperBackup \
HTMLViewer \
SoundRecorder \
VideoPlayer \
BuiltInPrintService \
NfcNci \
OneTimeInitializer \
PrintRecommendationService \
frameworks-base-overlays \
DynamicSystemInstallationService \
LocalTransport \
ManagedProvisioning \
mediametrics \
mediadrmserver \
drmserver \
vr

精简模块内容

    (1). 可以参看 1.10 和 1.11 小结的内容,找到模块中最占资源的内容,看看是不是可以精简掉。例如说在某些产品上精简掉不必要的壁纸,就能节省不少内存(同时去掉显示 Layer 和加载的 Bitmap)。如果说实在要保留壁纸,也可以限制壁纸的大小,让其不超过屏幕的大小,也能节省一些内存。

    (2). 可以参考 Android Watch 的一些宏定义,精简 SystemService 一些不需要的服务,可以减少 SystemService 的一些占用。还可以去掉 Class 预加载,但是这个会减慢应用启动速度。

后续有什么新发现,再补充。

资源瘦身

例如一些 ttf 字体和 apk 的体积,应该可以减少 file cache 的占用。虽然 file cache 也可以算上可用内存,但是多余的资源占用的 file cache 还是比不上 free 内存好。这部分占时还没怎么研究,不太清楚效果,只是根据前面的理论分析得出一个思路。

待补充。

虚拟机参数调优

待补充。

lmk 参数调优

待补充。

多媒体

在低内存方案如何在解码一些大分辨率、高帧率视频,可能会存在解码器占用内存过多的情况。这个时候可以适当的降低一些解码器的 buffer 数量来减少内存占用。但是好像会稍微降低视频的平滑度。例如说之前 H313 512M 方案预研的时候曾经将 vbv buffer 调整了8M,去掉降噪模块,减少3个 framebuffer。这个具体需要多媒体的同事来分析、优化。而且这个优化只针对视频播放场景(解码场景),不过我们的产品关键场景就是视频播放。

参考资料

(1). dumpsys meminfo 源码(Android 10): android/frameworks/services/core/java/com/android/server/am/ActivityManagerService.java: dumpApplicationMemoryUsage()
(2). dumpsys meminfo pid 源码(Android 10): android/frameworks/base/core/java/android/app/ActivityThread.java: dumpMemInfoTable()
(3). procrank 源码(Android 10): android/system/core/libmeminfo/tools/procrank.cpp
(4). /PROC/MEMINFO之谜
(5).Kernel Zram 说明
(6).Android Zram 说明
(7).zram 简介
(8).Android 进程内存分配说明
(9).Linux内存管理 — /proc/{pid}/smaps讲解
(10).利用/proc/pid/pagemap将虚拟地址转换为物理地址
(11).pagemap和VSS/USS/PSS/RSS的计算
(12).PageMap统计工具GitHub

文章目录
  1. 1. 统计方法
    1. 1.1. dumpsys meminfo
      1. 1.1.1. Total RAM:
      2. 1.1.2. Free RAM:
      3. 1.1.3. Used RAM:
      4. 1.1.4. ZRAM:
      5. 1.1.5. Lost RAM:
      6. 1.1.6. Tuning:
      7. 1.1.7. dumpsys meminfo pid:
    2. 1.2. procrank
      1. 1.2.1. 进程列表:
      2. 1.2.2. ZRAM:
      3. 1.2.3. RAM:
    3. 1.3. /proc/pid/maps
    4. 1.4. /proc/pid/smaps
    5. 1.5. /proc/meminfo
    6. 1.6. PageMap
    7. 1.7. ion
    8. 1.8. Kernel used
    9. 1.9. Kernel reserved
    10. 1.10. MAT
    11. 1.11. malloc_debug
    12. 1.12. 工具小结
      1. 1.12.1. 内存总计
      2. 1.12.2. 应用内存分析
  2. 2. 优化思路
    1. 2.1. Kernel
      1. 2.1.1. Kernel used
      2. 2.1.2. Kernel reserved
      3. 2.1.3. 快速回收
    2. 2.2. Android
      1. 2.2.1. 删除无用模块
      2. 2.2.2. 精简模块内容
      3. 2.2.3. 资源瘦身
      4. 2.2.4. 虚拟机参数调优
      5. 2.2.5. lmk 参数调优
    3. 2.3. 多媒体
  3. 3. 参考资料