学完了 Java 堆内存的优化,这一章咱们正式进入 Native 内存优化的学习。关于许多开发者来说,进行 Native 内存优化要比 Java 堆内存优化的频率少许多。一是 Native 内存可运用的内存大小理论上是手机设备的一切内存,并不会像 Java 堆内存相同被约束在 512M。二是 Naitve 内存比较 Java 堆内存优化会杂乱许多,许多人由于没有相关开发经验就直接抛弃了。

但我想说的是,尽管 Native 内存优化的频率较少,但假如 Naitve 内存占用出现了反常,相同会影响运用的安稳性。因而咱们不能忽略,而且了解 Native 内存优化是 Android 进阶的必经之路。

那在优化 Native 内存之前,和学习 Java 堆内存优化相同,咱们要先了解它的组成。Native 内存的组成并没有 Java 堆内存的组成那么杂乱,首要有 2 部分。

  1. so 库中经过 malloc 、calloc 、realloc 、mmap 等函数请求函数的内存。

  2. Bitmap 的占用,从 Android 8 开端,Bitmap 的内存都是算在 Native 上的。

这一章节咱们先解说榜首部分:so 库请求的内存优化。不过,假如你完全没有 Native 开发经验,学习起来或许会有必定的难度,但只要你耐心多看几遍,肯定能吸收并了解。

so 库内存优化思路

要知道,关于一个 App 的 Native 内存消耗来说,并不是越少越好,而是要确保 Native 内存没有反常,反常指的是占用过大,如超过了手机可运用内存的三分之一。反常状况下,运用进程很简略被体系的 LowMemoryKiller 机制强行杀掉,导致运用不可用。

那咱们该怎样确保 Native 内存没有反常呢?这就要从反常发生的原因说起了,首要有两种原因:一是在 so 库中请求了十分大的内存;二是 so 库有内存的走漏,导致 Native 内存一直增长,最终变得十分的大。想要解决这两个问题,咱们要先定位出问题,也便是要能精确知道反常是哪个 so 库中,乃至是 so 库中哪个函数中的哪一行代码请求了内存,当咱们定位出问题后,优化的计划就很简略了,关于能够修正的 so 库,咱们将走漏的内存及时调用 free 进行开释,减少大内存的请求,关于无法修正的第三方 so 库,能够更换安稳版别。

那怎样才干定位出 so 库中的内存反常运用呢?首要有下面这 3 个过程:

  1. 经过 Native Hook 技能,hook 住 so 库中请求内存和开释内存的函数。在此根底上,咱们就能够统计出一个 so 库一共请求了多少内存,开释了多少内存。而且当 so 库请求了超大的内存时,还能获取 Native 的仓库,便于定位反常函数。

  2. 关于 so 库请求了超大内存的的状况,咱们需求获取 Native 的仓库用于定位问题。

  3. 直接获取的 Native 仓库是一个个 16 进制地址的仓库,无法看出有用信息,所以还需求依据 16 进制的地址仓库复原出 so 名以及详细的函数和方位

后边咱们都会围绕这三个过程进行详细解说和实践。为了便利你了解,这儿经过一个简略 Demo 来说说,怎样经过这 3 个过程定位到 Native 内存中的反常。在 Demo 中,我会新建一个名 testmalloc 的 so 库,而且在这个 so 库中经过 TestMalloc 函数请求了 88 M 的超大内存。

内存优化:so 库申请的内存优化
内存优化:so 库申请的内存优化

Native Hook 技能的原理

在过程一中,咱们要经过 Native Hook 技能 hook 住 so 库中请求内存和开释内存的函数。可要怎样才干完结 Native 的 Hook 呢?首要有 2 种技能计划能够完结:

  1. PLT Hook:经过修正 GOT 外部函数跳转表进行hook。
  1. Inline Hook:经过修正方针函数的汇编代码来进行hook。

接下来,咱们先看榜首种计划。

修正 got 表 hook Native 函数

