精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Android Native內(nèi)存泄漏檢測方案詳解

移動開發(fā) Android
本文將分三個部分闡述如何實現(xiàn) Android Native 內(nèi)存泄漏監(jiān)控,包括代理實現(xiàn)的三種方案及其優(yōu)缺點,以及如何檢測Native內(nèi)存泄露和獲取Android Native堆棧的方法。

作者 | yeconglu

一個完整的 Android Native 內(nèi)存泄漏檢測工具主要包含三部分:代理實現(xiàn)、堆?;厮莺途彺婀芾怼4韺崿F(xiàn)是解決 Android 平臺上接入問題的關(guān)鍵部分,堆?;厮輨t是性能和穩(wěn)定性的核心要素。

本文會從三個方面介紹如何實現(xiàn) Native 內(nèi)存泄漏監(jiān)控:

  • 介紹代理實現(xiàn)的三個方案 Inline Hook、PLT/GOT Hook、LD_PRELOAD 的實現(xiàn)方式和優(yōu)缺點。
  • 介紹檢測 Android Native 內(nèi)存泄露的基本思路和包含緩存邏輯的示例代碼。
  • 介紹獲取 Android Native 堆棧的方法,用于記錄分配內(nèi)存時的調(diào)用棧。

一、代理內(nèi)存管理函數(shù)實現(xiàn)

首先我們來介紹一下代理內(nèi)存管理函數(shù)實現(xiàn)的三個方案 :

  • Inline Hook
  • PLT/GOT Hook
  • LD_PRELOAD

1. Native Hook

(1) 方案對比:Inline Hook和PLT/GOT Hook

目前主要有兩種Native Hook方案:Inline Hook和PLT/GOT Hook。

指令重定位是指在計算機程序的鏈接和裝載過程中,對程序中的相對地址進(jìn)行調(diào)整,使其指向正確的內(nèi)存位置。這是因為程序在編譯時,無法預(yù)知在運行時會被裝載到內(nèi)存的哪個位置,所以編譯后的程序中,往往使用相對地址來表示內(nèi)存位置。然而在實際運行時,程序可能被裝載到內(nèi)存的任何位置,因此需要在裝載過程中,根據(jù)程序?qū)嶋H被裝載到的內(nèi)存地址,對程序中的所有相對地址進(jìn)行調(diào)整,這個過程就叫做重定位。

在進(jìn)行Inline Hook時,如果直接修改目標(biāo)函數(shù)的機器碼,可能會改變原有的跳轉(zhuǎn)指令的相對地址,從而使程序跳轉(zhuǎn)到錯誤的位置,因此需要進(jìn)行指令重定位,確保修改后的指令能正確地跳轉(zhuǎn)到預(yù)期的位置。

(2) 案例:在Android應(yīng)用中Hook malloc 函數(shù)

為了更好地理解Native Hook的應(yīng)用場景,我們來看一個實際的案例:在Android應(yīng)用中Hook malloc 函數(shù),以監(jiān)控文件的打開操作。

① Inline Hook實現(xiàn)

#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <android/log.h>

#define TAG "NativeHook"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

typedef void* (*orig_malloc_func_type)(size_t size);

orig_malloc_func_type orig_malloc;

unsigned char backup[8];  // 用于保存原來的機器碼

void* my_malloc(size_t size) {
    LOGD("內(nèi)存分配: %zu 字節(jié)", size);

    // 創(chuàng)建一個新的函數(shù)指針orig_malloc_with_backup,指向一個新的內(nèi)存區(qū)域
    void *orig_malloc_with_backup = mmap(NULL, sizeof(backup) + 8, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    
    // 將備份的指令A(yù)和B復(fù)制到新的內(nèi)存區(qū)域
    memcpy(orig_malloc_with_backup, backup, sizeof(backup));

    // 在新的內(nèi)存區(qū)域的末尾添加一個跳轉(zhuǎn)指令,使得執(zhí)行流跳轉(zhuǎn)回原始malloc函數(shù)的剩余部分
    unsigned char *jump = (unsigned char *)orig_malloc_with_backup + sizeof(backup);
    jump[0] = 0x01;  // 跳轉(zhuǎn)指令的機器碼
    *(void **)(jump + 1) = (unsigned char *)orig_malloc + sizeof(backup);  // 跳轉(zhuǎn)目標(biāo)的地址

    // 調(diào)用orig_malloc_with_backup函數(shù)指針
    orig_malloc_func_type orig_malloc_with_backup_func_ptr = (orig_malloc_func_type)orig_malloc_with_backup;
    void *result = orig_malloc_with_backup_func_ptr(size);

    // 釋放分配的內(nèi)存區(qū)域
    munmap(orig_malloc_with_backup, sizeof(backup) + 8);

    return result;
}

void *get_function_address(const char *func_name) {
    void *handle = dlopen("libc.so", RTLD_NOW);
    if (!handle) {
        LOGD("錯誤: %s", dlerror());
        return NULL;
    }

    void *func_addr = dlsym(handle, func_name);
    dlclose(handle);

    return func_addr;
}

void inline_hook() {
    void *orig_func_addr = get_function_address("malloc");
    if (orig_func_addr == NULL) {
        LOGD("錯誤: 無法找到 'malloc' 函數(shù)的地址");
        return;
    }

    // 備份原始函數(shù)
    orig_malloc = (orig_malloc_func_type)orig_func_addr;

    // 備份原始機器碼
    memcpy(backup, orig_func_addr, sizeof(backup));

    // 更改頁面保護(hù)
    size_t page_size = sysconf(_SC_PAGESIZE);
    uintptr_t page_start = (uintptr_t)orig_func_addr & (~(page_size - 1));
    mprotect((void *)page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);

    // 構(gòu)造跳轉(zhuǎn)指令
    unsigned char jump[8] = {0};
    jump[0] = 0x01;  // 跳轉(zhuǎn)指令的機器碼
    *(void **)(jump + 1) = my_malloc;  // 我們的鉤子函數(shù)的地址

    // 將跳轉(zhuǎn)指令寫入目標(biāo)函數(shù)的入口點
    memcpy(orig_func_addr, jump, sizeof(jump));
}

void unhook() {
    void *orig_func_addr = get_function_address("malloc");
    if (orig_func_addr == NULL) {
        LOGD("錯誤: 無法找到 'malloc' 函數(shù)的地址");
        return;
    }

    // 更改頁面保護(hù)
    size_t page_size = sysconf(_SC_PAGESIZE);
    uintptr_t page_start = (uintptr_t)orig_func_addr & (~(page_size - 1));
    mprotect((void *)page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);

    // 將備份的機器碼寫入目標(biāo)函數(shù)的入口點
    memcpy(orig_func_addr, backup, sizeof(backup));
}

在my_malloc中,我們需要先執(zhí)行備份的指令,然后將執(zhí)行流跳轉(zhuǎn)回原始malloc函數(shù)的剩余部分:

  • 在my_malloc函數(shù)中,創(chuàng)建一個新的函數(shù)指針orig_malloc_with_backup,它指向一個新的內(nèi)存區(qū)域,該區(qū)域包含備份的指令以及一個跳轉(zhuǎn)指令。
  • 將備份的指令復(fù)制到新的內(nèi)存區(qū)域。
  • 在新的內(nèi)存區(qū)域的末尾添加一個跳轉(zhuǎn)指令,使得執(zhí)行流跳轉(zhuǎn)回原始malloc函數(shù)的剩余部分。
  • 在my_malloc中,調(diào)用orig_malloc_with_backup函數(shù)指針。

這里有三個難點,下面詳細(xì)解釋一下:

● 如何修改內(nèi)存頁的保護(hù)屬性

orig_func_addr & (~(page_size - 1)) 這段代碼的作用是獲取包含 orig_func_addr 地址的內(nèi)存頁的起始地址。這里使用了一個技巧:page_size 總是2的冪,因此 page_size - 1 的二進(jìn)制表示形式是低位全為1,高位全為0,取反后低位全為0,高位全為1。將 orig_func_addr 與 ~(page_size - 1) 進(jìn)行與操作,可以將 orig_func_addr 的低位清零,從而得到內(nèi)存頁的起始地址。

mprotect((void *)page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC); 這行代碼的作用是修改內(nèi)存頁的保護(hù)屬性。mprotect 函數(shù)可以設(shè)置一塊內(nèi)存區(qū)域的保護(hù)屬性,它接受三個參數(shù):需要修改的內(nèi)存區(qū)域的起始地址,內(nèi)存區(qū)域的大小,以及新的保護(hù)屬性。在這里,我們將包含 orig_func_addr 地址的內(nèi)存頁的保護(hù)屬性設(shè)置為可讀、可寫、可執(zhí)行(PROT_READ | PROT_WRITE | PROT_EXEC),以便我們可以修改這個內(nèi)存頁中的代碼。

● 如何恢復(fù)原函數(shù)

想要恢復(fù)原來的函數(shù),我們需要在Hook之前保存原來的機器碼,然后在需要恢復(fù)時,將保存的機器碼寫回函數(shù)的入口點。

代碼中的backup數(shù)組用于保存原始機器碼。在inline_hook函數(shù)中,我們在修改機器碼之前先將原始機器碼復(fù)制到backup數(shù)組。然后,我們提供了一個unhook函數(shù),用于恢復(fù)原始機器碼。在需要恢復(fù)malloc函數(shù)時,可以調(diào)用unhook函數(shù)。

需要注意的是,這個示例假設(shè)函數(shù)的入口點的機器碼長度是8字節(jié)。在實際使用時,你需要根據(jù)實際情況確定機器碼的長度,并相應(yīng)地調(diào)整backup數(shù)組的大小和memcpy函數(shù)的參數(shù)。

●如何實現(xiàn)指令重定位

我們以一個簡單的ARM64匯編代碼為例,演示如何進(jìn)行指令重定位。假設(shè)我們有以下目標(biāo)函數(shù):

TargetFunction:
    mov x29, sp
    sub sp, sp, #0x10
    ; ... 其他指令 ...
    bl SomeFunction
    ; ... 其他指令 ...
    b TargetFunctionEnd

我們要在TargetFunction的開頭插入一個跳轉(zhuǎn)指令,將執(zhí)行流跳轉(zhuǎn)到我們的HookFunction。為了實現(xiàn)這一目標(biāo),我們需要進(jìn)行以下操作:

  • 備份被覆蓋的指令:我們需要備份TargetFunction開頭的指令,因為它們將被我們的跳轉(zhuǎn)指令覆蓋。在這個例子中,我們需要備份mov x29, sp和sub sp, sp, #0x10兩條指令。
  • 插入跳轉(zhuǎn)指令:在TargetFunction的開頭插入一個跳轉(zhuǎn)到HookFunction的跳轉(zhuǎn)指令。在ARM64匯編中,我們可以使用b指令實現(xiàn)這一目標(biāo):
b HookFunction
  • 處理被覆蓋的指令:在HookFunction中,我們需要執(zhí)行被覆蓋的指令。在這個例子中,我們需要在HookFunction中執(zhí)行mov x29, sp和sub sp, sp, #0x10兩條指令。
  • 重定位跳轉(zhuǎn)和數(shù)據(jù)引用:在HookFunction中,我們需要處理目標(biāo)函數(shù)中的跳轉(zhuǎn)和數(shù)據(jù)引用。在這個例子中,我們需要重定位bl SomeFunction和b TargetFunctionEnd兩條跳轉(zhuǎn)指令。根據(jù)目標(biāo)函數(shù)在內(nèi)存中的新地址,我們需要計算新的跳轉(zhuǎn)地址,并修改這兩條指令的操作數(shù)。
  • 返回到目標(biāo)函數(shù):在HookFunction中執(zhí)行完被覆蓋的指令和其他自定義操作后,我們需要返回到目標(biāo)函數(shù)的未被修改部分。在這個例子中,我們需要在HookFunction的末尾添加一個跳轉(zhuǎn)指令,將執(zhí)行流跳轉(zhuǎn)回TargetFunction的sub sp, sp, #0x10指令。

經(jīng)過以上步驟,我們成功地在TargetFunction中插入了一個跳轉(zhuǎn)到HookFunction的跳轉(zhuǎn)指令,并對目標(biāo)函數(shù)中的跳轉(zhuǎn)和數(shù)據(jù)引用進(jìn)行了重定位。這樣,當(dāng)執(zhí)行到TargetFunction時,程序?qū)⑻D(zhuǎn)到HookFunction執(zhí)行,并在執(zhí)行完被覆蓋的指令和其他自定義操作后,返回到目標(biāo)函數(shù)的未被修改部分。

