Android的内存被多个进程共享。一个进程使用了多少的内存并不能一眼就看出。Android内存管理比较复杂,但是你可以在继续阅读之前可以参考Google IO上的视频讲座。
Android上的分页
分页技术的详细介绍需要参考操作系统的相关知识,wikipedia链接。简单来讲,分页的技术是将主存的内存移动到二级存储,或者将二级存储的内存移动到主存。
Android在移出分页到硬盘的时候,并没有使用到交换空间(swap space),这造成了很难观测到整个的内存消耗,尤其是Android中的每个应用都在一个Dalvik虚拟机上的独立进程上运行。
分页技术 vs 交换空间
Android用到了分页技术,但是没有使用交换空间。分页技术非常依赖内存映射(mmap()
)文件和将内核页按需存储到数据中。尽管这种情况不会经常发生,当内存不足的时候,分页需要减少内核分页,系统减少缓存的分页文件。Android在将脏的内存页移除的时候并没有使用交换空间,因为如果这样,在移动设备上回导致电池消耗并且导致内存过多的创建和销毁。
机载闪存
Android设备通常搭载的闪存比较小,只有有限的空间来存储数据。虽然这部分空间主要是用来存储应用数据,但是也可以用来存储交换文件。机载闪存比较慢而且相对于硬盘或者闪存设备而言访问稳定性也更差。尽管机载的闪存最近有了 很大的提升,但是还不足以有效承载交换空间的功能。一个简单的实践规则而言,对于1~2MB的RAM而言,交换文件大小应该差不多512MB。你可以修改内核.config(CONFIG_SWAP)然后自己编译内核,这部分的内容已经超出了这篇指南的范围。
内存消耗的限制
怎么知道在Android启动内存杀手关掉进程之前,你的app能够使用多少内存呢?很不幸,这个问题没有简单的答案,找到这个问题的答案需要涉及到非常多的剖析工作,使用dumpsys,procrank和Android Studio来分析才能知道。
许多不同的因素也会影响到对Android内存消耗的测量,如:
- 对于低、中、高设备不同的平台配置
- 在同一台测试设备上安装不同的操作系统OS
- 当你测量内存处在应用的不同阶段
- 整体的设备内存压力
所以在测量内存的时候,就需要保证在同一份代码,相同的平台配置,相同的操作系统版本,以及设备内存压力相等的情况。
低/高内存压力
一个剖析内存比较好的方法是确保设备有足够的内存空余(低内存压力)的时候对应用进行消耗内存的剖析。如果设备没有足够空闲内存,很难得到稳定的测试结果。需要记住的是,尽管你尝试使用剖析工具找到引起高内存的原因,但是同样也会受到硬件条件的限制。如果系统正在调整内存缓存的时候,这时候剖析的结构也是不稳定的。
Dumpsys
如果你想要每个进程消耗的实际的RAM,然后将所有的进程RAM加起来,最后的数字肯定会比实际上总的RAM要高。通过dumpsys
,你可以得到关于每个Java进程更加清晰的信息。dumpsys
提供的统计信息包含关于这个app内存的很多信息。dumpsys
是运行在设备上,转储系统服务和应用的状态信息的Android工具。dumpsys
允许你轻松获取系统信息。
- 获取到的系统信息以字符串的形式呈现
- 使用CPU,RAM,电池和存储转储的信息来检查应用如何影响整个设备。
下面的命令会列出dumpsys
找到的所有服务:
~$ adb shell dumpsys | grep "dumpsys services"
在Android上,你可以使用dumpsys meminfo
来查看Android的系统内存。
dumpsys meminfo
abd提供了许多工具来获取Android上运行的应用信息。最常用的方式是获取总览,使用adb shell dumpsys meminfo
命令。它会提供关于每个Java进程的内存使用,原生堆,二进制数据以及很多进程和系统信息。下面的命令会提供系统内存的快速总览:
~$ adb shell dumpsys meminfo
也可以通过name,bundle ID或者pid来追踪单个进程。例如如果想要追踪Unity的androidtest这个应用的信息,可以使用下面的命令。androidtest是一个空的Unity工程,只有一个空场景,没有天空盒和任何内容,只是作为内存的基准线。
~$ adb shell dumpsys meminfo com.unity.amemorytest
测试打印的结果如下(测试平台:Nexus 6P 2560 by 1440 px,Android 8.1.0,Unity 2018.1)
* Applications Memory Usage (in Kilobytes):
* Uptime: 6815563691 Realtime: 10882940478
*
* ** MEMINFO in pid 20676 [com.unity.androidtest] **
* Pss Private Private SwapPss Heap Heap Heap
* Total Dirty Clean Dirty Size Alloc Free
* ------ ------ ------ ------ ------ ------ ------
* Native Heap 31467 31448 0 0 51072 47261 3810
* Dalvik Heap 1872 1760 0 0 12168 7301 4867
* Dalvik Other 470 460 0 0
* Stack 492 492 0 2
* Ashmem 8 0 0 0
* Gfx dev 3846 2036 0 0
* Other dev 4 0 4 0
* .so mmap 17760 516 15908 161
* .jar mmap 4 0 4 0
* .apk mmap 243 0 0 0
* .dex mmap 116 4 112 0
* .oat mmap 6206 0 3244 0
* .art mmap 2571 716 232 22
* Other mmap 49 4 0 2
* EGL mtrack 99840 99840 0 0
* GL mtrack 64480 64480 0 0
* Unknown 1270 1264 0 14
* TOTAL 230899 203020 19504 201 63240 54562 8677
*
* App Summary
* Pss(KB)
* ------
* Java Heap: 2708
* Native Heap: 31448
* Code: 19788
* Stack: 492
* Graphics: 166356
* Private Other: 1732
* System: 8375
*
* TOTAL: 230899 TOTAL SWAP PSS: 201
*
* Objects
* Views: 7 ViewRootImpl: 1
* AppContexts: 2 Activities: 1
* Assets: 2 AssetManagers: 2
* Local Binders: 16 Proxy Binders: 21
* Parcel memory: 5 Parcel count: 23
* Death Recipients: 1 OpenSSL Sockets: 2
* WebViews: 0
*
* SQL
* MEMORY_USED: 0
* PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
*
如果在一个包含完整的3D场景的应用,使用同样的命令会得到更多的信息:
* Applications Memory Usage (in Kilobytes):
* Uptime: 6823482422 Realtime: 10890859209
*
* ** MEMINFO in pid 22903 [com.unity3d.androidtest] **
* Pss Private Private SwapPss Heap Heap Heap
* Total Dirty Clean Dirty Size Alloc Free
* ------ ------ ------ ------ ------ ------ ------
* Native Heap 304918 304900 0 0 327552 315885 11666
* Dalvik Heap 1240 1096 0 0 11858 7127 4731
* Dalvik Other 424 412 0 0
* Stack 528 528 0 1
* Ashmem 6 0 0 0
* Gfx dev 196934 132128 0 0
* Other dev 4 0 4 0
* .so mmap 23976 668 21920 199
* .apk mmap 368 0 0 0
* .dex mmap 116 4 112 0
* .oat mmap 6060 0 3768 0
* .art mmap 2774 604 332 25
* Other mmap 44 4 0 2
* EGL mtrack 21600 21600 0 0
* GL mtrack 384184 384184 0 0
* Unknown 6577 6568 0 17
* TOTAL 949997 852696 26136 244 339410 323012 16397
*
* App Summary
* Pss(KB)
* ------
* Java Heap: 2032
* Native Heap: 304900
* Code: 26472
* Stack: 528
* Graphics: 537912
* Private Other: 6988
* System: 71165
*
* TOTAL: 949997 TOTAL SWAP PSS: 244
*
* Objects
* Views: 7 ViewRootImpl: 1
* AppContexts: 3 Activities: 1
* Assets: 2 AssetManagers: 2
* Local Binders: 15 Proxy Binders: 20
* Parcel memory: 3 Parcel count: 14
* Death Recipients: 0 OpenSSL Sockets: 0
* WebViews: 0
*
* SQL
* MEMORY_USED: 0
* PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
*
下面的表格比较了两份数据的主要差异和详细的原因:
Area | Empty Scene [MB] | Full Scene [MB] | Description |
---|---|---|---|
Pss | 230 | 949 | Proportional set size是内核计算的度量,它将内存共享考虑在内。系统会根据内存页中的其他进程使用的比例动态调整每个RAM内存页。对于共享内存页而言,系统会将共享的内存页大小乘上这个进程在所有使用这个内存页的总进程数所占的比例。所有的私有内存页贡献100%的大小,共享内存则贡献了(共享的内存页大小)/(共享进程数)。例如,一个在两个进程间共享的内存页像每个进程的Pss贡献了其大小的一半。按照这种方式你就可以将所有进程的Pss数据相加得到总共的RAM消耗。在进程间比较Pss也能够大致比较两者权重。 |
Private Dirty | 203 | 825 | 最有意思和最昂贵的度量应该是Private Dirty,这部分代表着进程内不能用磁盘上的数据备份,所以不能被分页到硬盘,也不能和其他的进程共享的内存。另一个角度理解,就是这部分的消耗内存只有当应用被销毁的时候才会被系统回收。回收之后,立刻被分配到缓存和其他地方使用,因为系统要充分利用有限的内存 |
Native Heap | 51 | 328 | 原生堆代表进程自身使用的内存,例如Unity Engine Code, Native C malloc, 和 Mono VM。 |
Dalvik Heap | 12 | 12 | Dalvik Heap代表Dalvik虚拟机分配的内存,例如Unity中Java形式的Android代码中的变量。 |
Dalvik Other | 0.4 | 0.4 | JIT和Android GC使用的内存 |
Clean Memory | 19 | 26 | Android会在几个进程(例如公共框架)之间共享内存页。只要一个页内的内存改变,系统必须要写入而且将内存块标记为脏。然而,clean memory代表当从磁盘载入的时候还没有改变的内存。只有发生改变,内存标记成脏内存。 |
Swapped Dirty | 0.2 | 0.2 | 应用使用Dirty Memory来进行计算任务。Android没有交换区机制,所以Dirty Memory也是RAM部分,只有当应用退出的时候才会被释放。尽管如此,Swapped Dirty在部分Android设备上被用来做remap,但是也是交换到RAM而不是闪存。在Android上,这个部分和Linux类似,ZRAM可以压缩页,Linux内核将这些部分放到特殊的RAM区域,当需要的时候再次解压。 |
EGL mtrack | 99 | 22 | 这个部分是gralloc的内存使用。主要是SurfaceView和TextureView的总和。它也包括了帧缓冲区,因此大小也会取决于framebuffers的尺寸。支持的屏幕分辨率越高,EGL mtrack的数目越高。在这个测试中,帧缓冲区的分辨率被降低了来确保比较好的性能。降低帧缓存的大小也会降低这些缓存需要的内存量。 |
GL mtrack & Gfx dev |
69 | 581 | GL和Gfx是驱动反馈的GPU内存,主要是GL纹理大小的总和,GL命令缓冲区,固定的全局驱动RAM消耗以及Shader。需要指出,这些不会出现在旧的Android版本上。注意:客户空间驱动和内核空间驱动共享同一个内存空间。在某些Android版本上,这个部分会被重复计算两次,因此Gfx dev要比实际上使用的数值更大。 |
Unknown | 1.3 | 6.5 | Unknown值得是系统不能确定分页属于上面的那种。这个部分包括原生分配或者运行时metadata,因为Adress Space Layout Randomization,这个工具没办法确定内存。Private Dirty是只用于自己应用的未知RAM。 |
procrank
另一个可以取代dumpsys的工具是procrank,这个工具可以查看所有进程使用的内存量,从高到低列出每个进程使用的内存使用量。每个进程列出的Vss,Rss,Pss,和Uss。
~$ adb shell procrank
* PID Vss Rss Pss Uss cmdline
* 890 84456K 48668K 25850K 21284K system_server
* 1231 50748K 39088K 17587K 13792K com.android.launcher2
* 947 34488K 28528K 10834K 9308K com.android.wallpaper
* 987 26964K 26956K 8751K 7308K com.google.process.gapps
* 954 24300K 24296K 6249K 4824K com.unity.androidmemory
* 888 25728K 25724K 5774K 3668K zygote
* 977 24100K 24096K 5667K 4340K android.process.acore
- Vss - Virtual Set Size
一个进程总共可以获取的寻址空间。这个指标表明一个进程关联的虚拟内存空间。 - Rss - Resident Set Size
一个进程分配到的物理内存页。被多个进程共享的内存页被计算多次。 - Pss - Proportional Set Size
和Rss类似,只不过将共享的内存页处理共享的进程数来计算。 - Uss - Unique Set Size
= Private Dirty的概念,表明进程内不能被分页到硬盘的数据,不能被其他进程共享的内存部分。
meminfo
meminfo命令给出系统总内存使用情况。
~$ adb shell cat /proc/meminfo
前四个数值值得关注:
* MemTotal: 2866492 kB
* MemFree: 244944 kB
* Buffers: 36616 kB
* Cached: 937700 kB
* SwapCached: 13744 kB
- MemTotal 是内核和用户区可以获取的内存总量,这个值逼实际的物理RAM要小,因为GSM,缓冲区等这些也需要消耗内存。
- MemFree 是目前没有被使用的RAM量。在Android,这个值通常比较小,因为系统总是尝试使用所有可用内存保持进程运行。
- Cached 是用来作为文件系统缓存的RAM。
额外信息参照
RAM investigation page
Android performance guides
Android Studio
Android Studio除了SDK中的命令行工具,也提供了一个内存查看工具。和命令行提供的功能类似。
Android Studio内存查看工具.png
我们使用到的例子是在dumpsys meminfo中的空工程在Android Studio中得到的数据。基本上包括了使用dumpsys可以得到的信息,再加上一些额外信息。
Section | Size[MB] | Area |
---|---|---|
Total[mb] | 168.7 | All |
Others[mb] | 3.1 | Other dev + Unknown |
Code[mb] | 28 | mmaps |
Stack[mb] | 0.1 | Stack |
Graphics[mb] | 88.7 | Gfxdev + EGL mtrack + GL mtrack |
Native[mb] | 40.8 | Native Heap |
Java[mb] | 8 | Dalvik Heap |
Plugin
通常来讲,大部分的内存消耗来自于原生堆。Dalvik堆相对于原生堆来讲很小。如果比较大,需要检查一下你使用的Android插件。原生堆很难知道内存消耗具体来自哪,也没有很好的方法在剖析器中查看分配。一个可行的方法是将每个插件独立出来,和一个空项目进行对比得到具体的 消耗。
网友评论