android 存储空间监控,浅谈 Android 内存监控(中)

前言

在上篇 浅谈 Android 内存监控(上) 中,我们聊了 LeakCanary,微信的 Matirx 和美团的 Probe,它们各自有不同的应用场景,例如,在开发测试环境,我们会偏向用 LeakCanary,因为它能提供最完善的内存泄露机制和最详细的日志,非常方便定位问题,但它的不足之处就是,对性能影响比较大,所以如果要应用于线上生产环境,我们通常会考虑 Matrix 和 Probe,Matrix 除了提供 Activity/Fragment 对象泄露检测,它还支持重复 Bitamp 检测,同时它还会裁剪 hprof 文件,大大提高上传的成功率。而 Probe 的亮点在于,它不是对源 hprof 文件进行裁剪,而是 hook 了生成 hprof 的 native 方法,直接生成裁剪后的 hprof,这个优化能降低分析 hprof 文件的内存占用,提供分析的成功率。可惜的是,Probe 是个闭源的项目,没办法拿来主义。

关于 dumpHprofData

不管是 Matrix 也好,Probe 也好,它们都有一个痛点,没有解决调用 Debug.dumpHprofData() 带来的卡顿阻塞问题,dumpHprofData 是用来调用生成 hprof 文件,在生成过程中,整个页面会卡住,用户是没办法进行操作,至于为什么会带来这样的影响,我们可以通过源码去一探究竟。

Debug.dumpHprofData() 最终会通过 JNI 调用到 native 方法:

// art/runtime/native/dalvik_system_VMDebug.cc

static void VMDebug_dumpHprofData(JNIEnv* env, jclass, jstring javaFilename, jint javaFd){

// Only one of these may be null.

// 忽略一些判断代码

hprof::DumpHeap(filename.c_str(), fd, false);

}

// art/runtime/hprof/hprof.cc

void DumpHeap(const char* filename, int fd, bool direct_to_ddms){

// 忽略一些判断代码

ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);

Hprof hprof(filename, fd, direct_to_ddms);

// 开始执行 Dump 操作

hprof.Dump();

}

复制代码

从源码中,我们可以看到在进行 Dump 操作之前,会构造一个 ScopedSuspendAll 对象,用来暂停所有的线程,然后再析构方法中恢复:

// 暂停所有线程

ScopedSuspendAll::ScopedSuspendAll(const char* cause, bool long_suspend) {

Runtime::Current()->GetThreadList()->SuspendAll(cause, long_suspend);

}

// 恢复所有线程

ScopedSuspendAll::~ScopedSuspendAll() {

Runtime::Current()->GetThreadList()->ResumeAll();

}

复制代码

这个暂停操作,对用户体验是种极大的伤害,可以通过取巧的方式去规避,例如,新开个进程来显示 loading 页面,APP 退到后台去执行 dump 等等,但并没有真正解决这个问题。

KOOM

最近快手开源了一个内存监控库叫 KOOM,这个库有不少亮点:

实现了 Probe:Android线上OOM问题定位组件 中提出 hook 生成 hprof 的 native 方法

解决了 dumpHprofData 方法阻塞问题

使用了 LeakCanary2 中使用的 hprof 分析库 shark

裁剪 hprof

KOOM 通过 xhook 实现 PLT hook,通过 hook 两个虚拟机方法 open() 和 writ() 来实现裁剪 hprof:

JNIEXPORT void JNICALL

Java_com_kwai_koom_javaoom_dump_StripHprofHeapDumper_initStripDump(JNIEnv *env, jobject jObject){

hprofFd = -1;

hprofName = nullptr;

isDumpHookSucc = false;

xhook_enable_debug(0);

/**

*

* android 7.x,write方法在libc.so中

* android 8-9,write方法在libart.so中

* android 10,write方法在libartbase.so中

* libbase.so是一个保险操作,防止前面2个so里面都hook不到(:

*

* android 7-10版本,open方法都在libart.so中

* libbase.so与libartbase.so,为保险操作

*/

xhook_register("libart.so", "open", (void *)hook_open, nullptr);

xhook_register("libbase.so", "open", (void *)hook_open, nullptr);

xhook_register("libartbase.so", "open", (void *)hook_open, nullptr);

xhook_register("libc.so", "write", (void *)hook_write, nullptr);

xhook_register("libart.so", "write", (void *)hook_write, nullptr);

xhook_register("libbase.so", "write", (void *)hook_write, nullptr);

xhook_register("libartbase.so", "write", (void *)hook_write, nullptr);

xhook_refresh(0);

xhook_clear();

}

复制代码

可以看到在不同的 Android 版本中,要 hook 的位置都有所区别。

在 hook 方法后,我们实现对指定的 hprof 文件进行裁剪,所以,会先通过 hprofName() 这个 JNI 方法来标记:

JNIEXPORT void JNICALL Java_com_kwai_koom_javaoom_dump_StripHprofHeapDumper_hprofName(

JNIEnv *env, jobject jObject, jstring name){

hprofName = (char *)env->GetStringUTFChars(name, (jboolean *)false);

}

复制代码

