Android Cultivation Series (35), техническое решение для мониторинга памяти (часть 2)

В первых двух разделах соответственно было представлено , как контролировать FD и количество потоков , а с точки зрения исходного кода koom подробно описано, как отслеживать утечки памяти в стеках Java и нативных потоков.

И представил, как контролировать виртуальную память и память кучи java , и начал с исходного кода matrix и koom соответственно, и описал два основных решения для мониторинга утечек памяти java.

В этом разделе будет представлен мониторинг встроенной памяти с трех точек зрения:

  1. так Большой мониторинг приложений памяти.

  2. Мониторинг приложений с большой картинкой.

  3. Собственный мониторинг утечек памяти.

Родная память

Собственная память обычно относится к бизнес-библиотеке, поэтому память, динамически запрашиваемая через c/c++, обычно используется путем вызова функции malloc для применения к памяти и вызова функции free для освобождения памяти. Эти приложения памяти должны быть освобождены разумно, иначе произойдет утечка памяти.

Мы можем проанализировать текущее состояние памяти через файл /proc/pid/smaps ( подробности см. выше ), где Pss представляет собой сумму памяти, эксклюзивной для собственного слоя в процессе, и амортизированной физической памяти, совместно используемой с другими процессами.

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
...
复制代码

Когда приложение обращается к памяти, оно обращается к виртуальной памяти. Когда приложение обращается к этой памяти и выполняет операцию записи, если физическая память не была выделена, произойдет ошибка страницы и инициируется выделение физической памяти. При фактическом выделении физической памяти оно основано на «страницах», и каждая страница обычно занимает 4 КБ памяти. После того, как выделение завершено, отношение сопоставления между виртуальным адресом и физическим адресом каждой страницы записывается в PageTable.

Мониторинг собственной памяти, основные решения теперь начинаются с двух аспектов: один — это мониторинг приложений с большой памятью, а другой — мониторинг приложений с большими изображениями.Далее мы проанализируем этих двух пользователей с большой памятью:

Мониторинг большой памяти

В большинстве случаев бизнес подает и освобождает память через функции 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,这是最常见的创建方式

изображение.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, имя символа изменилось, поэтому мы не можем напрямую перехватить createBitmap, так как же нам получить новое имя функции? Вы можете напрямую передать следующую команду (не забудьте сначала подключить устройство):
> adb pull system/lib/libandroid_runtime.so
> arm-linux-androideabi-nm -D libandroid_runtime.so | grep bitmap
复制代码
  • Разные версии системы, имя функции тоже может быть разным, для совместимости, то есть разные версии перехватывают разные функции:

image.png

По поводу схемы хуков можно попробовать использовать фреймворк inline-hook android-inline-hook of bytes , который здесь представлен не будет, и я поделюсь им позже, если будет возможность.

Утечка встроенной памяти

Схема утечки нативной памяти аналогична упомянутой выше схеме утечки прослушивающего потока Вот краткий анализ:

Также возьмите 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));
}
复制代码

Вот установка:

// 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);
}
复制代码

Наконец, получите информацию live_alloc_records_#Dump при вызове jni_leak_monitor.cpp#GetLeakAllocs.

Ну, это общие методы мониторинга собственной памяти.

Эта глава окончена.

рекомендация

отjuejin.im/post/7084934068416512008