如何应对Android 面试官 -> 内存如何进行优化(下)?玩转 Koom本章我们继续进行上一章未完的进行讲解; 线上
前言
本章我们继续进行上一章未完的进行讲解;
线上内存监测方案思路优化
- Activity 在执行销毁的时候,我们如何得知?
- 如何判断一个 Activity 无法被 GC 回收?
上一章我们讲了线上内存如何监测的思路,主要是借助 ActivityLifeCycleCallbacks + WeakHashMap 但是在和方案也是有一些弊端的,例如:需要我们在 onDestory 的回调中手动的触发 GC,但是 GC 又比较耗费资源,就有会导致我们的项目卡顿;
所以,我们在触发 GC 的时候,应该给予一个阈值,当达到这个阈值之后才触发 GC;
常见内存泄漏原因
原因:长生命周期对象持有短生命周期对象,导致短生命周期对象在不再需要时无法被 GC 回收。
- 静态变量或单例导致的内存泄漏(静态变量或者单例不要持有 activity 或 view 等对象的引用,如果必须持有可以改为 WeakReference);
- 内部类导致的内存泄漏(内部类会持有外部类的引用,使用静态内部类实现,静态内部类相当于普通类,不会持有对外部类的引用);
- 匿名内部类同样会持有对外部类对象的引用(经典的 Handler 问题,在销毁 Activity 的时候调用 handler.removeCallbacksAndMessages(); 考虑是否可以使用静态内部类实现,静态内部类相当于普通类,不会持有外部类的引用;当业务确实需要调用 Activity 的方法时,使用 WeakReference);
- 动画问题(Activity 销毁的时候,未调用动画的 cancel 方法);
- InputStream、OutputStream、Cursor、File文件等没有 Close;
Native 层内存泄漏监测原理
- hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据「大小、堆栈、地址等」
- 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」
- 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据「不可达内存块地址、大小、分配堆栈等」
Koom 源码浅析
我们来分析下 Koom 是如何监测 native、java 层泄漏的;
Native 层泄漏监测
Koom 的 native 层泄漏监测模块是 koom-native-leak
Java 层其实没什么逻辑,一个是配置相关信息,一个是对 native 层的调用,主要集中在 LeakMonitor 和 LeakMonitorConfig;
LeakMonitor 主要是 init 初始化 和 startLoop 开启监测、以及 checkLeaks 监测泄漏并上报;
真正的监测逻辑在 jni 层,我们来详细看下 jni 层,是如何实现 native 层的监测的;
我们先进入 CMakeList.txt 中看下:
cmake_minimum_required(VERSION 3.6)
set(TARGET koom-native)
set(THIRD_PARTY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../koom-common/third-party)
set(KWAI_ANDROID_BASE_DIR ${CMAKE_SOURCE_DIR}/../../../../koom-common/kwai-android-base)
add_compile_options(-Oz)
project(${TARGET})
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include/
${KWAI_ANDROID_BASE_DIR}/src/main/cpp/include/
${KWAI_ANDROID_BASE_DIR}/src/main/cpp/liblog/include/
${THIRD_PARTY_DIR}/xhook/src/main/cpp/xhook/src/
)
link_directories(
${KWAI_ANDROID_BASE_DIR}/src/main/libs/${ANDROID_ABI}/
${THIRD_PARTY_DIR}/xhook/src/main/libs/${ANDROID_ABI}/
)
add_library( # Sets the name of the library.
${TARGET}
SHARED
src/memory_map.cpp
src/jni_leak_monitor.cpp
src/leak_monitor.cpp
src/memory_analyzer.cpp
src/utils/hook_helper.cpp
src/utils/stack_trace.cpp
)
find_library( # Sets the name of the path variable.
log-lib
log)
target_link_libraries( # Specifies the target library.
${TARGET}
xhook_lib
kwai-android-base
${log-lib})
可以看到,Koom 中借助了爱奇艺开源的 xhook 框架,用来 hook native 层的逻辑,xhook 感兴趣的可以 github 上搜索一下,这里主要讲内存监测,不做过多概述;
如果想知道 so 中有哪些函数,可以借助 ida 来查看一个 so 中有哪些函数;
我们接着看下 jni_leak_monitor.cpp
static const char *kLeakMonitorFullyName =
"com/kwai/koom/nativeoom/leakmonitor/LeakMonitor";
static const char *kLeakRecordFullyName =
"com/kwai/koom/nativeoom/leakmonitor/LeakRecord";
static const char *kFrameInfoFullyName =
"com/kwai/koom/nativeoom/leakmonitor/FrameInfo";
java 层定义的三个类;
static void Clean(JNIEnv *env) {
if (g_leak_record.global_ref) {
// 删除全局引用
env->DeleteGlobalRef(g_leak_record.global_ref);
// 清除内存空间
memset(&g_leak_record, 0, sizeof(g_leak_record));
}
if (g_frame_info.global_ref) {
env->DeleteGlobalRef(g_frame_info.global_ref);
memset(&g_frame_info, 0, sizeof(g_frame_info));
}
}
Clean 函数主要是用来清除内存空间的;
而如何做监听,是通过 UninstallMonitor 和 InstallMonitor 来实现的
static void UninstallMonitor(JNIEnv *env, jclass) {
LeakMonitor::GetInstance().Uninstall();
g_memory_map.~MemoryMap();
Clean(env);
}
InstallMonitor
static bool InstallMonitor(JNIEnv *env, jclass clz, jobjectArray selected_array,
jobjectArray ignore_array,
jboolean enable_local_symbolic) {
jclass leak_record;
FIND_CLASS(leak_record, kLeakRecordFullyName);
g_leak_record.global_ref =
reinterpret_cast<jclass>(env->NewGlobalRef(leak_record));
if (!CheckedClean(env, g_leak_record.global_ref)) {
return false;
}
GET_METHOD_ID(g_leak_record.construct_method, leak_record, "<init>",
"(JILjava/lang/String;[Lcom/kwai/koom/nativeoom/leakmonitor/"
"FrameInfo;)V");
jclass frame_info;
FIND_CLASS(frame_info, kFrameInfoFullyName);
g_frame_info.global_ref =
reinterpret_cast<jclass>(env->NewGlobalRef(frame_info));
if (!CheckedClean(env, g_frame_info.global_ref)) {
return false;
}
GET_METHOD_ID(g_frame_info.construct_method, frame_info, "<init>",
"(JLjava/lang/String;)V");
g_enable_local_symbolic = enable_local_symbolic;
auto array_to_vector =
[](JNIEnv *env, jobjectArray jobject_array) -> std::vector<std::string> {
std::vector<std::string> ret;
int length = env->GetArrayLength(jobject_array);
if (length <= 0) {
return ret;
}
for (jsize i = 0; i < length; i++) {
auto str = reinterpret_cast<jstring>(
env->GetObjectArrayElement(jobject_array, i));
const char *data = env->GetStringUTFChars(str, nullptr);
ret.emplace_back(data);
env->ReleaseStringUTFChars(str, data);
}
return std::move(ret);
};
std::vector<std::string> selected_so = array_to_vector(env, selected_array);
std::vector<std::string> ignore_so = array_to_vector(env, ignore_array);
// 最终调用到的是 CheckedClean 函数
return CheckedClean(
env, LeakMonitor::GetInstance().Install(&selected_so, &ignore_so));
}
我们接着往下看 GetLeakAllocs 函数
static void GetLeakAllocs(JNIEnv *env, jclass, jobject leak_record_map) {
ScopedLocalRef<jclass> map_class(env, env->GetObjectClass(leak_record_map));
jmethodID put_method;
GET_METHOD_ID(put_method, map_class.get(), "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
std::vector<std::shared_ptr<AllocRecord>> leak_allocs =
LeakMonitor::GetInstance().GetLeakAllocs();
for (auto &leak_alloc : leak_allocs) {
if (leak_alloc->num_backtraces <= kNumDropFrame) {
continue;
}
leak_alloc->num_backtraces -= kNumDropFrame;
std::vector<std::pair<jlong, std::string>> frames;
for (int i = 0; i < leak_alloc->num_backtraces; i++) {
uintptr_t offset;
// 计算引用链
auto *map_entry = g_memory_map.CalculateRelPc(
leak_alloc->backtrace[i + kNumDropFrame], &offset);
if (!map_entry) {
continue;
}
if (map_entry->NeedIgnore()) {
leak_alloc->num_backtraces = i;
break;
}
std::string symbol_info =
g_enable_local_symbolic
? g_memory_map.FormatSymbol(
map_entry, leak_alloc->backtrace[i + kNumDropFrame])
: basename(map_entry->name.c_str());
// 通过取阈值来判断是否发生了泄漏
frames.emplace_back(static_cast<jlong>(offset), symbol_info);
}
if (!leak_alloc->num_backtraces || frames.empty()) {
continue;
}
char address[sizeof(uintptr_t) * 2 + 1];
snprintf(address, sizeof(uintptr_t) * 2 + 1, "%lx",
CONFUSE(leak_alloc->address));
ScopedLocalRef<jstring> memory_address(env, env->NewStringUTF(address));
ScopedLocalRef<jobjectArray> frames_ref(env, BuildFrames(env, frames));
ScopedLocalRef<jobject> leak_record_ref(
env, BuildLeakRecord(env, leak_alloc->index, leak_alloc->size,
leak_alloc->thread_name, frames_ref.get()));
ScopedLocalRef<jobject> no_use(
env,
env->CallObjectMethod(leak_record_map, put_method, memory_address.get(),
leak_record_ref.get()));
}
}
数组 kLeakMonitorMethods 中比较重要的是 GetLeakAllocs
static const JNINativeMethod kLeakMonitorMethods[] = {
{"nativeInstallMonitor", "([Ljava/lang/String;[Ljava/lang/String;Z)Z",
reinterpret_cast<void *>(InstallMonitor)},
{"nativeUninstallMonitor", "()V",
reinterpret_cast<void *>(UninstallMonitor)},
{"nativeSetMonitorThreshold", "(I)V",
reinterpret_cast<void *>(SetMonitorThreshold)},
{"nativeGetAllocIndex", "()J", reinterpret_cast<void *>(GetAllocIndex)},
{"nativeGetLeakAllocs", "(Ljava/util/Map;)V",
reinterpret_cast<void *>(GetLeakAllocs)}};
GetLeakAllocs
static jlong GetAllocIndex(JNIEnv *, jclass) {
return LeakMonitor::GetInstance().CurrentAllocIndex();
}
可以看到,比较核心的逻辑在 LeakMonitor 中,我们进入这个 cpp 文件看下,这里面进行了一系列的 hook 操作来进行泄漏的监测;
#define HOOK(ret_type, function, ...) \
static ALWAYS_INLINE ret_type WRAP(function)(__VA_ARGS__)
hook free 函数
HOOK(void, free, void *ptr) {
free(ptr);
if (ptr) {
// 一旦 hook 了就会通过 UnregisterAlloc 来监测有没有进行释放
LeakMonitor::GetInstance().UnregisterAlloc(
reinterpret_cast<uintptr_t>(ptr));
}
}
hook 了 free 释放内存函数之后就会通过 UnregisterAlloc 来监测有没有进行释放;
hook malloc 函数
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;
}
一旦执行了 malloc 分配内存函数,立马用 OnMonitor 进行监测了,监测的就是 malloc 之后有没有 free
hook realloc 函数
HOOK(void *, realloc, void *ptr, size_t size) {
auto result = realloc(ptr, size);
if (ptr != nullptr) {
LeakMonitor::GetInstance().UnregisterAlloc(
reinterpret_cast<uintptr_t>(ptr));
}
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
size);
return result;
}
一旦执行了 realloc 重新分配内存函数,立马用 OnMonitor 进行监测了;
hook calloc 函数
HOOK(void *, calloc, size_t item_count, size_t item_size) {
auto result = calloc(item_count, item_size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
item_count * item_size);
return result;
}
hook memalign、posix_memalign 函数;
HOOK(void *, memalign, size_t alignment, size_t byte_count) {
auto result = memalign(alignment, byte_count);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
byte_count);
CLEAR_MEMORY(result, byte_count);
return result;
}
HOOK(int, posix_memalign, void **memptr, size_t alignment, size_t size) {
auto result = posix_memalign(memptr, alignment, size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(*memptr),
size);
CLEAR_MEMORY(*memptr, size);
return result;
}
那么这些函数是怎么被 hook 的呢?就是借助 xHook 框架来实现的,我们进入 hook_helper.cpp 中看下:
#define LOG_TAG "hook_helper"
#include "utils/hook_helper.h"
#include <dlopencb.h>
#include <log/log.h>
#include <xhook.h>
std::vector<const std::string> HookHelper::register_pattern_;
std::vector<const std::string> HookHelper::ignore_pattern_;
std::vector<std::pair<const std::string, void *const>> HookHelper::methods_;
bool HookHelper::HookMethods(
std::vector<const std::string> ®ister_pattern,
std::vector<const std::string> &ignore_pattern,
std::vector<std::pair<const std::string, void *const>> &methods) {
if (register_pattern.empty() || methods.empty()) {
ALOGE("Hook nothing");
return false;
}
register_pattern_ = std::move(register_pattern);
ignore_pattern_ = std::move(ignore_pattern);
methods_ = std::move(methods);
DlopenCb::GetInstance().AddCallback(Callback);
return HookImpl();
}
void HookHelper::UnHookMethods() {
DlopenCb::GetInstance().RemoveCallback(Callback);
register_pattern_.clear();
ignore_pattern_.clear();
methods_.clear();
}
void HookHelper::Callback(std::set<std::string> &, int, std::string &) {
HookImpl();
}
bool HookHelper::HookImpl() {
pthread_mutex_lock(&DlopenCb::hook_mutex);
xhook_clear();
for (auto &pattern : register_pattern_) {
for (auto &method : methods_) {
// 执行真正的 hook 逻辑
if (xhook_register(pattern.c_str(), method.first.c_str(), method.second,
nullptr) != EXIT_SUCCESS) {
ALOGE("xhook_register pattern %s method %s fail", pattern.c_str(),
method.first.c_str());
pthread_mutex_unlock(&DlopenCb::hook_mutex);
return false;
}
}
}
for (auto &pattern : ignore_pattern_) {
for (auto &method : methods_) {
if (xhook_ignore(pattern.c_str(), method.first.c_str()) != EXIT_SUCCESS) {
ALOGE("xhook_ignore pattern %s method %s fail", pattern.c_str(),
method.first.c_str());
pthread_mutex_unlock(&DlopenCb::hook_mutex);
return false;
}
}
}
int ret = xhook_refresh(0);
pthread_mutex_unlock(&DlopenCb::hook_mutex);
return ret == 0;
}
通过 xhook_register 来执行 hook 逻辑;
内存泄露如何被检测就是通过 hook 的方式来实现的;至于内存分析这块,koom 借助了 libmemunreachable 库来进行分析的;
所以 Koom native 层泄漏监测的原理可以总结为如下:
- hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据『大小、堆栈、地址等』;
- 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息『地址、大小』;
- 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据『不可达内存块地址、大小、分配堆栈等』;
Java 层内存泄漏监测
我们知道 上一章讲 LeakCanary 的内存泄漏监测是通过利用弱引用的特性,为 Activity 创建弱引用,当 Activity 对象变成弱可达时,弱引用会被加入到引用队列中,通过在 Activity.onDestory() 后连续两次的 GC 操作,并检查引用队列,可以判定 Activity 是否发生了泄漏。但是频繁的 GC 会造成用户可感知的卡顿,那么怎么解决这种性能损耗呢?Koom 采用了 无性能损耗的阈值监控策略 来实现的,我们具体来看一下;
为什么 LeakCanary 不能作为线上内存监控策略呢?
我们来看下系统中 dump Hprof 的时候发生了什么,我们通过 Debug.dumpHprofData() 方法最终跟到的是 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.
if (javaFilename == nullptr && javaFd < 0) {
ScopedObjectAccess soa(env);
ThrowNullPointerException("fileName == null && fd == null");
return;
}
std::string filename;
if (javaFilename != nullptr) {
ScopedUtfChars chars(env, javaFilename);
if (env->ExceptionCheck()) {
return;
}
filename = chars.c_str();
} else {
filename = "[fd]";
}
int fd = javaFd;
hprof::DumpHeap(filename.c_str(), fd, false);
}
最终执行的是 DumpHeap 我们进入这个方法看下:
// art/runtime/hprof/hprof.cc
void DumpHeap(const char* filename, int fd, bool direct_to_ddms) {
CHECK(filename != nullptr);
Thread* self = Thread::Current();
gc::ScopedGCCriticalSection gcs(self,
gc::kGcCauseHprof,
gc::kCollectorTypeHprof);
ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);
Hprof hprof(filename, fd, direct_to_ddms);
hprof.Dump();
}
从源码中可以看到,在执行 Dump 之前,会构造一个 ScopedSuspendAll 对象,用来暂停所有的线程,然后再析构方法中恢复;
// /art/runtime/thread_list.cc
ScopedSuspendAll::ScopedSuspendAll(const char* cause, bool long_suspend) {
Runtime::Current()->GetThreadList()->SuspendAll(cause, long_suspend);
}
ScopedSuspendAll::~ScopedSuspendAll() {
Runtime::Current()->GetThreadList()->ResumeAll();
}
Koom 是如何规避这个问题的呢?看源码可以得知,Koom 做了如下操作:
- fork 子进程,在子进程中执行 Debug.dumpHprofData();
- fork 子进程,采用的是 Copy-On-Write 的技术,只有在进行写入操作的时候,才会为子进程拷贝分配独立的内存空间,默认情况下,子进程可以和父进程共享同个内存空间,所以,当我们要执行 dumpHprofData() 的时候,可以先 fork 一个子进程,它拥有父进程的内存副本,然后在子进程去执行 Debug.dumpHprofData() 方法,而父进程则可以继续正常运行;
public synchronized boolean dump(String path) {
MonitorLog.i(TAG, "dump " + path);
if (!sdkVersionMatch()) {
throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");
}
init();
if (!mLoadSuccess) {
MonitorLog.e(TAG, "dump failed caused by so not loaded!");
return false;
}
boolean dumpRes = false;
try {
MonitorLog.i(TAG, "before suspend and fork.");
int pid = suspendAndFork();
if (pid == 0) {
// Child process
Debug.dumpHprofData(path);
exitProcess();
} else if (pid > 0) {
// Parent process
dumpRes = resumeAndWait(pid);
MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}
} catch (IOException e) {
MonitorLog.e(TAG, "dump failed caused by " + e);
e.printStackTrace();
}
return dumpRes;
}
可以看到调用 native 方法 suspendAndFork fork 一个子进程,如果子进程 id = 0 则执行 dump 操作;我们来看下 suspendAndFork 是如何 fork 的;
pid_t HprofDump::SuspendAndFork() {
KCHECKI(init_done_)
if (android_api_ < __ANDROID_API_R__) {
suspend_vm_fnc_();
} else if (android_api_ <= __ANDROID_API_U__) {
void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
sgc_constructor_fnc_((void *)sgc_instance_.get(), self, kGcCauseHprof,
kCollectorTypeHprof);
ssa_constructor_fnc_((void *)ssa_instance_.get(), LOG_TAG, true);
// avoid deadlock with child process
exclusive_unlock_fnc_(*mutator_lock_ptr_, self);
sgc_destructor_fnc_((void *)sgc_instance_.get());
}
pid_t pid = fork();
if (pid == 0) {
// Set timeout for child process
alarm(60);
prctl(PR_SET_NAME, "forked-dump-process");
}
return pid;
}
可以看到 pid_t pid = fork(); 使用 fork 函数进行子进程的 fork 操作;
整体流程如下
- 开始 dump
- 调用虚拟机 Suspend API
- Fork 创建子进程
- 子进程开始 dump
- 通知父进程 dump 完成
- 父进程开始分析
好了,Koom 的 native 层和 java 层的内存泄漏监控就讲完了;
Hprof
这块不做过多的解释了,讲一下裁剪思路;
如何裁剪
裁剪可以参考下 Martix,它里面的裁剪功能的目标是将 Bitmap 和 String 之外的所有对象的基础数据类型的值移除,因为 Hprof 文件的分析功能只需要用到字符串数组和 Bitmap 的 buffer 数组。另一方面,如果存在不同的 Bitmap 对象其 buffer 数组值相同的情况,则可以将它们指向同一个 buffer,以进一步减小文件尺寸。裁剪后的 Hprof 文件通常比源文件小 1/10 以上,代码结果和 ASM 很像,主要有 HprofVisitor、HprofReader、HprofWriter 组成,分别对应 ASM 的 ClassVisitor、ClassReader、ClassWriter;
下一张预告
App 启动流程优化
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~
转载自:https://juejin.cn/post/7410786086581190668