(2)PLT/GOT Hook實現(xiàn)

PLT(Procedure Linkage Table)和GOT(Global Offset Table)是Linux下動態(tài)鏈接庫(shared libraries)中用于解析動態(tài)符號的兩個重要表。

PLT(Procedure Linkage Table):過程鏈接表,用于存儲動態(tài)鏈接庫中函數(shù)的入口地址。當(dāng)程序調(diào)用一個動態(tài)鏈接庫中的函數(shù)時,首先會跳轉(zhuǎn)到PLT中的對應(yīng)條目,然后再通過GOT找到實際的函數(shù)地址并執(zhí)行。

GOT(Global Offset Table):全局偏移表,用于存儲動態(tài)鏈接庫中函數(shù)和變量的實際地址。在程序運行時,動態(tài)鏈接器(dynamic linker)會根據(jù)需要將函數(shù)和變量的實際地址填充到GOT中。PLT中的條目會通過GOT來找到函數(shù)和變量的實際地址。

在PLT/GOT Hook中,我們可以修改GOT中的函數(shù)地址,使得程序在調(diào)用某個函數(shù)時實際上調(diào)用我們自定義的函數(shù)。這樣,我們可以在自定義的函數(shù)中添加額外的邏輯(如檢測內(nèi)存泄漏),然后再調(diào)用原始的函數(shù)。這種方法可以實現(xiàn)對程序的無侵入式修改,而不需要重新編譯程序。

#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <android/log.h>

#define TAG "NativeHook"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

typedef void* (*orig_malloc_func_type)(size_t size);

orig_malloc_func_type orig_malloc;

void* my_malloc(size_t size) {
    LOGD("Memory allocated: %zu bytes", size);
    return orig_malloc(size);
}

void plt_got_hook() {
    void **got_func_addr = (void **)dlsym(RTLD_DEFAULT, "malloc");
    if (got_func_addr == NULL) {
        LOGD("Error: Cannot find the GOT entry of 'malloc' function");
        return;
    }

    // Backup the original function
    orig_malloc = (orig_malloc_func_type)*got_func_addr;

    // Replace the GOT entry with the address of our hook function
    *got_func_addr = my_malloc;
}

上面代碼中的RTLD_DEFAULT是一個特殊的句柄值,表示在當(dāng)前進(jìn)程已加載的所有動態(tài)鏈接庫中查找符號。當(dāng)使用RTLD_DEFAULT作為dlsym()的handle參數(shù)時,dlsym()會在當(dāng)前進(jìn)程已加載的所有動態(tài)鏈接庫中查找指定的符號,而不僅僅是某個特定的動態(tài)鏈接庫。

(3) 再看 Inline Hook 和 Got Hook 的區(qū)別

關(guān)鍵在于,兩種 Native Hook 方式的實現(xiàn)中,dlsym返回的地址含義是不一樣的:

① Inline Hook

void *get_function_address(const char *func_name) {
    void *handle = dlopen("libc.so", RTLD_NOW);
    ...
    void *func_addr = dlsym(handle, func_name);
    dlclose(handle);

    return func_addr;
}

void *orig_func_addr = get_function_address("malloc");
memcpy(orig_func_addr, jump, sizeof(jump));

dlsym 返回的地址是函數(shù)在內(nèi)存中的實際地址,這個地址通常指向函數(shù)的入口點(即函數(shù)的第一條指令)。

 ②Got Hook

