Android Memory(二) -- 应用内存占用分析

6,765 阅读6分钟

Android-Go.jpeg

前言

     在工作这几年,我一直深受内存问题的困扰,在和内存的不断抗争中,我逐渐积累了一些内存的知识,接来下来我会用几篇文章简单记录一下这几年的我学到的内存相关的经验。另外,本系列文章不去过多的分析Linux底层代码,只是探讨遇到内存问题时的解决方法论。 以下是全部文章的标题和链接:

  1. Android Memory(一) -- 内存基础知识
  2. Android Memory(二) -- 应用内存占用分析
  3. Android Memory(三) -- 问题原因分析
  4. Android Memory(四) -- 问题定位&解决方案1
  5. Android Memory(五) -- 问题定位&解决方案2
  6. Android Memory(六) -- 内存日常监控

应用内存占用分析

1. 通过Android Studio自带的Memory Profiler查看内存

     日常开发中,我们在监控应用内存占用是,经常使用的是AS自带的Memory Profiler, 如下图所示: 1.png 其中各个按钮的用法如下:

  1. 强制执行垃圾回收
  2. 堆转储,把内存信息通过文件的方式保存下来,可以进行分析
  3. 记录内存分配情况, 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示
  4. 放大/缩小时间线
  5. 跳转至实时内存数据
  6. Event 时间线,显示 Activity 状态、用户输入 Event 和屏幕旋转 Event\
  7. 内存使用量时间线,其包含以下内容:
  • 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示
  • 虚线表示分配的对象数,如右侧的 y 轴所示
  • 用于表示每个垃圾回收 Event 的图标

     我们可以明显的看到,在上图有当前应用每一部分所占用的内存,其中包括Other、Code、Stack、Graphics等部分的内存占用情况,那么这几部分的内存占用是怎么得到的呢,我们接下来会做进一步的分析。

2. 通过 adb shell dumpsys meminfo ${PROCESS_NAME} 查看内存情况

     这里我们选取手机上的一个应用Phoneix查看一下其内存占用情况: 执行命令:adb shell dumpsys meminfo com.trassion.phoneix 显示结果如下:

2.png

在上图中,我们可以看到App Summary部分,就是通过Memory Profiler界面上查看到的数值。 那么,问题来了,App Summary部分又是怎么计算得来的呢?

在上图中,我们注意到了App Summary上边的第一个表格的数据,莫非是根据这一部分计算来的吗? 答案是Yes,下面我们给出详细的计算规则:

Java Heap = Dalvik Heap private dirty+ .art mmap private (clean+ dirty) = 27412 + 4 + 7824 = 35240

Native Heap = Native private dirty = 98156

Code = .so mmap private (clean + dirty) + .jar mmap private (clean + dirty) + .apk mmap private (clean + dirty) + .ttf mmap private (clean+ dirty) + .dex mmap private (clean + dirty) + .oat mmap private (clean + dirty) = (520 + 2348) + (8 + 500) + (60 + 1764) + (0 + 48) + (15808 + 2316) + (0 + 0) = 23440

Stack = Stack private dirty = 6144 

Graphics = GL mtrack private (clean + dirty) + EGL mtrack private(clean + dirty) = (0 + 0) + (28769 + 0) = 28769

Private other= TOTAL private (clean + dirty) - Java Heap - Native Heap- Code- Stack -Graphic = (205245 + 7780) - 35240 - 98156 - 23440 - 6144 - 28769 = 21276

System = TOTAL - TOTAL private (clean + dirty)  = 309121 - (205245 + 7780) = 96096

  • Private Dirty     进程本身使用的内存总数,包含了进程主动申请的以及修改的继承自Zygote的内存。其实Private Dirty表示了该进程私有的,不跟Disk数据一致的内存段。例如堆(heap),栈(stack),bss段。

注:在新平台上,用于管理Dalvik的内存(如, just-in-time compilation (JIT) and GC bookkeeping)不再像以前一样归到 Dalvik Heap,而是归类到 Dalvik Other。

  • Private clean     进程独自使用的so和dex。Clean内存的好处是在内存紧张时,可以释放物理内存。因为是clean的,所以不需要写回到disk,只需要下次读取该内存(导致缺页错误)时再从disk读入。

  • Heap Size/ Heap Alloc/ Heap Free     Heap Alloc是(Dalvik、native)app申请的内存记录,包括了Private Dirty和继承自Zygote的(多进程共享的)内存。所以,它是比Pss Total和Private Dirty都要大的。

