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 的和
体系占用的内存,例如一些同享的字体,图画资源等。