void **got_func_addr = (void **)dlsym(RTLD_DEFAULT, "malloc");
*got_func_addr = my_malloc;

dlsym 返回的是 malloc 函數(shù)在 GOT 中的地址,注意void **got_func_addr是雙重指針。

2. 使用LD_PRELOAD

使用LD_PRELOAD的方式,可以在不修改源代碼的情況下重載內(nèi)存管理函數(shù)。雖然這種方式在Android平臺上有很多限制,但是我們也可以了解下相關(guān)的原理。

LD_PRELOAD 是一個環(huán)境變量,用于在程序運行時預(yù)加載動態(tài)鏈接庫。通過設(shè)置 LD_PRELOAD,我們可以在程序運行時強制加載指定的庫,從而在不修改源代碼的情況下改變程序的行為。這種方法通常用于調(diào)試、性能分析和內(nèi)存泄漏檢測等場景。

使用 LD_PRELOAD 檢測內(nèi)存泄漏的原理和方法如下:

(1) 原理

當(dāng)設(shè)置了 LD_PRELOAD 環(huán)境變量時,程序會在加載其他庫之前加載指定的庫。這使得我們可以在自定義庫中重載(override)一些原始庫(如 glibc)中的函數(shù)。在內(nèi)存泄漏檢測的場景中,我們可以重載內(nèi)存分配和釋放函數(shù)(如 malloc、calloc、realloc 和 free),以便在分配和釋放內(nèi)存時記錄相關(guān)信息。

(2) 方法:

  • 創(chuàng)建自定義庫:首先,我們需要創(chuàng)建一個自定義內(nèi)存泄露檢測庫,并在其中重載內(nèi)存分配和釋放函數(shù)。在這些重載的函數(shù)中,我們可以調(diào)用原始的內(nèi)存管理函數(shù),并在分配內(nèi)存時將內(nèi)存塊及其相關(guān)信息(如分配大小、調(diào)用棧等)添加到全局內(nèi)存分配表中,在釋放內(nèi)存時從全局內(nèi)存分配表中刪除相應(yīng)的內(nèi)存塊。
  • 設(shè)置 LD_PRELOAD 環(huán)境變量:在運行程序之前,我們需要設(shè)置 LD_PRELOAD 環(huán)境變量,使其指向自定義庫的路徑。這樣,程序在運行時會優(yōu)先加載自定義庫,從而使用重載的內(nèi)存管理函數(shù)。
  • 運行程序:運行程序時,它將使用重載的內(nèi)存管理函數(shù),從而記錄內(nèi)存分配和釋放的信息。我們可以在程序運行過程中或運行結(jié)束后,檢查全局內(nèi)存分配表中仍然存在的內(nèi)存塊,從而檢測內(nèi)存泄漏。

通過使用 LD_PRELOAD 檢測內(nèi)存泄漏,我們可以在不修改程序源代碼的情況下,動態(tài)地改變程序的行為,記錄內(nèi)存分配和釋放的信息,從而檢測到內(nèi)存泄漏并找出內(nèi)存泄漏的來源。

3. 小結(jié)

最后我們以一個表格總結(jié)一下本節(jié)的三種代理實現(xiàn)方式的優(yōu)缺點:

二、檢測Natie內(nèi)存泄露

本節(jié)我們將基于PLT/GOT Hook的代理實現(xiàn)方案,介紹檢測Native層內(nèi)存泄漏的整體思路。

1. 原理介紹

在Android中,要檢測Native層的內(nèi)存泄漏,可以重寫malloc、calloc、realloc和free等內(nèi)存分配和釋放函數(shù),以便在每次分配和釋放內(nèi)存時記錄相關(guān)信息。例如,我們可以創(chuàng)建一個全局的內(nèi)存分配表,用于存儲所有分配的內(nèi)存塊及其元數(shù)據(jù)(如分配大小、分配位置等)。然后,在釋放內(nèi)存時,從內(nèi)存分配表中刪除相應(yīng)的條目。定期檢查內(nèi)存分配表,找出沒有被釋放的內(nèi)存。

2. 代碼示例

下面代碼的主要技術(shù)原理是重寫內(nèi)存管理函數(shù),并使用弱符號引用原始的內(nèi)存管理函數(shù),以便在每次分配和釋放內(nèi)存時記錄相關(guān)信息,并能夠在程序運行時動態(tài)地查找和調(diào)用這些函數(shù)。

以下是代碼示例:

#include <cstdlib>
#include <cstdio>
#include <map>
#include <mutex>
#include <dlfcn.h>
#include <execinfo.h>
#include <vector>
#include <android/log.h>  

#define TAG "CheckMemoryLeaks" 
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

// 全局內(nèi)存分配表,存儲分配的內(nèi)存塊及其元數(shù)據(jù)(如分配大小、調(diào)用棧等)
std::map<void*, std::pair<size_t, std::vector<void*>>> g_memoryAllocations;
std::mutex g_memoryAllocationsMutex;

// 定義弱符號引用原始的內(nèi)存管理函數(shù)
extern "C" void* __libc_malloc(size_t size) __attribute__((weak));
extern "C" void  __libc_free(void* ptr) __attribute__((weak));
extern "C" void* __libc_realloc(void *ptr, size_t size) __attribute__((weak));
extern "C" void* __libc_calloc(size_t nmemb, size_t size) __attribute__((weak));

void* (*lt_malloc)(size_t size);
void  (*lt_free)(void* ptr);
void* (*lt_realloc)(void *ptr, size_t size);
void* (*lt_calloc)(size_t nmemb, size_t size);

#define LT_MALLOC  (*lt_malloc)
#define LT_FREE    (*lt_free)
#define LT_REALLOC (*lt_realloc)
#define LT_CALLOC  (*lt_calloc)

// 在分配內(nèi)存時記錄調(diào)用棧
std::vector<void*> record_call_stack() {
  //  ...
}

// 初始化原始內(nèi)存管理函數(shù),如果弱符號未定義,則使用 dlsym 獲取函數(shù)地址
void init_original_functions() {
  if (!lt_malloc) {
    if (__libc_malloc) {
      lt_malloc = __libc_malloc;
    } else {
      lt_malloc = (void*(*)(size_t))dlsym(RTLD_NEXT, "malloc");
    }
  }
  //calloc realloc free 的實現(xiàn)也類似
  ...
}

// 重寫 malloc 函數(shù)
extern "C" void* malloc(size_t size) {
  // 初始化原始內(nèi)存管理函數(shù)
  init_original_functions();

  // 調(diào)用原始的 malloc 函數(shù)
  void* ptr = LT_MALLOC(size);

  // 記錄調(diào)用棧
  std::vector<void*> call_stack = record_call_stack();

  // 在全局內(nèi)存分配表中添加新分配的內(nèi)存塊及其元數(shù)據(jù)
  std::unique_lock<std::mutex> lock(g_memoryAllocationsMutex);
  g_memoryAllocations[ptr] = std::make_pair(size, call_stack);

  return ptr;
}

// 重寫 calloc 函數(shù)
extern "C" void* calloc(size_t nmemb, size_t size) {
  // 跟 malloc 實現(xiàn)類似
  // ...
}

// 重寫 realloc 函數(shù)
extern "C" void* realloc(void* ptr, size_t size) {
  // 初始化原始內(nèi)存管理函數(shù)
  init_original_functions();

  // 調(diào)用原始的 realloc 函數(shù)
  void* newPtr = LT_REALLOC(ptr, size);

  // 記錄調(diào)用棧
  std::vector<void*> call_stack = record_call_stack();
  
  // 更新全局內(nèi)存分配表中的內(nèi)存塊及其元數(shù)據(jù)
  std::unique_lock<std::mutex> lock(g_memoryAllocationsMutex);
  g_memoryAllocations.erase(ptr);
  g_memoryAllocations[newPtr] = std::make_pair(size, call_stack);

  return newPtr;
}

