Android内存优化之极致清理

4,374 阅读9分钟

Android内存优化之极致清理

阅读这篇文章你将了解到以下内容

  1. Android app 内存信息的获取方式以及各种方式最终的获取原理。
  2. 如何解析smaps文件自定义计算更加详细的内存占用信息。
  3. .dex mmap内存占用优化,如何通过简单的hook 获取到类加载信息,加载类所在dex文件地址。
  4. .dex mmap内存优化终极方案 3行代码优化。

背景

小米小组件进程内存占用PSS必须小于35M dev.mi.com/console/doc…

image.png

优化

了解android 内存指标获取原理

优化前进程内存信息dump

image.png 上图我们可以看出pss total占用了135M超出了100M

简单优化

针对上面情况我们很容易想到 在子进程application阶段 移除非必要功能。通过裁剪能裁剪的代码和功能我们得到了以下优化效果。

image.png

提交小米测试后很快被打回了,测试反馈使用内存超过了51M ,看了下测试脚本是1秒取一次内存,结果取最大值,小米这边测试51M是关了主进程测试的,而我们自己测试是开着主进程。 这是因为Pss是平摊计算后的使用内存(有些内存会和其他进程共享,例如mmap进来的),我们开多个进程后共享内存就会被平均到其他进程。 关闭主进程后测试内存占用能到56M

image.png

继续分析优化

上面我们发现Code部分占用了37M,Java Heap部分占用了10M

Java heap问题分析

Java heap = dalvik private_dirty(772)+art mmap private_dirty(10132) +art mmap private_clean(84)

结合hprof文件分析,app heap占用仅1.2M 主要在image heap上,可以分析出Class加载是主要原因。

image.png

image.png

Code 问题分析

Code问题主要分析.dex mmap 34680/56526 = 61%

了解android 内存指标获取原理后,我们手动解析一些smaps文件 看下.dex 分布。

从之前的测试包数据看 内存占用主要在base.vdex上,这部分是dex加载也和class加载相关

image.png vdex文件

主要目的:降低dex2oat执行耗时 1、当系统OTA后,对于安装在data分区下的app,因为它们的apk都没有任何变化,那么在首次开机时,对于这部分app如果有vdex文件存在的话,执行dexopt时就可以直接跳过verify流程,进入compile dex的流程,从而加速首次开机速度; 2、当app的jit profile信息变化时,background dexopt会在后台重新做dex2oat,因为有了vdex,这个时候也可以直接跳过

vdex就是verified dex,包含 raw dex +(quicken info)

优化思路

  1. 通过applicaion 修改,除去widgetProvider进程不必要的类加载 从Hprof文件分析。还是有加载一些不必要的类。

  2. 对dex顺序做调整 ,类名混淆。

由于dex文件在生成时按字母顺序排列。由于4KB页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据。例如在代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面。但由于A1的数据只有1KB,那在加载的4KB页面中,还会有A2A3A4类,总共占用了4KB内存。

假设我们的代码里在用到A1类后,还会用到B1、C1、D1类,那么如果能在dex文件中将A1、B1、C1、D1类放在一起,虚拟机就只需要加载一个4KB页面,优化的思路就是调整dex文件中数据的顺序,将能够用到的数据紧密排列在一起,Proguard工具能够对类名进行修改,根据程序运行的逻辑将那些会互相调用的类改为同一个packag名,这样就可以使他们的数据排列在一起。

优化实践

代码修改
  1. 通过对比hprof文件和smali文件发现 BaseApplicatoin中存在aspectj, Fastjson等代码,子进程中移除。
  2. gson代码转为jsonobject实现

结论基本没什么降低

包名重混淆测试
  1. 排查到四个组件下keep规则 影响到类名混淆。
  2. 加入包名类名反混淆后包体减少1.6M

内存测试结论基本没什么降低

深入优化

这一步我们主要就是去要研究base.vdex 里面的内存占用是如何计算的。

vdex格式介绍

image.png 这边hook打印下打开.vdex文件的堆栈。然后从这附近去查看相关源码OpenAtAddress

image.png

ART 类加载日志hook

base.vdex是通过mmap的方式加载的,那就是在实际访问的时候也就是类加载的时候才会触发内存加载,现阶段我们想知道的是加载了哪些类之后导致内存占用上去了 ,其实art中本身有类加载的一些日志