我们知道了Memory Profiler中的数值是从上边的第一个表格的数值计算而来,那第一个表格的数值又是从何而来呢? 我们接着往下看!!

3. 通过adb shell run-as com.transsion.phoenix "cat /proc/${PID}/smaps 查看Smap数据

     这里我们选取手机上的一个应用Phoneix查看一下其内存占用情况: 我这边实现了一个简单的脚本如下:

#! /bin/bash
process_name=$1
PID=$(adb shell pidof ${process_name})
adb shell run-as com.transsion.phoenix "cat /proc/${PID}/smaps > /data/local/tmp/smaps.txt"
adb pull /data/local/tmp/smaps.txt $2

执行 bash pull_smaps.sh com.transsion.phoneix 1.0.0_smaps.txt

我们可以简单看一下相关的smap文件:

image.png

看起来是不是一头雾水,这一个个的内存区间都是干什么用的?

我从github上copy来一个python3的脚本,专门做smap数据的解析,目前作者已经找不到的!

解析结果如下(解析结果较长,我将部分解析结果做了省略):

Unknown : 9.861 M
	pss: 8.443 M
	swapPss: 1.418 M
		[anon:partition_alloc] : 8520 kB
		[anon:.bss] : 935 kB
		[anon:linker_alloc] : 189 kB
		[anon:bionic_alloc_small_objects] : 120 kB
		[anon:thread signal stack] : 24 kB
		[anon:cfi shadow] : 24 kB
		[anon:System property context nodes] : 20 kB
		[anon:atexit handlers] : 9 kB
		[anon:arc4random data] : 8 kB
		[anon:bionic_alloc_lob] : 4 kB
Dalvik : 30.602 M
	pss: 29.222 M
	swapPss: 1.380 M
		[anon:dalvik-main space (region space)] : 23336 kB
		[anon:dalvik-free list large object space] : 4233 kB
		[anon:dalvik-zygote space] : 2525 kB
		[anon:dalvik-non moving space] : 508 kB
Native : 126.734 M
	pss: 99.627 M
	swapPss: 27.107 M
		[anon:scudo:primary] : 84810 kB
		[anon:scudo:secondary] : 41924 kB
Dalvik Other : 21.381 M
	pss: 18.341 M
	swapPss: 3.040 M
		[anon:dalvik-LinearAlloc] : 10612 kB
		/memfd:jit-cache (deleted) : 6344 kB
		[anon:dalvik-DEX data] : 3364 kB
                ...
                ...
Stack : 8.188 M
	pss: 6.148 M
	swapPss: 2.040 M
		[anon:stack_and_tls:4854] : 168 kB
		[anon:stack_and_tls:4870] : 148 kB
		[stack] : 148 kB
		[anon:stack_and_tls:4878] : 144 kB
		[anon:stack_and_tls:4877] : 144 kB
		...
		...
Cursor : 0.000 M
	pss: 0.000 M
	swapPss: 0.000 M
Ashmem : 0.204 M
	pss: 0.204 M
	swapPss: 0.000 M
		/dev/ashmem/gralloc_shared_memory (deleted) : 50 kB
		/dev/ashmem/MessageQueue (deleted) : 48 kB
		/dev/ashmem/AshmemAllocator_hidl (deleted) : 38 kB
		...
		...
Gfx dev : 0.000 M
	pss: 0.000 M
	swapPss: 0.000 M
Other dev : 0.020 M
	pss: 0.020 M
	swapPss: 0.000 M
		/dev/binderfs/binder : 12 kB
		/dev/binderfs/hwbinder : 8 kB
.so mmap : 9.051 M
	pss: 8.839 M
	swapPss: 0.212 M
		/vendor/lib64/egl/libGLES_mali.so : 4044 kB
		/system/lib64/libhwui.so : 661 kB
		/system/lib64/libstagefright.so : 328 kB
		...
		...
.jar mmap : 2.217 M
	pss: 2.217 M
	swapPss: 0.000 M
		/system/framework/framework.jar : 1241 kB
		/data/data/com.transsion.phoenix/app_pccache/5/3ADD07A77E5BC23D41D5235C3F0C964B75D847A9/pcam.jar : 360 kB
		/apex/com.android.art/javalib/core-icu4j.jar : 265 kB
		/apex/com.android.art/javalib/core-oj.jar : 173 kB
		/apex/com.android.art/javalib/bouncycastle.jar : 78 kB
		...
		...