// 重寫 free 函數(shù)
extern "C" void free(void* ptr) {
  // 初始化原始內(nèi)存管理函數(shù)
  init_original_functions();

  // 從全局內(nèi)存分配表中刪除釋放的內(nèi)存塊
  std::unique_lock<std::mutex> lock(g_memoryAllocationsMutex);
  g_memoryAllocations.erase(ptr);

  // 調(diào)用原始的 free 函數(shù)
  LT_FREE(ptr);
}

// 定義一個函數(shù)用于檢查內(nèi)存泄漏
void check_memory_leaks() {
  // 使用互斥鎖保護(hù)對全局內(nèi)存分配表的訪問,防止在多線程環(huán)境下發(fā)生數(shù)據(jù)競爭
  std::unique_lock<std::mutex> lock(g_memoryAllocationsMutex);

  // 如果全局內(nèi)存分配表為空,說明沒有檢測到內(nèi)存泄漏
  if (g_memoryAllocations.empty()) {
    LOGD("No memory leaks detected.");
  } else {
    // 如果全局內(nèi)存分配表不為空,說明檢測到了內(nèi)存泄漏
    LOGD("Memory leaks detected:");
    // 遍歷全局內(nèi)存分配表,打印出所有未被釋放的內(nèi)存塊的地址和大小
    for (const auto& entry : g_memoryAllocations) {
      LOGD("  Address: %p, Size: %zu bytes\n", entry.first, entry.second.first);
      LOGD("  Call stack:");
      for (void* frame : entry.second.second) {
        LOGD("    %p\n", frame);
      }
    }
  }
}

int main() {
  // 初始化原始內(nèi)存管理函數(shù)
  init_original_functions();
  
  // 示例代碼
  void* ptr1 = malloc(10);
  void* ptr2 = calloc(10, sizeof(int));
  void* ptr3 = malloc(20);
  ptr3 = realloc(ptr3, 30);
  free(ptr1);
  free(ptr2);
  free(ptr3);

  // 檢查內(nèi)存泄漏
  check_memory_leaks();

  return 0;
}

上面代碼的核心邏輯包括:

  • 重寫內(nèi)存管理函數(shù):重寫malloc、calloc、realloc和free,在分配內(nèi)存時將內(nèi)存塊及其信息添加到全局內(nèi)存分配表,釋放內(nèi)存時從表中刪除相應(yīng)內(nèi)存塊。
  • 弱符號引用原始內(nèi)存管理函數(shù):使用__attribute__((weak))定義四個弱符號引用glibc/eglibc中的內(nèi)存管理函數(shù)。在init_original_functions函數(shù)中檢查弱符號定義,若未定義則使用dlsym函數(shù)查找原始內(nèi)存管理函數(shù)。
  • 全局內(nèi)存分配表:定義全局內(nèi)存分配表存儲所有分配的內(nèi)存塊及其信息。表是一個map,鍵是內(nèi)存塊地址,值是一個pair,包含內(nèi)存塊大小和調(diào)用棧。
  • 調(diào)用棧記錄:分配內(nèi)存時記錄當(dāng)前調(diào)用棧,有助于檢測內(nèi)存泄漏時找出泄漏來源。
  • 內(nèi)存泄漏檢測:定義check_memory_leaks函數(shù)檢查全局內(nèi)存分配表中仍存在的內(nèi)存塊,表示存在內(nèi)存泄漏。

(1) 使用弱符號:防止對dlsym函數(shù)的調(diào)用導(dǎo)致無限遞歸

dlsym函數(shù)用于查找動態(tài)鏈接庫中的符號。但是在glibc和eglibc中,dlsym函數(shù)內(nèi)部可能會調(diào)用calloc函數(shù)。如果我們正在重定義calloc函數(shù),并且在calloc函數(shù)中調(diào)用dlsym函數(shù)來獲取原始的calloc函數(shù),那么就會產(chǎn)生無限遞歸。

__libc_calloc等函數(shù)被聲明為弱符號,這是為了避免與glibc或eglibc中對這些函數(shù)的強符號定義產(chǎn)生沖突。然后在init_original_functions函數(shù)中,我們檢查了__libc_calloc等函數(shù)是否為nullptr。如果是,那么說明glibc或eglibc沒有定義這些函數(shù),那就使用dlsym函數(shù)獲取這些函數(shù)的地址。如果不是,那么說明glibc或eglibc已經(jīng)定義了這些函數(shù),那就直接使用那些定義。

(2) 關(guān)于RTLD_NEXT的解釋

RTLD_NEXT是一個特殊的“偽句柄”,用于在動態(tài)鏈接庫函數(shù)中查找下一個符號。它常常與dlsym函數(shù)一起使用,用于查找和調(diào)用原始的(被覆蓋或者被截獲的)函數(shù)。

在Linux系統(tǒng)中,如果一個程序鏈接了多個動態(tài)鏈接庫,而這些庫中有多個定義了同名的函數(shù),那么在默認(rèn)情況下,程序會使用第一個找到的函數(shù)。但有時候,我們可能需要在一個庫中覆蓋另一個庫中的函數(shù),同時又需要調(diào)用原始的函數(shù)。這時候就可以使用RTLD_NEXT。

dlsym(RTLD_NEXT, "malloc")會查找下一個名為"malloc"的符號,即原始的malloc函數(shù)。然后我們就可以在自定義的malloc函數(shù)中調(diào)用原始的malloc函數(shù)了。

(3) 注意事項

檢測內(nèi)存泄漏可能會增加程序的運行時開銷,并可能導(dǎo)致一些與線程安全相關(guān)的問題。在使用這種方法時,我們需要確保代碼是線程安全的,并在不影響程序性能的情況下進(jìn)行內(nèi)存泄漏檢測。同時,手動檢測內(nèi)存泄漏可能無法發(fā)現(xiàn)所有的內(nèi)存泄漏,因此建議大家還要使用其他工具(如AddressSanitizer、LeakSanitizer或Valgrind)來輔助檢測內(nèi)存泄漏。

三、獲取Android Native堆棧

大家可能也注意到了,在第二部分的Native內(nèi)存泄露檢測實現(xiàn)中,record_call_stack的實現(xiàn)省略了。所以我們還遺留了一個問題:應(yīng)該如何記錄分配內(nèi)存時的調(diào)用棧呢?最后一節(jié)我們就來闡述獲取Android Native堆棧的方法。

1. 使用unwind函數(shù)

(3) 工具和方法

對于Android系統(tǒng),不能直接使用backtrace_symbols函數(shù),因為它在Android Bionic libc中沒有實現(xiàn)。但是,我們可以使用dladdr函數(shù)替代backtrace_symbols來獲取符號信息。

Android NDK提供了unwind.h頭文件,其中定義了unwind函數(shù),可以用于獲取任意線程的堆棧信息。

(2) 獲取當(dāng)前線程的堆棧信息

如果我們需要獲取當(dāng)前線程的堆棧信息,可以使用Android NDK中的unwind函數(shù)。以下是使用unwind函數(shù)獲取堆棧信息的示例代碼:

#include <unwind.h>
#include <dlfcn.h>
#include <stdio.h>

// 定義一個結(jié)構(gòu)體,用于存儲回溯狀態(tài)
struct BacktraceState {
    void** current;
    void** end;
};

// 回溯回調(diào)函數(shù),用于處理每一幀的信息
_Unwind_Reason_Code unwind_callback(struct _Unwind_Context* context, void* arg) {
    BacktraceState* state = static_cast<BacktraceState*>(arg);
    uintptr_t pc = _Unwind_GetIP(context);
    if (pc) {
        if (state->current == state->end) {
            return _URC_END_OF_STACK;
        } else {
            *state->current++ = reinterpret_cast<void*>(pc);
        }
    }
    return _URC_NO_REASON;
}

// 捕獲回溯信息,將其存儲到buffer中
void capture_backtrace(void** buffer, int max) {
    BacktraceState state = {buffer, buffer + max};
    _Unwind_Backtrace(unwind_callback, &state);
}