image.png 不过这class_linker开关默认是关的, 我们通过xdl找到这个全局属性后修改class_linker为true

image.png Class load 日志。 拿到class load日志后很容易可以看出某个类什么时候从哪个dex加载的。(额外提一下,这个日志在我们排查framework的问题时也特别有用,我们可以根据问题类拿到jar包或apk进行反编译排查问题)

所以我们的优化思路就是减少类加载的dex数量。 对load类进行移除测试内存 ,对一些其他dex类进行移除后可以发现,如下图内存占用确实降了下来。

结论:测试包code内存占用明显降低

testflight打包后 内存未明显降低

初步发现线程监控 和神策有代码注入 导致dex加载个数又变多了。

测试包发现一段时间后 未加载类 code占用也提升了。

建议内核释放vdex rss

跟踪源码发现 系统可能会有使用madvise预载操作。

image.png 尝试通过madvise_dontneed直接discard filed-backed VMA数据

image.png

image.png

结论:vdex峰值降低10M 一段时间后.dex mmap 内存pss占用显著降低

最终方案

我们在小米机型上使用madvise_dontneed方案释放base.vdex内存。

  1. 代码仅在小米组件进程生效,不影响主进程。
  2. 通过art 类加载日志可以知道组件进程加载的类很少,所以使用时再通过mmap load到内存对组件进程性能影响也很小。
  3. 小米侧内存测试Pass

Android内存指标的获取原理

常用的查看内存信息方式

  1. 查看android studio 的profile面板
  2. 利用多啦A梦等客户端工具
  3. 利用 adb shell dumpsys meminfo <pid_of_app>命令

Android studio profile 实现

android.googlesource.com/platform/to…

获取代码在

profiler/native/perfd/memory/memory_usage_reader_impl.cc

通过

dumpsys meminfo --local --checkin pid 实现

客户端代码调用获取api

private int getPss(int pid){

ActivityManager am =(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)

MemoryInfo meminfo = am.getProcessMemoryInfo(pid)[0];

return meminfo.getTotalPss();

}
Debug.MemoryInfo memInfo = new Debug.MemoryInfo();

Debug.getMemoryInfo(memInfo);

Adb dumpsys meminfo

image.png

内存获取方式的原理

这里就不逐个去分析各种获取方式的源码了,先给出结论再结合adb dumpsys方案分析

跟源码后每种指标获取方式都指向了一个文件proc/pid/smaps

读取进程smaps的节点, /proc/pid/smaps 我们app中这个文件有9w+行

image.png /proc/PID/smaps 文件是基于 /proc/PID/maps 的扩展,他展示了一个进程的内存消耗,比同一目录下的maps文件更为详细。

(图片来源网络)

image.png 根据属性的不同,进程的虚拟地址空间被划分成若干个vma,每一个vma通过vm_next和vm_prev组成双向链表,链表头位于进程的task->mm->mmap.当通过proc接口读取进程的smaps文件时,内核会首先找到该进程的vma链表头,遍历链表中的每一个vma, 通过walk_page_vma统计这块vma的使用情况,最后显示出来。

image.png

smaps字段介绍.
  • size 是进程使用内存空间,并不一定实际分配了内存(VSS)
  • Rss是实际分配的内存(不需要缺页中断就可以使用的)
  • Pss是平摊计算后的使用内存(有些内存会和其他进程共享,例如mmap进来的)
  • Shared_Clean 和其他进程共享的未改写页面
  • Shared_Dirty 和其他进程共享的已改写页面
  • Private_Clean 未改写的私有页面页面
  • Private_Dirty 已改写的私有页面页面
  • Referenced 标记为访问和使用的内存大小
  • Anonymous 不来自于文件的内存大小
  • Swap 存在于交换分区的数据大小(如果物理内存有限,可能存在一部分在主存一部分在交换分区)
  • KernelPageSize 内核页大小
  • MMUPageSize MMU页大小,基本和Kernel页大小相同

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存

image.png

Adb dumpsys part 1

smap中内存分类枚举 load_maps源码地址,这里会按照name的命名规则来过滤不同类型的内存 进行分组。我这边把过滤规则贴到了类型后面。adb dumpsys图中第一部分就是使用分组后的数据计算而来。