接着,在 hook_open() 方法获取文件的 FD:

int hook_open(const char *pathname, int flags, ...){

va_list ap;

va_start(ap, flags);

int fd = open(pathname, flags, ap);

va_end(ap);

if (hprofName == nullptr) {

return fd;

}

if (pathname != nullptr && strstr(pathname, hprofName)) {

// 获取 FD

hprofFd = fd;

isDumpHookSucc = true;

}

return fd;

}

复制代码

最后,在 hook_write() 方法中过滤掉要裁剪的数据,这里的代码细节我们就不去讨论了,有兴趣可以去看看源码。

解决 Dump 阻塞问题

上面我们说到,在执行 dumpHprofData 方法时,会先暂停进程中所有的线程,后面再重新恢复,所以即使将这个操作放到异步线程也是没办法解决的。

KOOM 在解决这个问题上,用了一个非常赞的思路,通过 fork 子进程去执行 dumpHprofData 方法。 fork 进程采用的是 "Copy On Write" 技术,只有在进行写入操作时,才会为子进程拷贝分配独立的内存空间,默认情况下,子进程可以和父进程共享同个内存空间,所以,当我们要执行 dumpHprofData 方法时,可以先 fork 一个子进程,它拥有父进程的内存副本,然后在子进程去执行 dumpHprofData 方法,而父进程则可以正常继续运行,相关源码如下:

try {

int pid = trySuspendVMThenFork();

if (pid == 0) {

Debug.dumpHprofData(path);

KLog.i(TAG, "notifyDumped:" + dumpRes);

//System.exit(0);

exitProcess();

} else {

resumeVM();

dumpRes = waitDumping(pid);

KLog.i(TAG, "hprof pid:" + pid + " dumped: " + path);

}

} catch (Exception e) {

e.printStackTrace();

}

复制代码

trySuspendVMThenFork 是一个 JNI 方法,用于暂停线程,并执行 fork 操作:

JNIEXPORT jint JNICALL Java_com_kwai_koom_javaoom_dump_ForkJvmHeapDumper_trySuspendVMThenFork(

JNIEnv *env, jobject jObject){

if (suspendVM == nullptr) {

initForkVMSymbols();

}

if (suspendVM != nullptr) {

suspendVM();

}

return fork();

}

bool initForkVMSymbols(){

bool res = false;

void *libHandle = kwai::linker::DlFcn::dlopen("libart.so", RTLD_NOW);

if (libHandle == nullptr) {

return res;

}

suspendVM = (void (*)())kwai::linker::DlFcn::dlsym(libHandle, "_ZN3art3Dbg9SuspendVMEv");

if (suspendVM == nullptr) {

__android_log_print(ANDROID_LOG_ERROR, "KOOM", "suspendVM is null!");

}

resumeVM = (void (*)())kwai::linker::DlFcn::dlsym(libHandle, "_ZN3art3Dbg8ResumeVMEv");

if (resumeVM == nullptr) {

__android_log_print(ANDROID_LOG_ERROR, "KOOM", "resumeVM is null!");

}

kwai::linker::DlFcn::dlclose(libHandle);

return suspendVM != nullptr && resumeVM != nullptr;

}

复制代码

在执行之前,会先调用 initForkVMSymbols() 执行初始化,这里是为了获取 suspendVM 和 resumeVM 这两个方法引用,接着在执行 fork 之前,先调用 suspendVM 暂停父进程的所有线程。

当 fork 执行成功后,会返回两次,当 pid == 0 时,这时候是在 fork 创建的子进程中,这时候我们可以执行 dumpHprofData 方法:

if (pid == 0) {

Debug.dumpHprofData(path);

KLog.i(TAG, "notifyDumped:" + dumpRes);

//System.exit(0);

exitProcess();

}

复制代码

执行完 dumpHprofData 方法后,直接退出关闭子进程,节省资源。

当 pid != 0 是,这时候是在父进程中,这时候我们只需要恢复之前暂停的线程即可:

resumeVM();

dumpRes = waitDumping(pid);

KLog.i(TAG, "hprof pid:" + pid + " dumped: " + path);

复制代码

同时我们继续在异步线程中等待子进程 Dump 操作结束。

这里贴张 KOOM 团队给出的流程图:

47d475d9dc48e0fbddf5aa1be60f644e.png

这样,我们可以将主进程调用 dumpHprofData 方法阻塞的时候,优化到 fork 子进程耗时时间。这里我再贴张 KOOM 团队给出的基准测试结果:

1d328449e8c88f4f4199d786aa06f905.png

使用 Shark

LeakCanary2 重写了一个解析 hprof 文件的库,叫做 shark,它是用来代替原来的 haha,根据官方的说法,相比于 haha,shark 内存减少了10倍,速度快了6倍。KOOM 除了使用 shark 来解析,还在这个的基础上做了一些优化,减少了内存的占用,具体可以看源码。

小结

KOOM 的出现,提供了解决线上使用内存监控工具的一大障碍,我们完全可以基于 KOOM,再综合 Matrix 和 Probe 的优点,开发一款属于自己的线上内存监控工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值