// 打印回溯信息
void print_backtrace(void** buffer, int count) {
    for (int idx = 0; idx < count; ++idx) {
        const void* addr = buffer[idx];
        const char* symbol = "";

        Dl_info info;
        if (dladdr(addr, &info) && info.dli_sname) {
            symbol = info.dli_sname;
        }

        // 計算相對地址
        void* relative_addr = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(info.dli_fbase));

        printf("%-3d %p %s (relative addr: %p)\n", idx, addr, symbol, relative_addr);
    }
}

// 主函數(shù)
int main() {
    const int max_frames = 128;
    void* buffer[max_frames];

    // 捕獲回溯信息
    capture_backtrace(buffer, max_frames);
    // 打印回溯信息
    print_backtrace(buffer, max_frames);

    return 0;
}

在上述代碼中,capture_backtrace函數(shù)使用_Unwind_Backtrace函數(shù)獲取堆棧信息,然后我們使用dladdr函數(shù)獲取到函數(shù)所在的SO庫的基地址(info.dli_fbase),然后計算出函數(shù)的相對地址(relative_addr)。然后在打印堆棧信息時,同時打印出函數(shù)的相對地址。

(3) libunwind的相關(guān)接口

①  _Unwind_Backtrace

_Unwind_Backtrace是libunwind庫的函數(shù),用于獲取當(dāng)前線程調(diào)用堆棧。它遍歷棧幀并在每個棧幀上調(diào)用用戶定義的回調(diào)函數(shù),以獲取棧幀信息(如函數(shù)地址、參數(shù)等)。函數(shù)原型如下:

_Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn trace, void *trace_argument);

參數(shù):

  • trace:回調(diào)函數(shù),會在每個堆棧幀上被調(diào)用?;卣{(diào)函數(shù)需返回_Unwind_Reason_Code類型值,表示執(zhí)行結(jié)果。
  • trace_argument:用戶自定義參數(shù),傳遞給回調(diào)函數(shù)。通常用于存儲堆棧信息或其他用戶數(shù)據(jù)。

②  _Unwind_GetIP

_Unwind_GetIP是libunwind庫的函數(shù),用于獲取當(dāng)前棧幀的指令指針(即當(dāng)前函數(shù)的返回地址)。它依賴底層硬件架構(gòu)(如ARM、x86等)和操作系統(tǒng)實現(xiàn)。函數(shù)原型如下:

uintptr_t _Unwind_GetIP(struct _Unwind_Context *context);

參數(shù):

  • context:當(dāng)前棧幀的上下文信息。在_Unwind_Backtrace函數(shù)中創(chuàng)建并在每個棧幀上傳遞給回調(diào)函數(shù)。
  • _Unwind_GetIP返回?zé)o符號整數(shù),表示當(dāng)前函數(shù)的返回地址??捎么说刂帆@取函數(shù)的符號信息,如函數(shù)名、源文件名和行號等。

③ 在不同Android版本中的可用性

_Unwind_Backtrace和_Unwind_GetIP函數(shù)在libunwind庫中定義,該庫是GNU C Library(glibc)的一部分。但Android系統(tǒng)使用輕量級的C庫Bionic libc,而非glibc。因此,這兩個函數(shù)在Android系統(tǒng)中的可用性取決于Bionic libc和Android系統(tǒng)版本。

在早期Android版本(如Android 4.x),Bionic libc未完全實現(xiàn)libunwind庫功能,導(dǎo)致_Unwind_Backtrace和_Unwind_GetIP函數(shù)可能無法正常工作。這時,需使用其他方法獲取堆棧信息,如手動遍歷棧幀或使用第三方庫。

從Android 5.0(Lollipop)起,Bionic libc提供更完整的libunwind庫支持,包括_Unwind_Backtrace和_Unwind_GetIP函數(shù)。因此,在Android 5.0及更高版本中,可直接使用這兩個函數(shù)獲取堆棧信息。

盡管這兩個函數(shù)在新版Android系統(tǒng)中可用,但它們的行為可能受編譯器優(yōu)化、調(diào)試信息等影響。實際使用中,我們需要根據(jù)具體情況選擇最合適的方法。

2. 手動遍歷棧幀來實現(xiàn)獲取堆棧信息

在Android系統(tǒng)中,_Unwind_Backtrace的具體實現(xiàn)依賴于底層硬件架構(gòu)(例如ARM、x86等)和操作系統(tǒng)。它會使用特定于架構(gòu)的寄存器和數(shù)據(jù)結(jié)構(gòu)來遍歷棧幀。例如,在ARM64架構(gòu)上,_Unwind_Backtrace會使用Frame Pointer(FP)寄存器和Link Register(LR)寄存器來遍歷棧幀。

如果不使用_Unwind_Backtrace,我們可以手動遍歷棧幀來實現(xiàn)獲取堆棧信息。

(1) ARM64架構(gòu)下的示例代碼

以下是一個基于ARM64架構(gòu)的示例代碼,展示如何使用Frame Pointer(FP)寄存器手動遍歷棧幀:

#include <stdio.h>
#include <dlfcn.h>

void print_backtrace_manual() {
    uintptr_t fp = 0;
    uintptr_t lr = 0;

    // 獲取當(dāng)前的FP和LR寄存器值
    asm("mov %0, x29" : "=r"(fp));
    asm("mov %0, x30" : "=r"(lr));

    while (fp) {
        // 計算上一個棧幀的FP和LR寄存器值
        uintptr_t prev_fp = *(uintptr_t*)(fp);
        uintptr_t prev_lr = *(uintptr_t*)(fp + 8);

        // 獲取函數(shù)地址對應(yīng)的符號信息
        Dl_info info;
        if (dladdr(reinterpret_cast<void*>(lr), &info) && info.dli_sname) {
            printf("%p %s\n", reinterpret_cast<void*>(lr), info.dli_sname);
        } else {
            printf("%p\n", reinterpret_cast<void*>(lr));
        }

        // 更新FP和LR寄存器值
        fp = prev_fp;
        lr = prev_lr;
    }
}

在上述代碼中,我們首先獲取當(dāng)前的FP(x29)和LR(x30)寄存器值。然后,通過遍歷FP鏈,獲取每個棧幀的返回地址(存儲在LR寄存器中)。最后,使用dladdr函數(shù)獲取函數(shù)地址對應(yīng)的符號信息,并打印堆棧信息。

在這段代碼中,*(uintptr_t*)(fp)表示的是取fp所指向的內(nèi)存地址處的值。fp是一個無符號整數(shù),表示的是一個內(nèi)存地址,(uintptr_t*)(fp)將fp轉(zhuǎn)換成一個指針,然后*操作符取該指針?biāo)赶虻闹怠?/p>

在ARM64架構(gòu)中,函數(shù)調(diào)用時會創(chuàng)建一個新的棧幀。每個棧幀中包含了函數(shù)的局部變量、參數(shù)、返回地址以及其他與函數(shù)調(diào)用相關(guān)的信息。其中,F(xiàn)rame Pointer(FP,幀指針)寄存器(x29)保存了上一個棧幀的FP寄存器值,Link Register(LR)寄存器(x30)保存了函數(shù)的返回地址。

在這段代碼中,fp變量保存了當(dāng)前棧幀的FP寄存器值,也就是上一個棧幀的幀基址。因此,*(uintptr_t*)(fp)取的就是上一個棧幀的FP寄存器值,即上上個棧幀的幀基址。這個值在遍歷棧幀時用來更新fp變量,以便在下一次循環(huán)中處理上一個棧幀。

(2) ARM架構(gòu)下的示例代碼

在ARM架構(gòu)下,我們可以使用Frame Pointer(FP)寄存器(R11)和Link Register(LR)寄存器(R14)來手動遍歷棧幀。以下是一個基于ARM架構(gòu)的示例代碼,展示如何手動遍歷棧幀以獲取堆棧信息:

#include <stdio.h>
#include <dlfcn.h>

