Android内存优化之极致清理
阅读这篇文章你将了解到以下内容
- Android app 内存信息的获取方式以及各种方式最终的获取原理。
- 如何解析smaps文件自定义计算更加详细的内存占用信息。
- .dex mmap内存占用优化,如何通过简单的hook 获取到类加载信息,加载类所在dex文件地址。
- .dex mmap内存优化终极方案 3行代码优化。
背景
小米小组件进程内存占用PSS必须小于35M dev.mi.com/console/doc…
优化
了解android 内存指标获取原理
优化前进程内存信息dump
上图我们可以看出pss total占用了135M超出了100M
简单优化
针对上面情况我们很容易想到 在子进程application阶段 移除非必要功能。通过裁剪能裁剪的代码和功能我们得到了以下优化效果。
提交小米测试后很快被打回了,测试反馈使用内存超过了51M ,看了下测试脚本是1秒取一次内存,结果取最大值,小米这边测试51M是关了主进程测试的,而我们自己测试是开着主进程。 这是因为Pss是平摊计算后的使用内存(有些内存会和其他进程共享,例如mmap进来的),我们开多个进程后共享内存就会被平均到其他进程。 关闭主进程后测试内存占用能到56M
继续分析优化
上面我们发现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加载是主要原因。
Code 问题分析
Code问题主要分析.dex mmap 34680/56526 = 61%
了解android 内存指标获取原理后,我们手动解析一些smaps文件 看下.dex 分布。
从之前的测试包数据看 内存占用主要在base.vdex上,这部分是dex加载也和class加载相关
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)
优化思路
-
通过applicaion 修改,除去widgetProvider进程不必要的类加载 从Hprof文件分析。还是有加载一些不必要的类。
-
对dex顺序做调整 ,类名混淆。
由于dex文件在生成时按字母顺序排列。由于4KB页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据。例如在代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面。但由于A1的数据只有1KB,那在加载的4KB页面中,还会有A2A3A4类,总共占用了4KB内存。
假设我们的代码里在用到A1类后,还会用到B1、C1、D1类,那么如果能在dex文件中将A1、B1、C1、D1类放在一起,虚拟机就只需要加载一个4KB页面,优化的思路就是调整dex文件中数据的顺序,将能够用到的数据紧密排列在一起,Proguard工具能够对类名进行修改,根据程序运行的逻辑将那些会互相调用的类改为同一个packag名,这样就可以使他们的数据排列在一起。
优化实践
代码修改
- 通过对比hprof文件和smali文件发现 BaseApplicatoin中存在aspectj, Fastjson等代码,子进程中移除。
- gson代码转为jsonobject实现
结论基本没什么降低
包名重混淆测试
- 排查到四个组件下keep规则 影响到类名混淆。
- 加入包名类名反混淆后包体减少1.6M
内存测试结论基本没什么降低
深入优化
这一步我们主要就是去要研究base.vdex 里面的内存占用是如何计算的。
vdex格式介绍
这边hook打印下打开.vdex文件的堆栈。然后从这附近去查看相关源码OpenAtAddress
ART 类加载日志hook
base.vdex是通过mmap的方式加载的,那就是在实际访问的时候也就是类加载的时候才会触发内存加载,现阶段我们想知道的是加载了哪些类之后导致内存占用上去了 ,其实art中本身有类加载的一些日志
不过这class_linker开关默认是关的, 我们通过xdl找到这个全局属性后修改class_linker为true
Class load 日志。 拿到class load日志后很容易可以看出某个类什么时候从哪个dex加载的。(额外提一下,这个日志在我们排查framework的问题时也特别有用,我们可以根据问题类拿到jar包或apk进行反编译排查问题)
所以我们的优化思路就是减少类加载的dex数量。 对load类进行移除测试内存 ,对一些其他dex类进行移除后可以发现,如下图内存占用确实降了下来。
结论:测试包code内存占用明显降低
testflight打包后 内存未明显降低
初步发现线程监控 和神策有代码注入 导致dex加载个数又变多了。
测试包发现一段时间后 未加载类 code占用也提升了。
建议内核释放vdex rss
跟踪源码发现 系统可能会有使用madvise预载操作。
尝试通过madvise_dontneed直接discard filed-backed VMA数据
结论:vdex峰值降低10M 一段时间后.dex mmap 内存pss占用显著降低
最终方案
我们在小米机型上使用madvise_dontneed方案释放base.vdex内存。
- 代码仅在小米组件进程生效,不影响主进程。
- 通过art 类加载日志可以知道组件进程加载的类很少,所以使用时再通过mmap load到内存对组件进程性能影响也很小。
- 小米侧内存测试Pass
Android内存指标的获取原理
常用的查看内存信息方式
- 查看android studio 的profile面板
- 利用多啦A梦等客户端工具
- 利用 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
内存获取方式的原理
这里就不逐个去分析各种获取方式的源码了,先给出结论再结合adb dumpsys方案分析
跟源码后每种指标获取方式都指向了一个文件proc/pid/smaps
读取进程smaps的节点, /proc/pid/smaps 我们app中这个文件有9w+行
/proc/PID/smaps 文件是基于 /proc/PID/maps 的扩展,他展示了一个进程的内存消耗,比同一目录下的maps文件更为详细。
(图片来源网络)
根据属性的不同,进程的虚拟地址空间被划分成若干个vma,每一个vma通过vm_next和vm_prev组成双向链表,链表头位于进程的task->mm->mmap.当通过proc接口读取进程的smaps文件时,内核会首先找到该进程的vma链表头,遍历链表中的每一个vma, 通过walk_page_vma统计这块vma的使用情况,最后显示出来。
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 进程独自占用的物理内存(不包含共享库占用的内存
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 文件节点获取。
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 的和
系统占用的内存,例如一些共享的字体,图像资源等。