В первых двух разделах соответственно было представлено , как контролировать FD и количество потоков , а с точки зрения исходного кода koom подробно описано, как отслеживать утечки памяти в стеках Java и нативных потоков.
И представил, как контролировать виртуальную память и память кучи java , и начал с исходного кода matrix и koom соответственно, и описал два основных решения для мониторинга утечек памяти java.
В этом разделе будет представлен мониторинг встроенной памяти с трех точек зрения:
-
так Большой мониторинг приложений памяти.
-
Мониторинг приложений с большой картинкой.
-
Собственный мониторинг утечек памяти.
Родная память
Собственная память обычно относится к бизнес-библиотеке, поэтому память, динамически запрашиваемая через 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,这是最常见的创建方式
- 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, имя символа изменилось, поэтому мы не можем напрямую перехватить createBitmap, так как же нам получить новое имя функции? Вы можете напрямую передать следующую команду (не забудьте сначала подключить устройство):
> adb pull system/lib/libandroid_runtime.so
> arm-linux-androideabi-nm -D libandroid_runtime.so | grep bitmap
复制代码
- Разные версии системы, имя функции тоже может быть разным, для совместимости, то есть разные версии перехватывают разные функции:
По поводу схемы хуков можно попробовать использовать фреймворк 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.
Ну, это общие методы мониторинга собственной памяти.
Эта глава окончена.