前两节分别介绍了 如何进行 FD 监控、线程数量监控,并从 koom 源码角度,详细介绍了如何监听 java, native 线程栈内存泄漏的问题。
和介绍了如何进行 虚拟内存和 java 堆内存的监控,并分别从 matrix 和 koom 源码出发,说了主流的两种 java 内存泄漏监听方案。
本节会从三个角度介绍 Native 的内存监控:
-
so 大内存申请监控。
-
大图的申请监控。
-
Native 内存泄漏监控。
Native 内存
Native 内存一般是指的业务 so 库,通过 c/c++ 动态申请的内存,常用的方式就是调用 malloc 函数申请内存,调用 free 释放内存。这些内存的申请都需要合理的释放,否则就会导致内存泄漏。
我们可通过 /proc/pid/smaps 文件来分析当前的内存状态(具体见上文),其中 Pss 表示本进程内 native 层独占的内存和与其他进程共享内存的均摊的物理内存总和。
7d4a434000-7d4a435000 rw-p 00010000 fe:00 4380 /system/vendor/lib64/libqdMetaData.so
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
...
复制代码
当应用申请内存时,申请的是虚拟内存。当应用访问这块内存并进行写操作时,如果物理内存还未分配则会发生缺页中断并触发分配物理内存。在实际分配物理内存时,是以“页”为单位,每页通常是 4KB的内存空间。完成分配后,在 PageTable 记录了每一页的虚拟地址和物理地址映射关系。
Native 内存的监控,现在主流方案都是从两方面入手,一是 so 内大内存申请的监控,二是大图的申请监控,接下来我们就从这两个内存大户上分析下:
大内存监控
大多数情况下,业务都是通过 malloc 和 free 函数来申请和释放内存的(为了方便管理,业务侧应尽量不要直接通过 mmap 来申请内存)。
那么监控方案也就呼之欲出了: 直接 hook 掉系统库 libc.so 的 malloc 和 free 等我们操作内存的函数,在我们的 hook_malloc 和 hook_free 函数内记录每次分配的内存大小和地址,通过系统堆栈回溯机制追踪到业务函数调用堆栈地址,并读取当前的 smap 文件,进行内存分析。
下面以 matrix 为例,讲讲它是如何使用 iqiyi xhook hook 掉 malloc 函数的:
这是 MemoryHook,其提供了内存 hook 配置的接口:
MemoryHook#addHookSo(String) // 要hook的so库
MemoryHook#addIgnoreSo(String) // 要过滤的so库
MemoryHook#enableMmapHook(boolean) // 是否要hook mmap
MemoryHook#enableStacktrace(boolean) // 是否要收集堆栈
MemoryHook#getNativeLibraryName() // 当前本地库名称
MemoryHook#hook() // 开始hook
MemoryHook#dump(String, String) // 开始jump信息
复制代码
开始 hook:
// com.tencent.matrix.hook.memory.MemoryHook.java
// java层的作用就是: 正确加载so,并将相应配置通过native方法给c++库,最后 install hook
// 具体代码就不一条条贴了,大概步骤:
// 1. MemoryHook#hook() -> HookManager#commitHooks() -> HookManager#commitHooksLocked()
// 2.-> MemoryHook#onConfigure() -> MemoryHook#onHook(boolean)
// 3.-> MemoryHook#installHooksNative
public void hook() throws HookManager.HookFailedException {
HookManager.INSTANCE // 内部持有AbsHook对象,能直接调用MemoryHook接口
.clearHooks()
.addHook(this)
.commitHooks();
}
复制代码
这是 Jni 的入口:
// MemoryHookJNI.cpp
nstallHooksNative(JNIEnv* env, jobject thiz,
jobjectArray hook_so_patterns,
jobjectArray ignore_so_patterns,
jboolean enable_debug) {
memory_hook_init(); // 开启一个线程,并执行 BufferManagement::process_routine, 定时检查 busy_ratio
LOGI(TAG, "memory_hook_init");
xhook_block_refresh();
{
jsize size = env->GetArrayLength(hook_so_patterns);
for (int i = 0; i < size; ++i) { // 拿到每个要 hook 的 so name
auto jregex = (jstring) env->GetObjectArrayElement(hook_so_patterns, i);
const char* regex = env->GetStringUTFChars(jregex, nullptr);
hook(regex); // 开始hook
env->ReleaseStringUTFChars(jregex, regex);
}
}
... // ignore_so 同理,并通 过xhook_grouped_ignore 忽略部分 hook 信息
xhook_unblock_refresh();
}
复制代码
这是 hook 接口:
// MemoryHookJNI.cpp
static void hook(const char *regex) {
for (auto f : HOOK_MALL_FUNCTIONS) {
// 真正执行 hook 操作,将原 f.name 方法 hook 到我们新方法 f.handler_ptr 上
int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr);
LOGD(TAG, "hook fn, regex: %s, sym: %s, ret: %d", regex, f.name, ret);
}
LOGD(TAG, "mmap enabled ? %d", enable_mmap_hook);
if (enable_mmap_hook) { // 是否需要 hook mmap
for (auto f: HOOK_MMAP_FUNCTIONS) {
xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr);
}
}
}
复制代码
这是 HOOK_MALL_FUNCTIONS:
// HookCommon.h
typedef struct {
const char *name; // 要 hook 的函数 name
void *handler_ptr; // 要被替换成的 PLT 入口点地址值
void **origin_ptr; // 调用函数的 PLT 入口点的地址值
} HookFunction;
// MemoryHookJNI.cpp
const HookFunction HOOK_MALL_FUNCTIONS[] = {
{"malloc", (void *) h_malloc, NULL},
{"calloc", (void *) h_calloc, NULL},
{"realloc", (void *) h_realloc, NULL},
{"free", (void *) h_free, NULL},
{"memalign", (void *) HANDLER_FUNC_NAME(memalign), NULL},
{"posix_memalign", (void *) HANDLER_FUNC_NAME(posix_memalign), NULL}
...
}
复制代码
hook 成功后,当我们调用 malloc 时,会执行 h_malloc:
// MemoryHookFunctions.cpp
// CALL_ORIGIN_FUNC_RET 是被定义的多行宏函数
// DEFINE_HOOK_FUN 相当于 void* h_malloc(size_t __byte_count)
DEFINE_HOOK_FUN(void *, malloc, size_t __byte_count) {
CALL_ORIGIN_FUNC_RET(void*, p, malloc, __byte_count);
LOGI(TAG, "+ malloc %p", p);
DO_HOOK_ACQUIRE(p, __byte_count);
return p;
}
// HookCommon.h
#define HANDLER_FUNC_NAME(fn_name) h_##fn_name
#define DEFINE_HOOK_FUN(ret, sym, params...) \
ORIGINAL_FUNC_PTR(sym); \
ret HANDLER_FUNC_NAME(sym)(params)
// MemoryHookFunctions.cpp
#define DO_HOOK_ACQUIRE(p, size) \
GET_CALLER_ADDR(caller); \
on_alloc_memory(caller, p, size);
复制代码
随后会执行 on_alloc_memory:
// MemoryHook.cpp
// 不仅 malloc,realloc、mmap 都会统一到 on_acquire_memory 方法
// 同理 free,munmap 都会统一调用on_release_memory
// 当然我们也可以分开监听
void on_alloc_memory(void *caller, void *ptr, size_t byte_count) {
on_acquire_memory(caller, ptr, byte_count, message_type_allocation);
}
复制代码
这是 on_acquire_memory:
// MemoryHook.cpp
static inline void on_acquire_memory(
void *caller,
void *ptr,
size_t byte_count,
message_type type) {
...
// 1. 是否需要堆栈回溯
memory_backtrace_t backtrace{0};
if (LIKELY(byte_count > 0 && is_stacktrace_enabled && should_do_unwind(byte_count))) {
size_t frame_size = 0;
do_unwind(backtrace.frames, MEMHOOK_BACKTRACE_MAX_FRAMES,
frame_size);
backtrace.frame_size = frame_size;
}
container->lock();
...
// 2. 记录函数地址和申请内存大小和地址
message->ptr = reinterpret_cast<uintptr_t>(ptr);
message->size = byte_count;
message->caller = reinterpret_cast<uintptr_t>(caller);
if (backtrace.frame_size) {
message->backtrace = backtrace;
}
container->unlock();
}
...
复制代码
到这里 hook 的大致逻辑就完了。
大图监控
创建 Bitmap 的方式很多,
- 可以通过 SDK 提供的 API 来创建 Bitmap
- 加载某些布局或资源时会创建 Bitmap
- Glide 等第三方图片库会创建 Bitmap
先说通过 API 创建 Bitmap。SDK 中创建 Bitmap 的 API 很多,分成三大类:
-
创建 Bitmap - Bitmap.createBitmap() 方法在内存中从无到有地创建 Bitmap
-
拷贝 Bitmap - Bitmap.copy() 从已有的 Bitmap 拷贝出一个新的 Bitmap
-
解码 - 从文件或字节数组等资源解码得到 Bitmap,这是最常见的创建方式
- BitmapFactory.decodeResource()
- ImageDecoder.decodeBitmap(Android9+)
Java 层的创建 Bitmap 的所有 API 进入到 Native 层后,全都会走如下这四个步骤。
- 资源转换 - 这一步将 Java 层传来的不同类型的资源转换成解码器可识别的数据类型
- 内存分配 - 分配内存时会考虑是否复用 Bitmap、是否缩放 Bitmap 等因素
- 图片解码 - 实际的解码工作由第三方库完成,解码结果填在上一步分配的内存中。注,Bitmap.createBitmap() 和 Bitmap.copy() 创建的 Bitmap 不需要进行图片解码
- 创建对象 - 这一步将包含解码数据的内存块包装成 Java 层的 android.graphics.Bitmap 对象,方便 App 使用
我们知道在 Android8.0 及更高版本,图片的内存申请是在 Native 层的。
通过源码能发现,所有的图片创建最终都会走到 BitmapFactory.cpp 的 doDecode() 函数中,通过 HeapAllocate 等内存分配器来分配内存并存储图片的像素数据。完成内存分配后,会创建一个 SkBitmp 对象,它持有的 SkPixelRef 存储了内存地址。最后调用JNI层的 createBitmap 函数,在这里创建了Java 层的 bitmap 对象。Bitmap.cpp 见下:
所以监控方案:只需要 hook 掉这个 createBitmap 函数,就能够拿到每次图片创建时的 bitmap 的 Java 对象。通过该对象,就可以获得每次创建的图片的尺寸大小、内存占用大小,堆栈等信息。
- 因为 JNI createBitmap 函数被编译进了 libandroid_runtime.so,所以符号名已经改变了,所以我们并不能直接 hook createBitmap,那我们要怎么拿到新的函数名呢?可以直接通过下面命令(记得先连接设备):
> adb pull system/lib/libandroid_runtime.so
> arm-linux-androideabi-nm -D libandroid_runtime.so | grep bitmap
复制代码
- 系统版本不同,这个函数名也可能不同,要做好兼容,即不同版本 hook 不同函数:
关于 hook 方案,可以尝试着使用字节的这个 inline-hook 框架 android-inline-hook,这里就不介绍了,后续如果有机会我再分享。
Native 内存泄漏
关于 Native 内存泄漏的方案,跟前面说的监听线程泄漏方案差不多,这里仅简单分析下:
还以 KOOM 为例,这是 JNI 的入口:
// jni_leak_monitor.cpp
static bool InstallMonitor(JNIEnv *env, jclass clz, jobjectArray selected_array,
jobjectArray ignore_array,
jboolean enable_local_symbolic) {
...
// hook的so和要过滤的so
std::vector<std::string> selected_so = array_to_vector(env, selected_array);
std::vector<std::string> ignore_so = array_to_vector(env, ignore_array);
return CheckedClean(
env, LeakMonitor::GetInstance().Install(&selected_so, &ignore_so));
}
复制代码
这是 Install:
// leak_monitor.cpp
bool LeakMonitor::Install(std::vector<std::string> *selected_list,
std::vector<std::string> *ignore_list) {
...
// 定义要hook的函数,和重定向后的函数
std::vector<std::pair<const std::string, void *const>> hook_entries = {
std::make_pair("malloc", reinterpret_cast<void *>(WRAP(malloc))),
std::make_pair("realloc", reinterpret_cast<void *>(WRAP(realloc))),
std::make_pair("calloc", reinterpret_cast<void *>(WRAP(calloc))),
std::make_pair("memalign", reinterpret_cast<void *>(WRAP(memalign))),
std::make_pair("posix_memalign",
reinterpret_cast<void *>(WRAP(posix_memalign))),
std::make_pair("free", reinterpret_cast<void *>(WRAP(free)))};
// 开始hook
if (HookHelper::HookMethods(register_pattern, ignore_pattern, hook_entries)) {
has_install_monitor_ = true;
return true;
}
...
}
// 宏定义,如 WRAP(malloc) -> mallocMonior
#define WRAP(x) x##Monitor
复制代码
当调用 malloc 后,执行:
// leak_monitor.cpp
HOOK(void *, malloc, size_t size) {
auto result = malloc(size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
size);
CLEAR_MEMORY(result, size);
return result;
}
// 多行宏定义
#define HOOK(ret_type, function, ...) \
static ALWAYS_INLINE ret_type WRAP(function)(__VA_ARGS__)
复制代码
之后调用 OnMonitor:
// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::OnMonitor(uintptr_t address, size_t size) {
...
RegisterAlloc(address, size);
}
复制代码
再后调用 RegisterAlloc:
// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::RegisterAlloc(uintptr_t address, size_t size) {
...
// 记录内存大小和地址指针,并加入 live_alloc_records_
thread_local ThreadInfo thread_info;
auto alloc_record = std::make_shared<AllocRecord>();
alloc_record->address = CONFUSE(address);
alloc_record->size = size;
alloc_record->index = alloc_index_++;
memcpy(alloc_record->thread_name, thread_info.name, kMaxThreadNameLen);
unwind_backtrace(alloc_record->backtrace, &(alloc_record->num_backtraces));
live_alloc_records_.Put(CONFUSE(address), std::move(alloc_record));
}
复制代码
同理,再执行完 free 后,从 live_alloc_records_ 移除:
// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::UnregisterAlloc(uintptr_t address) {
live_alloc_records_.Erase(address);
}
复制代码
最后调用 jni_leak_monitor.cpp#GetLeakAllocs 时获取 live_alloc_records_#Dump 信息即可。
好了,关于 Native 内存监控的常用手段就这些了。
本章完。