Android 应用内存分析(一)

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

这里我们选取手机上的一个应用Phoneix查看一下其内存占用情况: 执行命令:

adb shell dumpsys meminfo com.trassion.phoneix 

显示结果如下:

image.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中的数值是从上边的第一个表格的数值计算而来,那第一个表格的数值又是从何而来呢? 我们接着往下看!!

    二 查看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文件:

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

    我从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做进一步分析, 此部分相对比较简单。

  • 三 smaps文件

    Linux在2.6版本之后有一个proc伪文件,在它下面记录各种信息,其中在proc/pid/smaps记录某个pid的内,smaps记录内存详细的使用情况。使用下面的命令可以读文件的值

    cat /proc/pid/smaps
    

    文件格式如下:

  • 400ca000-400cb000:本段虚拟内存的地址范围
  • r-xp :文件权限,r(读)、w(写)、x(执行)、p表示私有,s代表共享,如果不具有哪项权限用"-"代替
  • 00000000 :映射文件的偏移量
  • b3:11 :文件设备号
  • 1345 :被映射到虚拟内存文件的映索节点
  • /system/lib/libplddbgutil.so:文件名称
  • Size:相应虚拟地址空间的大小
  • RSS: 正在使用的物理内存的大小
  • Shared_Clean: Rss中和其他进程共享的未使用页数
  • Shared_Dirty: Rss和其他进程共享已经使用的页数
  • Private_Clean: Rss私有区域未使用的页数
  • Private_Dirty: Rss私有区域已经使用的页数