void print_backtrace_manual_arm() {
    uintptr_t fp = 0;
    uintptr_t lr = 0;

    // 獲取當(dāng)前的FP和LR寄存器值
    asm("mov %0, r11" : "=r"(fp));
    asm("mov %0, r14" : "=r"(lr));

    while (fp) {
        // 計算上一個棧幀的FP和LR寄存器值
        uintptr_t prev_fp = *(uintptr_t*)(fp);
        uintptr_t prev_lr = *(uintptr_t*)(fp + 4);

        // 獲取函數(shù)地址對應(yīng)的符號信息
        Dl_info info;
        if (dladdr(reinterpret_cast<void*>(lr), &info) && info.dli_sname) {
            printf("%p %s\n", reinterpret_cast<void*>(lr), info.dli_sname);
        } else {
            printf("%p\n", reinterpret_cast<void*>(lr));
        }

        // 更新FP和LR寄存器值
        fp = prev_fp;
        lr = prev_lr;
    }
}

在這個示例代碼中,我們首先獲取當(dāng)前的FP(R11)和LR(R14)寄存器值。然后,通過遍歷FP鏈,獲取每個棧幀的返回地址(存儲在LR寄存器中)。最后,使用dladdr函數(shù)獲取函數(shù)地址對應(yīng)的符號信息,并打印堆棧信息。

通過以上示例代碼,我們可以看到,在不同架構(gòu)上手動遍歷棧幀以獲取堆棧信息的方法大致相同,只是寄存器和數(shù)據(jù)結(jié)構(gòu)有所不同。這種方法提供了一種在不使用_Unwind_Backtrace的情況下獲取堆棧信息的方式,有助于我們更好地理解和調(diào)試程序。

(3) 寄存器

在函數(shù)調(diào)用過程中,fp(Frame Pointer,幀指針)、lr(Link Register,鏈接寄存器)和sp(Stack Pointer,棧指針)是三個關(guān)鍵寄存器,它們之間的關(guān)系如下:

  • fp(Frame Pointer):幀指針寄存器用于指向當(dāng)前棧幀的幀基址。在函數(shù)調(diào)用過程中,每個函數(shù)都會有一個棧幀,用于存儲函數(shù)的局部變量、參數(shù)、返回地址等信息。fp寄存器有助于定位和訪問這些信息。在不同的架構(gòu)中,fp寄存器可能有不同的名稱,例如,在ARM64架構(gòu)中,fp寄存器對應(yīng)X29;在ARM架構(gòu)中,fp寄存器對應(yīng)R11;在x86_64架構(gòu)中,fp寄存器對應(yīng)RBP。
  • lr(Link Register):鏈接寄存器用于保存函數(shù)的返回地址。當(dāng)一個函數(shù)被調(diào)用時,程序需要知道在函數(shù)執(zhí)行完畢后返回到哪里繼續(xù)執(zhí)行。這個返回地址就被保存在lr寄存器中。在不同的架構(gòu)中,lr寄存器可能有不同的名稱,例如,在ARM64架構(gòu)中,lr寄存器對應(yīng)X30;在ARM架構(gòu)中,lr寄存器對應(yīng)R14;在x86_64架構(gòu)中,返回地址通常被保存在棧上,而不是專用寄存器中。
  • sp(Stack Pointer):棧指針寄存器用于指向當(dāng)前棧幀的棧頂。在函數(shù)調(diào)用過程中,棧指針會根據(jù)需要分配或釋放棧空間。在不同的架構(gòu)中,sp寄存器可能有不同的名稱,例如,在ARM64架構(gòu)中,sp寄存器對應(yīng)XSP;在ARM架構(gòu)中,sp寄存器對應(yīng)R13;在x86_64架構(gòu)中,sp寄存器對應(yīng)RSP。

fp、lr和sp三者在函數(shù)調(diào)用過程中共同協(xié)作,以實現(xiàn)正確的函數(shù)調(diào)用和返回。fp用于定位棧幀中的數(shù)據(jù),lr保存函數(shù)的返回地址,而sp則負(fù)責(zé)管理??臻g。在遍歷棧幀以獲取堆棧信息時,我們需要利用這三個寄存器之間的關(guān)系來定位每個棧幀的位置和內(nèi)容。

(4) 棧幀

棧幀(Stack Frame)是函數(shù)調(diào)用過程中的一個重要概念。每次函數(shù)調(diào)用時,都會在棧上創(chuàng)建一個新的棧幀。棧幀包含了函數(shù)的局部變量、參數(shù)、返回地址以及其他一些與函數(shù)調(diào)用相關(guān)的信息。下圖是一個標(biāo)準(zhǔn)的函數(shù)調(diào)用過程:

  • EBP:基址指針寄存器,指向棧幀的底部。在 ARM 下寄存器為 R11。在 ARM64 中寄存器為 X29。
  • ESP:棧指針寄存器,指向棧幀的棧頂 , 在 ARM 下寄存器為 R13。
  • EIP:指令寄存器,存儲的是 CPU 下次要執(zhí)行的指令的地址,ARM 下為 PC,寄存器為 R15。

每次函數(shù)調(diào)用都會保存 EBP 和 EIP 用于在返回時恢復(fù)函數(shù)棧幀。這里所有被保存的 EBP 就像一個鏈表指針,不斷地指向調(diào)用函數(shù)的 EBP。

在Android系統(tǒng)中,棧幀的基本原理與其他操作系統(tǒng)相同,通過SP和FP所限定的stack frame,就可以得到母函數(shù)的SP和FP,從而得到母函數(shù)的stack frame(PC,LR,SP,F(xiàn)P會在函數(shù)調(diào)用的第一時間壓棧),以此追溯,即可得到所有函數(shù)的調(diào)用順序。

在ARM64和ARM架構(gòu)中,我們可以使用FP鏈(幀指針鏈)來遍歷棧幀。具體方法是:從當(dāng)前FP寄存器開始,沿著FP鏈向上遍歷,直到遇到空指針(NULL)或者無效地址。在遍歷過程中,我們可以從每個棧幀中提取返回地址(存儲在LR寄存器中)以及其他相關(guān)信息。

(5) 名字修飾(Name Mangling)

Native堆棧的符號信息跟代碼中定義的函數(shù)名字相比,可能會有一些差別,因為GCC生成的符號表有一些修飾規(guī)則。

C++支持函數(shù)重載,即同一個函數(shù)名可以有不同的參數(shù)類型和個數(shù)。為了在編譯時區(qū)分這些函數(shù),GCC會對函數(shù)名進(jìn)行修飾,生成獨特的符號名稱。修飾后的名稱包含了函數(shù)名、參數(shù)類型等信息。例如,對于如下C++函數(shù):

namespace test {
  int foo(int a, double b);
}

經(jīng)過GCC修飾后,生成的符號可能類似于:_ZN4test3fooEid,其中:

  • _ZN和E是修飾前綴和后綴,用于標(biāo)識這是一個C++符號。
  • 4test表示命名空間名為test,4表示命名空間名的長度。
  • 3foo表示函數(shù)名為foo,3表示函數(shù)名的長度。
  • id表示函數(shù)的參數(shù)類型,i代表int,d代表double。

四、實踐建議

通過前文的詳細(xì)介紹,我們已經(jīng)了解了如何實現(xiàn)Android Native內(nèi)存泄漏監(jiān)控的三個方面:包括代理實現(xiàn)、檢測Native內(nèi)存泄露和獲取Android Native堆棧的方法。最后,我們再來看一下現(xiàn)有的一些內(nèi)存泄露檢測工具對比,并給出一些實踐建議。

1. Native 內(nèi)存泄露檢測工具對比

在實際應(yīng)用中,我們需要根據(jù)具體場景選擇最合適的方案。下面表格中的前三種工具都是現(xiàn)成的,但是具有一定的局限性,特別是不適合在線上使用。

2. 實踐建議

