Android修炼系列(35),内存监控技术方案(下)

3,191 阅读7分钟

前两节分别介绍了 如何进行 FD 监控、线程数量监控,并从 koom 源码角度,详细介绍了如何监听 java, native 线程栈内存泄漏的问题。

和介绍了如何进行 虚拟内存和 java 堆内存的监控,并分别从 matrix 和 koom 源码出发,说了主流的两种 java 内存泄漏监听方案。

本节会从三个角度介绍 Native 的内存监控:

  1. so 大内存申请监控。

  2. 大图的申请监控。

  3. 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,这是最常见的创建方式

image.png

Java 层的创建 Bitmap 的所有 API 进入到 Native 层后,全都会走如下这四个步骤。

  • 资源转换 - 这一步将 Java 层传来的不同类型的资源转换成解码器可识别的数据类型
  • 内存分配 - 分配内存时会考虑是否复用 Bitmap、是否缩放 Bitmap 等因素
  • 图片解码 - 实际的解码工作由第三方库完成,解码结果填在上一步分配的内存中。注,Bitmap.createBitmap() 和 Bitmap.copy() 创建的 Bitmap 不需要进行图片解码
  • 创建对象 - 这一步将包含解码数据的内存块包装成 Java 层的 android.graphics.Bitmap 对象,方便 App 使用

摘自 Bitmap 之从出生到死亡

我们知道在 Android8.0 及更高版本,图片的内存申请是在 Native 层的。

通过源码能发现,所有的图片创建最终都会走到 BitmapFactory.cpp 的 doDecode() 函数中,通过 HeapAllocate 等内存分配器来分配内存并存储图片的像素数据。完成内存分配后,会创建一个 SkBitmp 对象,它持有的 SkPixelRef 存储了内存地址。最后调用JNI层的 createBitmap 函数,在这里创建了Java 层的 bitmap 对象。Bitmap.cpp 见下:

image.png

所以监控方案:只需要 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 不同函数:

image.png

关于 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 内存监控的常用手段就这些了。

本章完。