5. Android 内存管理

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

也可以通过namebundle 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的内存使用。主要是SurfaceViewTextureView的总和。它也包括了帧缓冲区,因此大小也会取决于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插件。原生堆很难知道内存消耗具体来自哪,也没有很好的方法在剖析器中查看分配。一个可行的方法是将每个插件独立出来,和一个空项目进行对比得到具体的 消耗。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,072评论 25 707
  • 操作系统对内存的管理 没有内存抽象的年代 在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物...
    Mr槑阅读 16,695评论 3 24
  • 10.5 Output Formats默认情况下,gdb根据变量的数据类型来打印变量,不过有时这并不是你想要的。比...
    YummyDog阅读 626评论 0 50
  • “黎明即起,洒扫庭除,且务要整洁”,在《颜氏家训》中明确写道,这是古人的勤俭治家之道,是每天要做的第一要事。在家乡...
    西岭布衣阅读 574评论 1 3
  • 3.1 抽象类 抽象类是从多个类中抽象出来的模板。 抽象方法和抽象类的规则如下: - 抽象类必...
    王毅巽阅读 324评论 0 0