在實際項目中,我們可以結(jié)合多種內(nèi)存泄漏檢測方案來提高檢測效果。以下是一些建議:

  • 編碼規(guī)范:在編寫代碼時,遵循一定的編碼規(guī)范和最佳實踐,例如使用智能指針、避免循環(huán)引用等,可以有效地降低內(nèi)存泄漏的風(fēng)險。
  • 代碼審查:在開發(fā)過程中,定期進(jìn)行代碼審查,檢查代碼中是否存在潛在的內(nèi)存泄漏風(fēng)險。代碼審查可以幫助我們及時發(fā)現(xiàn)和修復(fù)問題,提高代碼質(zhì)量。
  • 自動化測試:在項目中引入自動化測試,對關(guān)鍵功能進(jìn)行內(nèi)存泄漏檢測??梢栽诔掷m(xù)集成環(huán)境中使用ASan、LSan等工具來檢測內(nèi)存泄漏,確保新提交的代碼不會引入新的內(nèi)存泄漏問題。
  • 性能監(jiān)控:在線上環(huán)境中,定期監(jiān)控應(yīng)用程序的內(nèi)存使用情況。如果發(fā)現(xiàn)內(nèi)存使用異常,可以使用手動檢測方法或者將問題反饋到開發(fā)環(huán)境,使用其他工具進(jìn)行進(jìn)一步分析和處理。
  • 問題定位:當(dāng)發(fā)現(xiàn)內(nèi)存泄漏問題時,根據(jù)工具提供的錯誤信息,快速定位問題發(fā)生的位置。結(jié)合堆棧信息、相對地址等,可以幫助我們更好地理解問題的原因,從而修復(fù)問題。

五、總結(jié)

在開發(fā)和測試階段,我們可以使用ASan、LSan和Valgrind等工具來檢測內(nèi)存泄漏。而在線上環(huán)境中,由于這些工具的性能開銷較大,不適合直接使用。在這種情況下,我們可以采用手動檢測的方法,結(jié)合代碼審查和良好的編程習(xí)慣,來盡可能地減少內(nèi)存泄漏的發(fā)生。

然而,這些工具并不能保證檢測出所有的內(nèi)存泄漏。內(nèi)存泄漏的發(fā)現(xiàn)和修復(fù),需要我們對代碼有深入的理解,以及良好的編程習(xí)慣。只有這樣,我們才能有效地防止和解決內(nèi)存泄漏問題,從而提高我們的應(yīng)用程序的穩(wěn)定性和性能。

責(zé)任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2013-08-02 09:52:14

AndroidApp內(nèi)存泄漏

2015-04-17 10:35:51

c++c++程序內(nèi)存泄漏檢測代碼

2011-08-15 10:16:55

內(nèi)存泄露

2023-10-31 16:40:38

LeakCanary內(nèi)存泄漏

2010-09-26 15:38:33

JVM內(nèi)存泄漏

2024-04-19 08:00:00

2025-11-17 09:27:09

2017-09-07 16:52:23

2013-08-07 10:16:43

Android內(nèi)存泄漏

2021-03-26 05:59:10

內(nèi)存檢測工具

2018-12-07 10:52:08

內(nèi)存泄漏方法

2015-07-10 09:15:47

LeakCanary內(nèi)存泄漏

2010-09-25 11:07:45

Java內(nèi)存泄漏

2021-01-16 16:07:51

RustAndroid Nat內(nèi)存

2011-06-16 09:28:02

C++內(nèi)存泄漏

2009-06-16 11:20:22

內(nèi)存泄漏

2025-03-03 00:00:00

2021-11-08 12:44:48

AndroidC++內(nèi)存

2025-10-31 07:32:00

內(nèi)存泄漏C++編程

2016-12-22 17:21:11

Android性能優(yōu)化內(nèi)存泄漏
點贊
收藏

51CTO技術(shù)棧公眾號