程序在运转过程中会不断调用函数并履行,而调用函数的过程中只要知道了该函数的地址后才干进行正常的调用。假如是 so 库内部的函数,在编译阶段就能承认函数地址,由于编译器只需核算函数在这个 so 库中的相对偏移地址就能够了,当这个 so 库被加载进内存后并,这个函数的实践地址便是 so 库的基地址 + 该函数的相对偏移地址。

但假如咱们调用的是一个外部库的函数时,比方 malloc 函数,它坐落 libc.so 这个库中,在编译期间就无法承认这个函数的地址了,只要运转时才干知道。这样一来,当程序运转且履行这个外部函数时,由 Linker (动态连接器) 这个体系程序将方针函数的地址写进 got 表中。什么是 got 表呢?

在《从操作体系层面重新认识内存》这一章中咱们说到,so 库其实便是一个 ELF 文件,里边包含了 .text .data .bss 段。其实,ELF 文件还有许多其他的段,got 表便是坐落 .dynamic 段中 (这儿的 got 表实践称号为 .got.plt 表,后边为了便利统称为 got 表),用于记载外部函数的地址,外部库函数的地址在 got 表中的初始值都是 0 ,只要当实践调用这个函数时,Linker 程序才会写入实践的地址。

当咱们想要 hook 某个函数时,比方上面 Demo 中的 Malloc 函数,只需求修正 testmalloc.so 文件中存放在 got 表中的 malloc 函数地址,改成自己的函数地址即可。后续这个 so 库每次调用 malloc 函数都会调用到咱们自己的函数。那为什么这种计划不叫 GOT Hook,要叫 PLT Hook呢?

实践上函数在调用的时分,会先跳转到 plt 表,plt 表是坐落 .text 段中的一张表,plt 表中记载着方针函数 .got 表的地址,.got 表中又记载着方针函数的地址。它们的调用联系如下。

内存优化:so 库申请的内存优化

所以 plt 表的效果相当所以一个跳板,之所以有一个 plt 表,而不直接运用 got 表,是由于这样能够完结延迟绑定,也便是说只要当真正调用方针函数时,再去绑定 got 表中的真实地址。接下来咱们就看看怎样修正 so 库 got 表中 malloc 函数的地址吧!其实也不难,只需求遍历 ELF 文件中的段,找到 .dynamic 段,然后在 .dynamic 段的 plt 表中找到方针函数后修正地址即可,详细完结首要经过下面这 5 个过程。

  1. 获取动态库的基地址。这儿咱们能够经过读取并解析 maps 文件来找到 so 的基地址,咱们也能够经过 dl_iterate_phdr 这个 Linux 供给的函数找到方针 so 库的基地址。