.apk mmap : 17.179 M
	pss: 16.923 M
	swapPss: 0.256 M
		/data/app/~~l7NnUJ5BUuoASfLO4ps6rQ==/com.google.android.webview-cmrXpqowpCjrrfYc0lesEQ==/base.apk : 14642 kB
		/data/user_de/0/com.google.android.gms/app_chimera/m/0000004d/dl-AdsFdrDynamite.integ_221310604100000.apk : 850 kB
		/data/app/~~WkMxdLO_EFZvRB7Y7O4Tfw==/com.transsion.phoenix-PVevdVSKlTJCD3Q2aDbscQ==/base.apk : 783 kB
		...
		...
.ttf mmap : 0.140 M
	pss: 0.140 M
	swapPss: 0.000 M
		/system/fonts/Roboto-Bold.ttf : 99 kB
		/system/fonts/Roboto-Regular.ttf : 37 kB
		/system/fonts/NotoSansLaoUI-Regular.ttf : 4 kB
.dex mmap : 44.585 M
	pss: 19.645 M
	swapPss: 24.940 M
		[anon:dalvik-classes7.dex extracted in memory from /data/app/~~WkMxdLO_EFZvRB7Y7O4Tfw==/com.transsion.phoenix-PVevdVSKlTJCD3Q2aDbscQ==/base.apk!classes7.dex] : 10932 kB
		[anon:dalvik-classes6.dex extracted in memory from /data/app/~~WkMxdLO_EFZvRB7Y7O4Tfw==/com.transsion.phoenix-PVevdVSKlTJCD3Q2aDbscQ==/base.apk!classes6.dex] : 10864 kB
		[anon:dalvik-classes.dex extracted in memory from /data/app/~~WkMxdLO_EFZvRB7Y7O4Tfw==/com.transsion.phoenix-PVevdVSKlTJCD3Q2aDbscQ==/base.apk] : 9328 kB
		...
		...
.oat mmap : 0.079 M
	pss: 0.079 M
	swapPss: 0.000 M
		/system/framework/arm64/boot-framework.oat : 36 kB
		/apex/com.android.art/javalib/arm64/boot.oat : 19 kB
		/apex/com.android.art/javalib/arm64/boot-core-icu4j.oat : 13 kB
		...
		...
.art mmap : 11.323 M
	pss: 8.984 M
	swapPss: 2.339 M
		[anon:dalvik-/system/framework/boot-framework.art] : 7112 kB
		[anon:dalvik-/apex/com.android.art/javalib/boot.art] : 1711 kB
		[anon:dalvik-/system/framework/boot-telephony-common.art] : 756 kB
		...
		...
Other mmap : 2.297 M
	pss: 2.297 M
	swapPss: 0.000 M
		/system/fonts/NotoSansCJK-Regular.ttc : 1030 kB
		/data/misc/shared_relro/libwebviewchromium64.relro : 626 kB
		/data/data/com.transsion.phoenix/app_webview/BrowserMetrics/BrowserMetrics-6281F38F-12BF.pma : 256 kB
		...
		...

以上合并的计算结果就是第二部分中第一个表的数据,其中有略微的差异, 是因为两个数据dump的时机是有微小的时间间隔导致。另外此部分的合并数据,没有包含GL相关的数据,GL相关的数据需要从显存中读取,此处暂时不做进一步的探讨。

可以清晰的看到,使用smaps统计出来的内存和使用adb shell dumpsys meminfo是一致的,但是smaps聚合统计到的数据,可以清晰的看到哪一个so、ttf、oat所占的内存,这部分信息adb shell dumpsys meminfo是不具有的。

从合并以后的结果来看,我们知道了进程每个部分详细的内存占用情况:

  • 如果是Native部分内存占用的比较高,如果是Android 8.0以上,我们首先去分析Bimap占用的内存是否异常。如果Bitmap占用正常, 那么此部分就主要是通过malloc和mmap进行分配的,我们可以通过Loliprofile或者Malloc Debug进行进一步的堆栈抓取,来解决不合理的内存分配。也可以通过xHook或者字节跳动的Native Hook工具去hook内存分配函数做进一步的内存定位;

  • 如果Code部分占用过多, 我们可以考虑优化包大小或者不合理的字体载入等;

  • 如果Java Heap占用比较多,如果是Android 8.0 以下的设备,可以去看一下Bitmap的占用,如果Bitmap占用是正常的,需要分析是否有不合理的Java引用或者内存泄漏,此部分可以借助Android Studio自带的内存工具或者MAT做进一步分析, 此部分相对比较简单。