接上一篇Android CPU监控,接着记录下Android内存的监控,包括系统内存和独立进程内存获取方式解析,以及系统内存和独立进程内存信息在C++层获取的实现。
一、Android系统内存解析
1. dumpsys meminfo方式解析
通常获取系统内存是通过dumpsys meminfo的方式:
adb shell dumpsys meminfo
示例输出:
Applications Memory Usage (in Kilobytes):
Uptime: 118719544 Realtime: 448708145
Total PSS by process:
246,397K: com.autonavi.amapauto (pid 28033 / activities)
…
Total PSS by OOM adjustment:
270,031K: Native
150,621K: surfaceflinger (pid 448)
…
136,799K: System
136,799K: system (pid 1304)
…
296,312K: Persistent
187,565K: com.android.systemui (pid 1579)
…
324,776K: Foreground
246,397K: com.autonavi.amapauto (pid 28033 / activities)
…
62,099K: Visible
15,833K: com.huawei.imonitor (pid 1915)
…
160,413K: Perceptible
56,856K: com.huawei.android.launcher (pid 2676 / activities)
…
6,613K: A Services
6,613K: com.huawei.android.hwouc (pid 26007)
…
93,701K: B Services
51,664K: com.android.settings (pid 1822 / activities)
…
257,433K: Cached
34,026K: com.huawei.hidisk (pid 1085 / activities)
…
Total PSS by category:
…
Total RAM: 2,847,064K (status normal)
Free RAM: 1,457,677K ( 257,433K cached pss + 1,111,432K cached kernel + 88,812K free)
Used RAM: 1,691,708K (1,350,744K used pss + 340,964K kernel)
Lost RAM: -261,582K
ZRAM: 56,328K physical used for 179,836K in swap (1,572,860K total swap)
Tuning: 384 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)
这边主要参考的是最后几个字段信息,Total Ram总内存,Free Ram可用内存,Used RAM已使用内存。
Lost RAM = Total Ram - Free Ram - Used RAM
Lost RAM是属于误差无法统计的部分,是个负数。也就是说通过dumpsys meminfo获取到的内存信息,计算出的内存占用其实是存在误差的,并不准确。
1)dumpsys meminfo存在的问题
- 执行耗时:在华为手机上测试,一次dumpsys meminfo的执行耗时在2s左右,如果用在性能监控上,实时性无法保证,车载设备性能更差,耗时更久。
- 影响system_server进程CPU:测试持续调用命令获取内存信息,在手机上system_server的CPU占用会在8%-11%左右,车镜性能相对手机较差,CPU占用会到18%左右,车机性能更差,CPU会在18% – 33%之间波动。
2)为什么会耗时并影响到system_server进程的CPU?
直接看下dumpsys meminfo源码实现逻辑,命令执行后执行逻辑在dumpsys.cpp里面调用,这边拿Android 6.0的源码来解析,从7.0开始加了很多逻辑。
/frameworks/native/cmds/dumpsys/dumpsys.cpp int main(int argc, char* const argv[])int main(int argc, char* const argv[]){ signal(SIGPIPE, SIG_IGN); sp<IServiceManager> sm = defaultServiceManager(); fflush(stdout); if (sm == NULL) { ALOGE("Unable to get default service manager!"); aerr << "dumpsys: Unable to get default service manager!" << endl; return 20; } Vector<String16> services; Vector<String16> args; bool showListOnly = false; if ((argc == 2) && (strcmp(argv[1], "-l") == 0)) { showListOnly = true; } if ((argc == 1) || showListOnly) { // 命令不带参数获取系统所有服务信息 services = sm->listServices(); services.sort(sort_func); args.add(String16("-a")); } else { // 命令带参数获取指定服务信息 services.add(String16(argv[1])); for (int i=2; i<argc; i++) { args.add(String16(argv[i])); } } ... for (size_t i=0; i<N; i++) { sp<IBinder> service = sm->checkService(services[i]); if (service != NULL) { if (N > 1) { aout << "------------------------------------------------------------" "-------------------" << endl; aout << "DUMP OF SERVICE " << services[i] << ":" << endl; } // 执行服务的dump方法,输出dump内容 int err = service->dump(STDOUT_FILENO, args); if (err != 0) { aerr << "Error dumping service info: (" << strerror(err) << ") " << services[i] << endl; } } else { aerr << "Can't find service: " << services[i] << endl; } } return 0; }
接着看下实际执行dumpsys的地方,在ActivityManagerService.java类里面。
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
实际执行dump的逻辑在dumpApplicationMemoryUsage方法里面,以下代码已简化,保留几处关键信息
final void dumpApplicationMemoryUsage(FileDescriptor fd, PrintWriter pw, String prefix, String[] args, boolean brief, PrintWriter categoryPw) { ... if (!isCheckinRequest && procs.size() > 1 && !packages) { // If we are showing aggregations, also look for native processes to // include so that our aggregations are more accurate. updateCpuStatsNow(); mi = null; synchronized (mProcessCpuTracker) { final int N = mProcessCpuTracker.countStats(); for (int i=0; i<N; i++) { // 遍历所有进程统计数据 ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i); if (st.vsize > 0 && procMemsMap.indexOfKey(st.pid) < 0) { if (mi == null) { mi = new Debug.MemoryInfo(); } if (!brief && !oomOnly) { Debug.getMemoryInfo(st.pid, mi); } else { mi.nativePss = (int)Debug.getPss(st.pid, tmpLong, null); mi.nativePrivateDirty = (int)tmpLong[0]; } final long myTotalPss = mi.getTotalPss(); totalPss += myTotalPss; nativeProcTotalPss += myTotalPss; MemItem pssItem = new MemItem(st.name + " (pid " + st.pid + ")", st.name, myTotalPss, st.pid, false); procMems.add(pssItem); nativePss += mi.nativePss; dalvikPss += mi.dalvikPss; for (int j=0; j<dalvikSubitemPss.length; j++) { dalvikSubitemPss[j] += mi.getOtherPss(Debug.MemoryInfo.NUM_OTHER_STATS + j); } otherPss += mi.otherPss; for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) { long mem = mi.getOtherPss(j); miscPss[j] += mem; otherPss -= mem; } oomPss[0] += myTotalPss; if (oomProcs[0] == null) { oomProcs[0] = new ArrayList<MemItem>(); } oomProcs[0].add(pssItem); } } } ... if (!brief) { if (!isCompact) { pw.print("Total RAM: "); pw.print(memInfo.getTotalSizeKb()); pw.print(" kB (status "); switch (mLastMemoryLevel) { case ProcessStats.ADJ_MEM_FACTOR_NORMAL: pw.println("normal)"); break; case ProcessStats.ADJ_MEM_FACTOR_MODERATE: pw.println("moderate)"); break; case ProcessStats.ADJ_MEM_FACTOR_LOW: pw.println("low)"); break; case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: pw.println("critical)"); break; default: pw.print(mLastMemoryLevel); pw.println(")"); break; } pw.print(" Free RAM: "); pw.print(cachedPss + memInfo.getCachedSizeKb() + memInfo.getFreeSizeKb()); pw.print(" kB ("); pw.print(cachedPss); pw.print(" cached pss + "); pw.print(memInfo.getCachedSizeKb()); pw.print(" cached kernel + "); pw.print(memInfo.getFreeSizeKb()); pw.println(" free)"); } else { pw.print("ram,"); pw.print(memInfo.getTotalSizeKb()); pw.print(","); pw.print(cachedPss + memInfo.getCachedSizeKb() + memInfo.getFreeSizeKb()); pw.print(","); pw.println(totalPss - cachedPss); } } if (!isCompact) { pw.print(" Used RAM: "); pw.print(totalPss - cachedPss + memInfo.getKernelUsedSizeKb()); pw.print(" kB ("); pw.print(totalPss - cachedPss); pw.print(" used pss + "); pw.print(memInfo.getKernelUsedSizeKb()); pw.print(" kernel)\n"); pw.print(" Lost RAM: "); pw.print(memInfo.getTotalSizeKb() - totalPss - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() - memInfo.getKernelUsedSizeKb()); pw.println(" kB"); } ... } }
从源码可以看出,耗时是因为dumpApplicationMemoryUsage里面有很多for循环遍历逻辑,dump逻辑执行的时候要逐个进程遍历统计PSS值,而ActivityManagerService属于系统服务,这就是为什么会影响到system_server进程的CPU。
2. cat /proc/meminfo方式解析
adb shell cat /proc/meminfo
示例结果输出如下,字段的定义可以查看Proc 官方文档:
MemTotal: 2847064 kB MemFree: 93768 kB MemAvailable: 1159236 kB Buffers: 4188 kB Cached: 1072436 kB SwapCached: 57720 kB Active: 1171184 kB ...
这边需要关注的只有前几个字段,MemTotal、MemFree、Buffers、Cached。MemFree表示当前空闲内存,Buffer和Cache是已被操作系统分配用于缓存块数据和文件数据的缓存。
从操作系统层面看Buffer和Cached是已被占用的。
MemUsed = MemTotal - MemFree
从用户层面来看,Buffers和Cached属于保留空间。
MemUsed = MemTotal - MemFree - Buffers - Cached
也就是一般在应用程序的内存监控中,用以下方式来计算系统内存的使用率:
MemUsedRate = (MemTotal - MemFree - Buffers - Cached)/ MemTotal
通过读取/proc/meminfo的方式来监控系统内存更能实时反馈出系统内存的使用情况,只涉及到信息的读取,不耗时,且不影响system_server进程。
二、Android系统内存信息获取C++实现
output是Socket输出对象,具体逻辑可以查看前面Android 性能监控之CPU监控中的Socket服务端C++实现。
// 读取系统总内存信息 read_procs_all("/proc/meminfo", output); /** * 用于读取系统内存信息 */ static int read_procs_all(char* fileName, FILE* output){ FILE *statFile; char buf[MAX_LINE], result[BUF_SIZE]; memset(buf, 0, MAX_LINE); memset(result, 0, BUF_SIZE); int needBreak = -1; statFile = fopen(fileName, "r"); if (!statFile) { printf("Could not open %s .\n", fileName); return 1; } while(fgets(buf, MAX_LINE, statFile) != NULL){ /** * 系统内存从"SwapCached:"开始的地方截取,去掉后面的无用字段 */ if(needBreak != 0){ needBreak = memcmp(buf, "SwapCached:", 11); if(needBreak == 0){ break; } } strcat(result, buf); } fprintf(output, "%s", result); fclose(statFile); return 0; }
三、Android独立进程内存获取解析
adb shell dumpsys meminfo <package>
目前除了以上adb命令方式没有更好的方法来获取独立进程的内存信息。
还有另外一种方式来读取独立进程的内存信息,不过前提是设备具备Root权限,另外这种方式很容易导致IO问题,最后读取到的信息都是乱码。实现方式就是通过读取/proc/[pid]/maps文件中的内存映射地址信息,去匹配读取/proc/<pid>/mem文件中的内存信息。具体实现可以参考下下面两篇文章:
dumpsys meminfo <package>独立进程内存输出示例:
Applications Memory Usage (in Kilobytes): Uptime: 128428913 Realtime: 458417514 ** MEMINFO in pid 28033 [com.android.test] ** Pss Private Private SwapPss Heap Heap Heap Total Dirty Clean Dirty Size Alloc Free ------ ------ ------ ------ ------ ------ ------ Native Heap 77946 74156 3772 0 135424 93392 42031 Dalvik Heap 9893 3964 5860 0 15861 9517 6344 Dalvik Other 1725 1604 116 88 Stack 1372 1268 104 376 Ashmem 23 0 0 0 Other dev 8 0 8 0 .so mmap 27183 432 25228 661 .apk mmap 351 0 56 0 .ttf mmap 1325 0 1236 0 .dex mmap 1381 0 792 4 .oat mmap 3913 0 1908 0 .art mmap 2511 672 812 163 Other mmap 9 4 0 1 GL mtrack 100784 100784 0 0 Unknown 706 492 208 508 TOTAL 230931 183376 40100 1801 151285 102909 48375 App Summary ... Objects ... SQL ...
这边主要看TOTAL总内存值,如果应用存在内存泄漏,总内存就会出现持续增长的状态。
测试通过dumpsys meminfo <package>读取独立进程内存信息对system_server的CPU占用几乎没影响,间隔1S持续调用获取信息,system_server的CPU占用几乎维持在1%左右,偶尔一两次会出现2%或3%的情况。
如果要实现自动化监控,可以监控内存增长曲线的斜率,如果只是场景切换,斜率会先增长,之后再逐步降低。如果是内存泄漏,斜率会维持在一个数值,或者持续增大。可以定义一个计数器来统计斜率增长曲线(注意不是内存增长曲线)的增长和降低,斜率增加则计数+1,斜率降低计数-1,设定一个阈值比如计数>60的时候触发内存泄漏Hprof文件的抓取。
四、Android独立进程内存获取C++实现
这边注意一个点,在C++层,可以用popen来调用shell命令行命令并获取命令行的输出内容。
popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。可以通过这个管道执行标准输入输出操作。这个管道必须由pclose()函数关闭, 而不是fclose()函数(若使用fclose则会产生僵尸进程)。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。
output是Socket输出对象,具体逻辑可以查看前面Android 性能监控之CPU监控中的Socket服务端C++实现。
//monitorProcessPkg 为监控的进程包名参数 char processMem[128]; sprintf(processMem, "dumpsys meminfo %s", monitorProcessPkg); executeCMD(processMem, output); static int executeCMD(const char *cmd, FILE* output) { char buf[MAX_LINE], result[BUF_SIZE], cmdBuf[MAX_LINE]; memset(buf, 0, MAX_LINE); memset(cmdBuf, 0, MAX_LINE); memset(result, 0, BUF_SIZE); FILE *ptr; strcpy(cmdBuf, cmd); int needBreak = -1; if((ptr=popen(cmdBuf, "r"))!=NULL) { while(fgets(buf, MAX_LINE, ptr)!=NULL) { /** * 独立进程内存输出的内容过多会导致Socket分批传送,代码上无法对分批次传送的时序问题做容错,会导致无法一次性解析正确的结果。 * 此处选择在"SQL" 或 "DATABASES" 开始处做截断 */ if(memcmp(buf, " SQL", 4) == 0 || memcmp(buf, " DATABASES", 10) == 0){ break; } strcat(result, buf); if(strlen(result) > BUF_SIZE){ break; } } fprintf(output, "%s", result); pclose(ptr); ptr = NULL; return 1; } else { printf("Performance monitor popen %s error\n", cmdBuf); return -1; } return 0; }
五、参考资料
Interpreting /proc/meminfo and free output for Red Hat Enterprise Linux 5, 6 and 7
How do I read from /proc/$pid/mem under Linux?
Linux 101: How to hack your process’ memory
扩展阅读:
转载请注明出处:陈文管的博客 – Android 性能监控之内存监控
扫码或搜索:文呓
微信公众号 扫一扫关注