FILE *fp = fopen("/proc/self/maps", "r")
while(fgets(line, sizeof(line), fp))
{
    // “PRIxPTR”是将数据转化成 16 进制地址格局的标志,然后赋值给 base_addr 
    if(NULL != strstr(line, "libtestmalloc.so") &&
       sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
        break;
}
fclose(fp);
  1. 核算 so 库中程序头部表的地址,头部表中记载了 ELF 文件中各个段的地址:
//将 base_addr 强制转化成Elf32_Ehdr格局,即32位 ELF header的结构体,假如是 64 位需求转化成 Elf64_Ehdr
Elf32_Ehdr *header = (Elf32_Ehdr *) (base_addr);
Elf32_Phdr *phdr_table = (Elf32_Phdr *) (base_addr + header->e_phoff);  // 程序头部表的地址
if (phdr_table == 0) {
    return 0;
}
size_t phr_count = header->e_phnum;  // 程序头表项个数
//ELF_Ehdr 的数据结构如下
typedef struct elf_hdr{
    unsigned char e_ident[EI_NIDENT];     /* 魔数和相关信息 */
    Elf_Half    e_type;                 /* 方针文件类型 */
    Elf_Half    e_machine;              /* 硬件体系 */
    Elf_Word    e_version;              /* 方针文件版别 */
    Elf_Addr    e_entry;                /* 程序进入点 */
    Elf_Off     e_phoff;                /* 程序头部偏移量 */
    Elf_Off     e_shoff;                /* 节头部偏移量 */
    Elf_Word    e_flags;                /* 处理器特定标志 */
    Elf_Half    e_ehsize;               /* ELF头部长度 */
    Elf_Half    e_phentsize;            /* 程序头部中一个条意图长度 */
    Elf_Half    e_phnum;                /* 程序头部条目个数  */
    Elf_Half    e_shentsize;            /* 节头部中一个条意图长度 */
    Elf_Half    e_shnum;                /* 节头部条目个数 */
    Elf_Half    e_shstrndx;             /* 节头部字符表索引 */
} Elf_Ehdr;
  1. 遍历程序头部表,获取 .dynamic 段的地址:
unsigned long dynamicAddr;
unsigned int dynamicSize;  
for (int i = 0; i < phr_count; i++) {
    if (phdr_table[i].p_type == PT_DYNAMIC) {
        //so基地址加dynamic段的偏移地址,便是dynamic段的实践地址
        dynamicAddr = phdr_table[i].p_vaddr + base_addr; 
        dynamicSize = phdr_table[i].p_memsz;
        break;
    }
}
  1. 遍历 .dynamic 段,d_tag为 3(不同平台下的 so,这儿的序列或许会不相同,所以为了兼容性考虑,咱们最好用称号来进行承认) 即为 .got.plt 表地址:
int symbolTableAddr = 0;
Elf32_Dyn *dynamic_table = (Elf32_Dyn *) dynamicAddr;
for (i = 0; i < dynamicSize; i++) {
    int val = dynamic_table[i].d_un.d_val;
    if (dynamic_table[i].d_tag == 3) {
        symbolTableAddr = val + base_addr;
        break;
    }
}
  1. 修正内存属性为可写,并遍历 .got.plt 表,找到 Malloc 函数的地址后,将 Malloc 函数地址替换成咱们自己的 Malloc_hook 函数地址:
//读写权限改为可写
mprotect((void *)PAGE_START(symbolTableAddr), PAGE_SIZE, PROT_READ | PROT_WRITE);
int oldFunc = &malloc- (int) base_addr; // 方针函数偏移地址
int newFunc = &malloc_hook ;  // 替换的hook函数的偏移地址
 while (1) {
    if (symbolTableAddr[i].st_value == oldFunc) {
        symbolTableAddr[i].st_value = newFunc;  
        break;
    }
    i++;
}

能够看到经过修正 got 表来达到 hook Native 函数的技能原理并不杂乱,原理便是遍历 ELF 文件然后寻觅方针数据并进行修正。假如对 ELF 文件格局不太明晰的,能够等14章更新后一同结合着来看,里边会持续解说 ELF 文件相关的常识。尽管上面的代码完结看起来比较简略,可是一个能在 Android 项目中运用的 PLT Hook 库,需求考虑不同体系版别的兼容、功能、安稳性等多方面因素,所以一个能用于线上运用的安稳版别的 PLT Hook 的库仍是比较杂乱的。

Inline 办法 hook Native 函数

修正 got 表的 Hook 计划只要对调用外部的库的函数时才有用,假如是调用当前 so 库内的函数,怎样进行 hook 呢?这时,Inline Hook 就派上用场了,但 Inline Hook 的技能较杂乱,兼容性较差,还需求较深的汇编根底,所以我就不深化解说代码完结了,只介绍完结原理。

Inline Hook 是 经过在程序运转时动态修正内存中的汇编指令,来改变程序履行流程的一种 Hook 办法,根本思路便是在已有的代码段中刺进跳转指令,把代码的履行流程转向咱们自己的函数中

比方 Demo 中的 TestMalloc 函数,它的汇编指令如下。当咱们将榜首行 (地址为 62c)和第二行(地址为 630)的指令掩盖成跳转到咱们自己的函数的指令时,那履行这个函数就会先跳转到咱们自己的函数中(需求掩盖榜首和第二行是由于一个全地址规模跳转指令:LDR 指令,它至少需求 8 位才干完结,榜首行和第二行指令加起来刚好 8 位)。

内存优化:so 库申请的内存优化

当咱们的函数履行完结,就会持续回到 TestMalloc 函数中,从 634 的地址开端履行。这个时分,咱们还需求康复履行 62c 和 630 这两个地址中被掩盖的指令。整个流程如下图。

内存优化:so 库申请的内存优化

经过上面的比如,咱们能够将 inline hook 的完结过程总结下:

  1. 拷贝原函数的头部两条汇编指令,并掩盖成跳转到 Hook 函数的指令;

  2. Hook 函数履行完结,再履行前面备份的已被掩盖的两条指令。不过这儿直接履行或许会出现问题,比方有的指令假如用到了 PC 寄存器 (保存的是当前正在取指的指令的地址),但此刻的 PC 现已改变了,所以咱们还需求进行指令修复,修复的计划是将 PC 相关的指令替换成 PC 无关的指令。

尽管上面的原理看起来比较简略了解,可是实践完结起来仍是比较杂乱的,需求对汇编有较深的根底,由所以直接修正的汇编指令,因而会有许多兼容性问题,不太安稳,最终的指令修复计划也很简略出问题,你假如感爱好,能够参考相关的开源库深化剖析。

讲完了 Native Hook 技能的两种计划,咱们来比照下它们的优缺陷。

计划 优点 缺陷
PLT Hook 安稳 – 只能Hook 外部 so 函数调用– hook过程比较费时
Inline Hook 能够 hook so 内部调用 – 不安稳- 只能hook大于8字节长度的办法

Naitve hook 是一项很成熟的技能,GitHub 上有许多相关的开源库,咱们在这儿把握原理即可,并不需求自己重复造轮子再去完结一遍。这儿推荐几个大厂开源的 Native Hook 库:

PLT Hook

  • bhook:github.com/bytedance/b…
  • xhook:github.com/iqiyi/xHook
  • profilo:github.com/facebookinc…

Inline hook

  • ShadowHook:github.com/bytedance/a…

除了 PLT Hook 和 Inline Hook 两种 Naitve Hook 计划,还有一种虚函数表 Hook 计划,可是这种计划只要在虚函数中才干运用,在《经过 GC 按捺来提升速度》文章中会详细解说这一种计划,就不在这儿打开讲了。

总的来说,我更推荐你在 hook so 库的内存请求这一场景中,运用修正 got 表的 Native Hook 计划,由于这个计划更安稳也更简略。但并不是说 Inline Hook 就不能用,Inline Hook 在许多场景下,往往是首选,比方需求 hook so 内部的函数调用时,可是需求咱们有必定的技能功底,才干掌控的住 Inline Hook。

所以这儿我以 bhook 这个开源的 PLT Hook 库为例,来 hook demo 中 testmalloc.so 库的 Malloc 函数。咱们新建一个 hookmalloc 的 so 库,依照 bhook 供给的 bytehook_hook_all 接口,来完结对一切 so 库中的 malloc 函数的 hook,并在 hook 函数 malloc_hook 中,打印请求的内存大小。

内存优化:so 库申请的内存优化
内存优化:so 库申请的内存优化

运转后,经过 log 日志能够看到,咱们成功检测到了这一笔 88 M ( 92274688 k)的内存请求。

内存优化:so 库申请的内存优化

Demo 中 hook 了malloc 函数,但假如咱们想要统计全面,还需求把其他的一切内存请求相关的函数 hook 住,内存开释的函数也 hook 住。

Native 仓库获取原理

当咱们检测到这笔反常的内存请求后,就需求获取仓库来协助咱们定位问题了。在 Java 代码中,咱们只需求调用 Debug.DumpHeap 办法就能获取当前 Java 函数的仓库,Naitve 中没有直接获取仓库的办法,需求咱们自己去完结,首要的办法有 2 种:

  1. 经过 FP 栈帧寄存器获取 Native 仓库;

  2. 经过栈信息 CFI (Call Frame Information) 获取 Native 仓库。

经过 FP 寄存器获取仓库

在讲怎样经过 FP 寄存器获取仓库之前,先介绍几个 Android 设备上常用的寄存器。

  • PC(ProgramCounter,程序计数器) :保存的是当前正在取指的指令的地址。

  • LR (LinkRegister) :在进行函数调用时,会将函数回来后要履行的下一条指令放入 lr 寄存器中。

  • FP(FramePointer,帧指针):一般指向一个函数的栈帧底部,表示一个函数栈的开端方位。

  • SP(StackPointer,栈顶指针):指向当前栈空间的顶部方位,当函数履行流程进行 push 和 pop 时会一同移动。

在前面的章节中,咱们现已知道栈在虚拟内存中是从高地址向底地址扩展的,所以从下图中能够看到,函数栈中 FP 和 SP 寄存器分别指向栈底和栈顶,经过 FP 寄存器就能够找到存储在栈中的 LR 寄存器的数据,这个数据便是函数回来地址。同时也能够找到保存在函数栈中的上一级函数的 FP 寄存器的数据,这个数据指向了上一级函数的栈底。接下来就能够依照相同的办法找出上一级函数栈中存储的 LR 和 FP 数据,因而也知道了上上一级函数以及它的栈底地址,这样循环起来就构成了一个栈回溯过程。整个流程以 FP 为核心,顺次找出每个函数栈中存储的 LR 和 FP 数据,核算出函数回来地址和上一级函数栈底地址,从而找出每一级函数调用联系。

内存优化:so 库申请的内存优化

这种栈回溯办法的完结简略且十分快速,但它在运转时需求占用一个通用寄存器,也便是 FP 会占用寄存器,而 ARM32 的寄存器又比较紧缺,所以默许状况下编译器会进行优化,也便是封闭 FP 寄存器,这能够优化程序的功能和履行速度。假如想要运用 FP 这种办法来回溯,需求在编译 so 库时,显式地增加 -fno-omit-frame-pointer 标志来告诉编译器不要封闭 FP 寄存器,达到快速回溯的意图

咱们运用的第三方库的 so,许多都封闭了 FP 寄存器,兼容性不高,所以这儿就不持续介绍完结计划了,只需求了解大致原理作为常识储备,等咱们需求这种计划的那时分再深化调研即可。

经过栈信息 CFI 获取仓库

接下来,咱们重点说说怎样经过栈信息 CFI 获取仓库。目前 Android 中 Naitve 的仓库获取,根本都是经过 CFI 来完结的,包含 Native Carash 时输出的仓库,以及 Android 官方的一些 Naitve 调试东西等都是采用的这种计划。那什么是 CFI 呢?

CFI (Call Frame Information)是帧调用信息的缩写,在程序运转时,当 Native 函数履行进入栈指令后,就会将对应指令的信息,如地址等(即 CFI ) 写入 .eh_frame 和 .eh_frame_hdr 段中,这两个段也属于 so 这个 ELF文件中的段组成之一。因而,想要获取 Naitve 的仓库,只需求经过这两个段中的数据就能够了。

Android 体系能够直接运用 libunwind 这个库来获取 Naitve 的仓库信息,libunwind 的底层原理实践上便是经过读取 CFI 来完结的。unwind 的用法也比较简略,代码完结如下:

#include <unwind.h> //引入 unwind 库
struct backtrace_stack
{
    void** current;
    void** end;
};
static _Unwind_Reason_Code _unwind_callback(struct _Unwind_Context* context, void* data)
{
    struct backtrace_stack* state = (struct backtrace_stack*)(data);
    uintptr_t pc = _Unwind_GetIP(context);  // 获取 pc 值,即肯定地址
    if (pc) {
        if (state->current == state->end) {
            return _URC_END_OF_STACK;
        } else {
            *state->current++ = (void*)(pc);
        }
    }
    return _URC_NO_REASON;
}
static size_t _fill_backtraces_buffer(void** buffer, size_t max)
{
    struct backtrace_stack stack = {buffer, buffer + max};
    _Unwind_Backtrace(_unwind_callback, &stack);
    return stack.current - buffer;
}
//Usage 
void* buffer[30];
int count = _fill_backtraces_buffer(buffer, 30);

上面的代码中,_Unwind_Backtrace(_unwind_callback, &stack) 函数便是 libunwind 库供给,用于栈遍历的,回调函数 _unwind_callback 中不断将每个栈的 PC 值写到 current 变量,直到一切的栈遍历完,或许达到 end 才停止。

Native 仓库复原函数详细信息

咱们经过上面的办法获取的 Native 仓库后仅仅 16 进制的地址,依据这些地址是无法查看到有用信息的,所以咱们还需求将地址复原成对应的函数详细信息。

内存优化:so 库申请的内存优化

将 16 进制的地址仓库复原成带有用信息的仓库,需求经过下面 3 个过程 :

  1. 承认 so 名;

  2. 核算的偏移地址;

  3. 依据带符号表(ELF 文件中的一张表,存放了函数,办法,变量等称号符号信息)的 so 文件,复原指针对应的函数名和行数。

承认 so 库

承认 so 库名有多种办法,常见的是经过解析 maps 文件,承认每个 so 文件的基地址和完毕地址,然后比照仓库中 16 进制地址,就能承认是哪个 so 文件了。但这儿,咱们要介绍一种更简略的办法,也便是运用 dladdr() 函数。

int  dladdr ( void * addr , Dl_info * info ) ;
typedef struct {
    const char *dli_fname;   //地址对应的 so 名
    void       *dli_fbase;   //对应so库的基地址
    const char *dli_sname;   //假如so库有符号表,这会显现离地址最近的函数名
    void       *dli_saddr;   //符号表中,离地址最近的函数的地址
} Dl_info;

dladdr 函数的用法也很简略,传入一个地址和 Dl_info 结构体指针,便能在结构体中获取该函数的 so 名及 so库的基地址。

内存优化:so 库申请的内存优化

该函数的 log 日志如下:

内存优化:so 库申请的内存优化

经过日志咱们能够看到,关于 libhookmalloc 带了符号表,那么 dli_sname 和 dli_saddr 字段就能显现出正确的值。关于 libart ,现已移除了符号表,则显现为 null ,地址也为 0 。假如是 Release 包,打包的途中,libhookmalloc 的符号表也会主动被移除。

核算地址

复原函数信息后的仓库 log 日志,现已比只要 16 进制地址的仓库多了许多有用数据了,咱们能够看到日志中第四行便是分配内存反常的当地:

D/MallocHook: # 3 : 0x70329d164c : /data/app/~~HAIFZZBf0QGqnGoZmT_ixA==/com.example.hooktest-6c6SbIWGaQmXST7N_NH0xw==/lib/arm64/libtestmalloc.so(Java_com_example_hooktest_MainActivity_TestMalloc)(0x70329d162c)

咱们还知道了这个反常的 so 名为 libtestmalloc.so,反常函数为Java_com_example_hooktest_MainActivity_TestMalloc。基于此,咱们还能进一步定位到这个函数中哪一行出了问题吗?当然是能够的。

咱们能够运用 addr2line 东西,依据函数偏移地址,获取地址对应的函数名、行号等信息。

addr2line -C -f -e xxx.so 函数偏移地址
-C:将低级其他符号名解码为用户级其他姓名。
-e:指定需求转化地址的可履行文件名
-f:在显现文件名、行号信息的同时显现函数名。

这儿咱们需求知道函数的偏移地址,那什么是函数的偏移地址呢?仓库中的 16 进制是函数的肯定地址,偏移地址是相对 so 库的偏移地址,在前面讲修正 got 表 hook Native 函数也讲过,所以只需求用肯定地址减去 so 库的相对地址就能得到偏移地址。

内存优化:so 库申请的内存优化

弥补了偏移地址,再看 Log 日志,咱们就能承认出问题的函数的偏移地址是 0X64C。

内存优化:so 库申请的内存优化

复原函数名及行数

经过上面的办法,咱们现已知道了函数的偏移地址,接下来就经过 addr2line 东西试验一下吧!Android 的 NDK 中现已供给了这个东西了,坐落 /ndk/xxx/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin 目录中。由于我的电脑是 M1,所以挑选 aarch64 目录,你能够依据自己的电脑挑选对应的目录,如 arm 或许 x86 等。

arm-linux-androideabi-addr2line   -C -f -e libionativehook.so  0x64c

这儿的 so 文件需求挑选带符号表的 so ,咱们能够在编译产品的 native_libs 中去寻觅 Debug 版别的 so。编译产品文件中有一个 stripped_native_libs 的文件,里边的 so 都是去符号表的,记得不要挑选这儿面的 so。

内存优化:so 库申请的内存优化
内存优化:so 库申请的内存优化

运转后的结果如下。能够看到,结果显现了地址对应函数名坐落 15 行,依据这个行数,咱们能够准承认位出问题的当地。

内存优化:so 库申请的内存优化
内存优化:so 库申请的内存优化

关于第三方 sdk,都是现已去符号表的,是无法经过 add2line 查看到对应行数的,如下用去符号表之后的 libtestmalloc.so 查询的结果。

内存优化:so 库申请的内存优化

不过,即便第三方 sdk 的 so 即便没去符号表,咱们在没有源码的状况下也无法修正。因而,咱们只需求知道是否有反常就行了,咱们能够将分配的和现已开释记载下来,看是否有请求大量内存而没开释的状况,关于这些内存运用反常的第三方so,咱们最好的做法是替换成安稳正常的版别。

当然,假如是线上监控,需求考虑到功能,那么只需求抓取到 16 进制的仓库就行了,然后将 16 进制的仓库和 maps 文件上传到服务端,服务端经过 maps 文件的计划,承认对应的 so 名和函数名,假如服务端有带符号表的 so ,还能进一步承认行号。

东西介绍

剖析和治理 so 库中内存反常的全流程仍是挺长的,而且常识点也比较多,这儿主张大家能够自己操作一遍,能够协助咱们更深刻地了解和认知整个流程。前面也说到过,开发一款线上可用、安稳性又高的东西是需求付出许多精力的,假如你没有这样的精力去开发一套 so 库的反常内存检测东西,咱们也有现成的东西,这儿介绍 2 款:

  1. Malloc_Debug

malloc_debug 是谷歌 Google 官方供给的 Native 剖析东西,malloc_debug 的技能原理和上面讲到的流程是共同,可是它是 hook 整个 zygote 进程中的内存请求相关的函数,而且需求在 Root 后的手机上才干运用,运用起来不太灵活,功能也较差,只能作为线下的作业运用。

  1. Memory-leak-detector

MemoryLeakDetector 是字节开源的一款 Native 内存走漏监控东西,具有接入简略、监控规模广、功能优良、 安稳性好的特色,而且经过了众多字节 App 线上的验证。

这两款东西我也主张你都用一用,把流程跑通,有爱好也能够对着两个库的源码阅读,在前面根底原理的加持下,应该也是读得懂。

小结

这一章尽管只解说了 so 库的内存优化这一部分,但涉及 Native 常识仍是挺多的,这儿做个导图进一步总结下:

内存优化:so 库申请的内存优化
你能够重复阅读并进行实操,直到把里边的常识点都吸收。