Adb dumpsys part 2
  long nativeMax = Debug.getNativeHeapSize() / 1024;

            long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024;

            long nativeFree = Debug.getNativeHeapFreeSize() / 1024;



            Runtime runtime = Runtime.getRuntime();

            runtime.gc();  // Do GC since countInstancesOfClass counts unreachable objects.

            long dalvikMax = runtime.totalMemory() / 1024;

            long dalvikFree = runtime.freeMemory() / 1024;

            long dalvikAllocated = dalvikMax - dalvikFree;
Adb dumpsys part 3

gpu这部分内存需要通过gpu 文件节点获取。

image.png

Adb dumpsys part 4
  pw.println(" App Summary");

        printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "Pss(KB)", "", "Rss(KB)");

        printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "------", "", "------");

        printRow(pw, TWO_COUNT_COLUMNS,

                "Java Heap:", memInfo.getSummaryJavaHeap(), "", memInfo.getSummaryJavaHeapRss());

        printRow(pw, TWO_COUNT_COLUMNS,

                "Native Heap:", memInfo.getSummaryNativeHeap(), "",

                memInfo.getSummaryNativeHeapRss());

        printRow(pw, TWO_COUNT_COLUMNS,

                "Code:", memInfo.getSummaryCode(), "", memInfo.getSummaryCodeRss());

        printRow(pw, TWO_COUNT_COLUMNS,

                "Stack:", memInfo.getSummaryStack(), "", memInfo.getSummaryStackRss());

        printRow(pw, TWO_COUNT_COLUMNS,

                "Graphics:", memInfo.getSummaryGraphics(), "", memInfo.getSummaryGraphicsRss());

        printRow(pw, ONE_COUNT_COLUMN,

                "Private Other:", memInfo.getSummaryPrivateOther());

        printRow(pw, ONE_COUNT_COLUMN,

                "System:", memInfo.getSummarySystem());

        printRow(pw, ONE_ALT_COUNT_COLUMN,

                "Unknown:", "", "", memInfo.getSummaryUnknownRss());

        pw.println(" ");

        if (memInfo.hasSwappedOutPss) {

            printRow(pw, THREE_COUNT_COLUMNS,

                    "TOTAL PSS:", memInfo.getSummaryTotalPss(),

                    "TOTAL RSS:", memInfo.getTotalRss(),

                    "TOTAL SWAP PSS:", memInfo.getSummaryTotalSwapPss());

        } else {

            printRow(pw, THREE_COUNT_COLUMNS,

                    "TOTAL PSS:", memInfo.getSummaryTotalPss(),

                    "TOTAL RSS:", memInfo.getTotalRss(),

                    "TOTAL SWAP (KB):", memInfo.getSummaryTotalSwap());
  • Java Heap

    • // dalvik private_dirty dalvikPrivateDirty // art mmap private_dirty + private_clean + getOtherPrivate(OTHER_ART);
  • Native Heap

    • libc_malloc
  • Code

    • // so mmap private_dirty + private_clean getOtherPrivate(OTHER_SO) // jar mmap private_dirty + private_clean + getOtherPrivate(OTHER_JAR) // apk mmap private_dirty + private_clean + getOtherPrivate(OTHER_APK) // ttf mmap private_dirty + private_clean + getOtherPrivate(OTHER_TTF) // dex mmap private_dirty + private_clean + getOtherPrivate(OTHER_DEX) // oat mmap private_dirty + private_clean + getOtherPrivate(OTHER_OAT);
    • 所有私有静态资源求和
  • Stack

    • // stack private_dirty getOtherPrivateDirty(OTHER_STACK);
    • 进程本身栈占用的大小。
  • Graphic

    • //Gfx Dev private_dirty + private_clean getOtherPrivate(OTHER_GL_DEV) // EGL mtrack private_dirty + private_clean + getOtherPrivate(OTHER_GRAPHICS) // GL mtrack private_dirty + private_clean + getOtherPrivate(OTHER_GL);
  • System

    • Pss Total 的和- Private Dirty 和Private Clean 的和

系统占用的内存,例如一些共享的字体,图像资源等。