色综合天天做天天爱| 岛国精品一区二区| 丝袜美腿精品国产二区| 亚洲第一天堂久久| 黑人玩欧美人三根一起进 | 97精品国产91久久久久久| 国产精品无码毛片| 免费一区二区三区四区| 一区二区免费视频| 日本欧美精品久久久| 国产乱码久久久| 亚洲国产清纯| 日韩在线观看免费高清完整版| 亚洲一区二区中文字幕在线观看| 国产伦子伦对白在线播放观看| 久久精品水蜜桃av综合天堂| 亚洲a区在线视频| 成人精品在线看| 亚洲精品二区三区| 亚洲欧洲在线视频| 第一页在线视频| av在线日韩| 粉嫩久久99精品久久久久久夜| 4p变态网欧美系列| 日韩三级在线观看视频| 伊人久久大香线蕉av不卡| 欧美一级精品在线| 一区二区三区韩国| 欧美gv在线| 亚洲精品国产成人久久av盗摄| 欧美精品亚洲精品| 亚洲精品中文字幕成人片| 蜜桃传媒麻豆第一区在线观看| 久久人人爽国产| 男人的午夜天堂| 国产91精品对白在线播放| 日韩欧美综合一区| 国产永久免费网站| 91tv亚洲精品香蕉国产一区| 欧美日韩国产激情| 国产一区二区三区乱码| 黄色小网站在线观看| 久久精品这里都是精品| 久久综合九色99| 亚洲精品国产精品国| 狠狠色丁香久久婷婷综| 国产精品激情av在线播放| 九九热精品视频在线| 伊人久久成人| 欧美区二区三区| 日韩国产第一页| 日韩午夜电影网| 在线播放精品一区二区三区 | 久久综合五月天婷婷伊人| 国产精品一区在线播放| 午夜精品久久久久久久第一页按摩| 麻豆成人91精品二区三区| 国产精品久久久久久av| 无码免费一区二区三区| 久久激情婷婷| 日韩免费av片在线观看| 国产黄色免费观看| 国产精品呻吟| 欧洲成人免费视频| 黄色片中文字幕| 久久福利毛片| 日韩免费在线免费观看| 波多野结衣高清在线| 视频一区免费在线观看| 国产精品久久77777| 无码视频在线观看| 日本最新不卡在线| 成人黄色av播放免费| av天堂一区二区三区| 国产福利一区二区三区视频 | 国产h在线观看| 久久久国产午夜精品| 色之综合天天综合色天天棕色| 丁香婷婷在线| 最新不卡av在线| 在线观看三级网站| 黑人精品视频| 日韩欧美999| 手机视频在线观看| 精品国产不卡一区二区| 欧美精品一区二区三区久久久| 国产激情第一页| 精品大片一区二区| yellow中文字幕久久| 精品视频在线观看免费| 亚洲综合三区| 国产日韩精品在线| 韩国中文字幕hd久久精品| 久久婷婷综合激情| 亚洲免费久久| 青草av在线| 欧美性猛交xxxx黑人猛交| 中文字幕网av| 欧美日本三级| 亚洲天堂男人的天堂| 成年人一级黄色片| 狠狠久久婷婷| 青青青国产精品一区二区| 国产一区二区波多野结衣| 国产精品99久久久久久似苏梦涵| 国产精品.com| 2021av在线| 午夜视黄欧洲亚洲| 日本美女视频一区| 亚洲理论电影片| 欧美成人全部免费| 青青草视频在线观看免费| 国产成人8x视频一区二区| 涩涩涩999| av资源中文在线天堂| 51久久夜色精品国产麻豆| 国产精品九九九九九| 久久久久国产精品| 日韩av免费在线观看| 亚洲av色香蕉一区二区三区| 欧美精彩视频一区二区三区| 男女猛烈激情xx00免费视频| 亚洲色图综合| 亚洲午夜av电影| 日韩av女优在线观看| 国产乱子伦视频一区二区三区| 日韩在线电影一区| 蜜桃麻豆av在线| 日韩免费高清av| 日本 欧美 国产| 三级不卡在线观看| 精品麻豆av| 免费在线中文字幕| 欧美久久婷婷综合色| 69xxx免费| 久久性色av| 久久精品国产精品国产精品污 | 国产亚洲精品aa午夜观看| 日韩精品一区在线视频| 美女精品视频在线| 色婷婷**av毛片一区| 无码视频一区二区三区| 26uuu国产电影一区二区| 青春草国产视频| 日韩视频1区| 免费av一区二区| 国产精品天天操| 中文字幕一区免费在线观看| 色综合色综合色综合色综合| 欧美三级情趣内衣| 国产精品扒开腿做爽爽爽视频 | 国产精品资源在线| 熟女视频一区二区三区| 四虎地址8848精品| 日韩一区二区三区国产| 国产精品九九九九| 亚洲天堂中文字幕| 中文字幕一二三| 午夜欧美精品| 国产精品久久久久久久天堂第1集 国产精品久久久久久久免费大片 国产精品久久久久久久久婷婷 | 精品国产aaa| 日本va欧美va瓶| 亚洲欧美精品| 日韩第二十一页| 日韩一区av在线| 国产绳艺sm调教室论坛| 一区二区国产盗摄色噜噜| 性高潮免费视频| 亚洲综合丁香| 亚洲欧美久久234| 91精品国产一区二区在线观看 | 精品欧美一区二区在线观看视频| 极品视频在线| 亚洲日本成人女熟在线观看| 成人免费一区二区三区| ㊣最新国产の精品bt伙计久久| 手机在线视频一区| 欧美午夜电影在线观看| 国模精品一区二区三区| 欧美粗大gay| 在线国产精品视频| 国产成人三级一区二区在线观看一| 一区二区日韩电影| 国内精品久久99人妻无码| 热久久免费视频| 99re6这里有精品热视频| 国产精品毛片视频| 国产精品白嫩美女在线观看| av在线麻豆| 亚洲激情在线观看视频免费| 国产九色91回来了| 一区二区三区色| 中文字幕xxx| 精品一区二区免费在线观看| av在线观看地址| 欧洲激情视频| 国产精品一区二区三区免费观看 | 无需播放器亚洲| 狠狠色噜噜狠狠色综合久| 欧美色网在线| 欧美日韩国产二区| 国外av在线| 日韩欧美国产精品| www.久久视频| 亚洲高清不卡在线| 懂色av粉嫩av蜜臀av一区二区三区| 成人在线综合网站| 91女神在线观看| 免费在线播放第一区高清av| 中文字幕av久久| 蜜桃一区二区| 国产成人成网站在线播放青青| 亚洲一区二区三区四区| 欧美精品xxx| 色网站在线看| 精品调教chinesegay| 国产美女永久免费| 91黄色在线观看| 久久影院一区二区| 亚洲欧洲另类国产综合| 少妇久久久久久久久久| 成人黄色a**站在线观看| 天堂在线中文在线| 日韩精品电影在线观看| 日本韩国欧美在线观看| 一区二区三区四区电影| 日本在线观看一区二区三区| 精品国产导航| 亚洲影影院av| 久久久加勒比| 国产成人精品综合久久久| 高清电影在线观看免费| 欧美精品一区三区| 精产国品自在线www| 中文字幕日韩欧美| 成人在线观看黄色| 国产一区二区三区在线观看视频 | 成人h动漫精品| 久久久久亚洲av无码麻豆| 美腿丝袜亚洲色图| 欧美黑人又粗又大又爽免费| 国产精品人人爽人人做我的可爱| av免费观看大全| 亚洲国产日韩欧美一区二区三区| 欧美精品一区二区性色a+v| 99久久激情| 亚洲欧美日韩不卡| 99视频精品全部免费在线视频| 日韩福利视频| av亚洲在线观看| 亚洲国产欧洲综合997久久| 色综合中文网| 日韩不卡av| 激情五月综合网| 色播亚洲婷婷| 欧美aaaa视频| 国产免费内射又粗又爽密桃视频| 一区二区三区在线观看免费| av电影一区二区三区| 久久久久久久久久久9不雅视频| 一级特黄录像免费播放全99| 三级电影一区| 伊人色综合影院| 自由日本语亚洲人高潮| 大陆极品少妇内射aaaaaa| 欧美日本一区二区视频在线观看| 欧美国产视频一区| 亚洲黄色影片| 国产第一页视频| 蜜桃视频在线观看一区| 亚洲黄色av片| 成人一区在线看| 国产精品无码专区| 久久久精品tv| 蜜臀av午夜精品久久| 亚洲制服丝袜在线| 亚洲va在线观看| 精品视频在线免费看| 国产毛片毛片毛片毛片| 亚洲国产精品免费| 91大神在线网站| 欧美激情欧美激情| 欧美大胆成人| 亚洲综合小说区| 婷婷综合福利| 在线精品日韩| 在线亚洲观看| 极品粉嫩美女露脸啪啪| 成人精品国产一区二区4080| 亚洲午夜久久久久久久国产| 亚洲人123区| 亚洲永久精品在线观看| 欧美福利电影网| 熟妇人妻系列aⅴ无码专区友真希 熟妇人妻av无码一区二区三区 | 成人av午夜电影| 国产精品一二三区在线观看| 亚洲毛片av在线| 9i精品福利一区二区三区| 欧美一区二区福利视频| 色播色播色播色播色播在线 | 天天干天天舔天天操| 亚洲精品国产一区二区精华液| 久久久久在线视频| 日韩三级在线观看| yiren22亚洲综合伊人22| 久久久久久久久电影| 久久精品国产精品亚洲毛片| 国产在线精品一区二区中文| 99久久亚洲精品| 欧美日韩激情视频在线观看| 国产在线精品免费| 国产肥白大熟妇bbbb视频| 亚洲一区二区欧美日韩| 国产一区二区三区中文字幕| 亚洲美女av电影| 狂野欧美性猛交xxxxx视频| 国产精品视频免费在线观看| 日韩精品导航| 亚洲精品无码国产| 国产一区二区三区在线观看精品| 久久精品—区二区三区舞蹈| 亚洲成人777| 国产高潮流白浆喷水视频| 中文字幕精品av| 欧洲精品一区二区三区| 国产在线一区二| 欧美精品麻豆| 欧美丝袜在线观看| 中文字幕欧美日本乱码一线二线| 久久国产精品免费看| 精品久久久久一区| 中中文字幕av在线| 91久久嫩草影院一区二区| 日韩电影一区| 亚洲天堂网一区| 国产亚洲欧美在线| av资源免费观看| 亚洲国产精品久久久久久| 欧美日韩在线视频免费观看| 亚洲一区二区三区成人在线视频精品| 波多野结衣在线观看一区二区三区 | 天天躁日日躁成人字幕aⅴ| 国产精品igao激情视频| 国产在线不卡一区| 在线观看亚洲网站| 555www色欧美视频| 最近中文字幕免费mv2018在线 | 豆花视频一区二区| 久久综合亚洲精品| 国产精品99久| 中文字幕第28页| 亚洲精品电影在线观看| 国产在线看片免费视频在线观看| 国产亚洲一区二区三区在线播放| 欧美精品一级| 天天躁日日躁狠狠躁av麻豆男男| 亚洲成a人v欧美综合天堂下载| 欧美性受xxxx狂喷水| 97久久精品人搡人人玩| 亚洲区小说区图片区qvod| 久久久久狠狠高潮亚洲精品| 欧美国产一级| 日韩精品视频在线免费观看| 国产精品久久久久久av| 日韩一区二区不卡视频| 日韩欧美高清在线播放| 日韩三级电影| 日韩极品在线观看| av永久免费观看| 欧美日韩国产首页在线观看| 国产一级在线播放| 日韩精品一区在线观看| 国产免费拔擦拔擦8x高清在线人| 精品蜜桃一区二区三区| 日韩在线一区二区三区| 波兰性xxxxx极品hd| 日韩区在线观看| 欧美调教sm| 婷婷精品国产一区二区三区日韩| 久久精品国产亚洲一区二区三区| 欧美三级黄色大片| 精品嫩草影院久久| 亚洲天堂资源| 亚洲最新在线| 成人一区在线看| 伊人久久久久久久久久久久| 久久久精品国产| 加勒比久久高清| 性生活免费在线观看| 亚洲高清免费视频| 91在线导航| 国产精品久久久久免费| 久热精品在线| 中文字幕影音先锋| 亚洲女人天堂色在线7777| 婷婷久久免费视频| 奇米精品一区二区三区| 国产精品久久久久永久免费观看| www.亚洲欧美| 国产精品高潮在线|