对于Java而言,语言本身有自动垃圾回收机制,所以导致在内存泄漏检测上需要确定对象应该被回收的特征,以便进行检查,如LeakCanary,Matrix中监听生命周期,KOOM中检查finished或者destroyed的Activity,究其根本,主要还是要对对象应该被回收的状态进行定义。不同于Java,在Native层主要使用C或者C++来进行编码,语言本身并没有垃圾回收机制,对于对象的回收依赖于开发者手动释放空间,这也就意味着在native层进行泄漏检测相对而言更加困难,那么Native层内存泄漏就没办法监控了吗?
当然有的,在Android N(7.0)以后系统新增了libmemunreachable模块,该模块是一个零开销的本地内存泄漏检测器,其使用不精确的标记-清楚垃圾回收器遍历所有Native内存,并将不可访问的内存块报告为泄漏内存区域。基于libmemunreachable我们可以设计一套机制用于监控Native层内存泄漏问题,主要原理如下:
- hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据「大小、堆栈、地址等」
- 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」
- 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据「不可达内存块地址、大小、分配堆栈等」
Native层内存泄漏对象 = 不可达的内存块信息
接下来我们来看下koom-native-leak模块的实现,与koom-java-leak模块的实现类似,该模块也是通过XXXMonitor对象来启动监控的,我们可以通过LeakMonitor.INSTANCE.start();启动监控,通过MonitorManager.addMonitorConfig(config)为其添加通用配置(配置参数可以查看github上的说明)。
LeakMonitor.INSTANCE.start()
通过start代码可以看到,最终真正执行的函数是nativeInstallMonitor和nativeGetLeakAllocs。
nativeInstallMonitor
如上代码所示,install阶段的主要功能是hook malloc/free 等内存分配器方法。
nativeGetLeakAllocs
memory_analyzer_->CollectUnreachableMem
namespace kwai {
namespace leak_monitor {
static const char *kLibMemUnreachableName = "libmemunreachable.so";
// GetUnreachableMemory函数名在libmemunreachable.so中在不同Android版本上的标记
// Just need the symbol in arm64-v8a so
// API level > Android O
static const char *kGetUnreachableMemoryStringSymbolAboveO =
"_ZN7android26GetUnreachableMemoryStringEbm";
// API level <= Android O
static const char *kGetUnreachableMemoryStringSymbolBelowO =
"_Z26GetUnreachableMemoryStringbm";
MemoryAnalyzer::MemoryAnalyzer()
: get_unreachable_fn_(nullptr), handle_(nullptr) {
auto handle = kwai::linker::DlFcn::dlopen(kLibMemUnreachableName, RTLD_NOW);
if (!handle) {
ALOGE("dlopen %s error: %s", kLibMemUnreachableName, dlerror());
return;
}
// 通过kwai-linker去调用libmemunreachable.so的GetUnreachableMemory函数(分Android版本)
if (android_get_device_api_level() > __ANDROID_API_O__) {
get_unreachable_fn_ =
reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
handle, kGetUnreachableMemoryStringSymbolAboveO));
} else {
get_unreachable_fn_ =
reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
handle, kGetUnreachableMemoryStringSymbolBelowO));
}
}
MemoryAnalyzer::~MemoryAnalyzer() {
if (handle_) {
kwai::linker::DlFcn::dlclose(handle_);
}
}
bool MemoryAnalyzer::IsValid() { return get_unreachable_fn_ != nullptr; }
std::vector<std::pair<uintptr_t, size_t>>
MemoryAnalyzer::CollectUnreachableMem() {
std::vector<std::pair<uintptr_t, size_t>> unreachable_mem;
if (!IsValid()) {
ALOGE("MemoryAnalyzer NOT valid");
return std::move(unreachable_mem);
}
// libmemunreachable NOT work in release apk because it using ptrace
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) {
ALOGE("Set process dumpable Fail");
return std::move(unreachable_mem);
}
// Note: time consuming
std::string unreachable_memory = get_unreachable_fn_(false, 1024);
// Unset "dumpable" for security
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
// 正则表达式匹配输出内容
std::regex filter_regex("[0-9]+ bytes unreachable at [A-Za-z0-9]+");
std::sregex_iterator unreachable_begin(
unreachable_memory.begin(), unreachable_memory.end(), filter_regex);
std::sregex_iterator unreachable_end;
for (; unreachable_begin != unreachable_end; ++unreachable_begin) {
const auto& line = unreachable_begin->str();
auto address =
std::stoul(line.substr(line.find_last_of(' ') + 1,
line.length() - line.find_last_of(' ') - 1),
0, 16);
auto size = std::stoul(line.substr(0, line.find_first_of(' ')));
unreachable_mem.emplace_back(address, size);
}
return std::move(unreachable_mem);
}
} // namespace leak_monitor
} // namespace kwai
通过libmemunreachable.so的GetUnreachableMemory函数获取native内存情况。
Is_leak
auto is_leak = [&](decltype(unreachable_allocs)::value_type &unreachable,
std::shared_ptr<AllocRecord> &live) -> bool {
auto live_start = CONFUSE(live->address);
auto live_end = live_start + live->size;
auto unreachable_start = unreachable.first;
auto unreachable_end = unreachable_start + unreachable.second;
return live_start == unreachable_start ||
live_start >= unreachable_start && live_end <= unreachable_end;
};