百度APP iOS端内存优化实践-大块内存监控方案

01 布景

‍内存缺少引发的APP崩溃一般称为OOM(Out Of Memory),iOS端无法捕获OOM异常,也得不到任何库房信息,给我们排查和处理问题带来许多困扰。引起OOM的原因归根结底便是由于内存分配不合理引起的,尤其是内存处于风险水位时单次内存分配过大引起Jetsam机制开端生效而杀掉进程,通过我们线上数据监控,百度APP客户端单次内存分配逾越30M的case许多。

针对这种潜在的引起OOM的风险,我们开发了一种大内存分配监控方案,充分运用线上监控优势(丰盛实在的用户场景和用户途径)和线下贱水线优势(可获取更多的库房信息),其间线上环境除了功用完结外,还要关键考虑稳定性,不能引入额定的功用问题,通过技术探究我们处理了此类难题,线上监控和线下贱水线监控相结合完结对百度APP大块内存的监控。

02 技术方案总述

大块内存监控大体分为两个功用模块,缺一不可:

  • 获取内存分配概略。判别单次内存分配是否逾越阈值,若逾越阈值,说明是大内存分配行为;

  • 获取库房信息。丰盛的库房信息可直接协助开发同学定位到产生大内存分配的具体代码,定位分配不合理的case。

毕竟可通过优化内存分配不合理的case,达到下降OOM率的政策。

03 获取内存分配概略

3.1 方案对比

关于获取iOS端每次内存分配信息,有如下处理方案:

  1. 通过 hook 内存分配函数 alloc 方法获取,用 swizzle 方法完结 hook,存在的缺点是监控规划不可全面,只能监控 OC 方针,不能监控 C/C++ 方针。

  2. hook 库libsystem_malloc 内存分配函数 malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc来获取内存信息。这种方案关于 OC 方针和 C/C++ 方针都可监控,但是由于要 hook 系统 C/C++ 方法而不是 OC 方法,现在的技术条件需求运用 fishhook。

百度APP iOS端内存优化实践-大块内存监控方案

百度APP 选用的技术方案如下图所示,首要通过重置 libsystem_malloc 库中的malloc_logger 函数指针获取内存活动概略(分配和开释两种活动),然后通过Type类型过滤出内存分配的活动, 终究获取内存分配大小,该方案可监控 OC 方针和 C/C++ 方针,对iOS框架系统没有侵入性,没有用 fishhook 库所以没有对 mach-o 文件做任何修改,也没有 hook 任何底层分配内存系统方法,从 APP 功用和质量视点来说是最好的挑选。

百度APP iOS端内存优化实践-大块内存监控方案

3.2 libsystem_malloc源码分析

libsystem_malloc.dylib 是iOS系统虚拟内存处理的中心库之一,任何涉及到OC、C/C++ 方针的内存分配都会调用该库的API,由它去调用操作系统Mach内核供应的接口去分配或开释内存。具体来说 libsystem_malloc 供应了 malloc_zone_malloc,malloc_zone_calloc,malloc_zone_valloc,malloc_zone_realloc,malloc_zone_free 五个API来完结内存分配和开释,在 iOS 系统中全部涉及到的内存活动都会调用如上接口,当我们App进程需求创建新的方针时,如调用 [NSObject alloc],或开释方针调用 release 方法时(编译器会添加),恳求先会走到 libsystem_malloc.dylib 的上述函数。

Apple 已开源此库,从如下地址可以下载到源码:opensource.apple.com/source/libm…

void *malloc_zone_malloc(malloc_zone_t *zone, size_t size){  MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);void *ptr;if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {    internal_check();  }if (size > MALLOC_ABSOLUTE_MAX_SIZE) {return NULL;  }  ptr = zone->malloc(zone, size);    // if lite zone is passed in then we still call the lite methodsif (malloc_logger) {    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);  }  MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);return ptr;}

3.3要害函数malloc_logger

从源码中我们发现malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc、malloc_zone_free五个API,在每次调用mach内核函数进行内存分配和开释后都有如下函数调用:

if(malloc_logger){
  malloc_logger(MALLOC_LOG_TYPE_ALLOCATE|MALLOC_LOG_TYPE_HAS_ZONE,(uintptr_t)zone,(uintptr_t)size,0,(uintptr_t)ptr,0);
}

先判别malloc_logger函数指针是否为空,假如不为空会调用上述函数,将内存活动的具体信息通过该函数传递进来,从源码分析的视点来看这是iOS系统供应的一个日志函数,具体函数定义如下所示:

typedef void(malloc_logger_t)(uint32_t type,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t result,
uint32_t num_hot_frames_to_skip);
extern malloc_logger_t *malloc_logger;

依据源码我们对malloc_logger函数的入参做如下分析:

参数称谓 参数意义
type 类别,不同函数入参都不同
arg1 分配内存后zone地址
arg2 malloc_zone_realloc时为零,其他情况代表分配内存大小
arg3 malloc_zone_realloc时代表内存分配大小,其他情况为0
result 新内存开端地址ptr
num_hot_frames_to_skip 值都为0

关于榜首个type字段,不同函数都不同,具体值后面具体解释,此外,我们发现malloc_logger生命为extern类型,在C++中声明extern要害字的全局变量和函数可以使得它们可以跨文件被访问。

3.4通过重置malloc_logger函数指针获取内存活动概略

在前面章节我们说过malloc_logger为extern全局变量,所以通过以下进程可以重置该变量获取内存活动概略;

1. 引入libmalloc头文件malloc/malloc.h

2. 定义函数bba_malloc_stack_logger,参数定义与源码定义完全共同,这样做的目的是避免实参传递的时候出现类型和参数个数不共同的问题。

3. 先保存malloc_logger函数指针的值到一个暂时变量origin_malloc_logger,目的是保存系统原始调用方法,交换函数指针后还要调用此方法。

#import<malloc/malloc.h>
typedefvoid(malloc_logger_t)(uint32_ttype,uintptr_targ1,uintptr_targ2,uintptr_targ3,uintptr_tresult,uint32_tnum_hot_frames_to_skip);
//定义函数bba_malloc_stack_logger
voidbba_malloc_stack_logger(uint32_ttype,uintptr_targ1,uintptr_targ2,uintptr_targ3,uintptr_tresult,uint32_tbacktrace_to_skip);
//保存malloc_logger到暂时变量origin_malloc_logger
orgin_malloc_logger=malloc_logger;

4. 将malloc_logger赋值为自定义函数bba_malloc_stack_logger,在定义函数中先调用原始的系统方法origin_malloc_logger,该方法的调用保证了本方案对系统没有侵入性,接下来做大块内存检测。

//malloc_logger赋值为自定义函数bba_malloc_stack_logger
malloc_logger=(malloc_logger_t*)bba_malloc_stack_logger;
//bba_malloc_stack_logger具体完结
voidbba_malloc_stack_logger(uint32_ttype,uintptr_targ1,uintptr_targ2,uintptr_targ3,uintptr_tresult,uint32_tbacktrace_to_skip)
{
if (orgin_malloc_logger != NULL) {
        orgin_malloc_logger(type, arg1, arg2, arg3, result, backtrace_to_skip);
    }
//大块内存监控
    ......
}

通过上面四个进程,malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc每次内存分配结束后,调用如下函数,由于malloc_logger现在不为空,具体值为bba_malloc_stack_logger,所以在bba_malloc_stack_logger中可以获取内存分配活动概略。

if (malloc_logger) {    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);}

3.5 通过type类型过滤出内存分配概略

通过上面章节我们知道malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc、malloc_zone_free五个API都会调用bba_malloc_stack_logger,其间的API完结又各有不同,malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc代表内存分配,malloc_zone_realloc代表内存先开释再分配,malloc_zone_free代表内存开释,不同的API调用是通过入参type来区别的,所以本技术方案通过type反解析来获取内存分配,过滤掉内存开释。

API 意义 type
malloc_zone_malloc 分配内存给一个方针 MALLOC_LOG_TYPE_ALLOCATE |MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_calloc 分配内存给多个方针 MALLOC_LOG_TYPE_ALLOCATE |MALLOC_LOG_TYPE_HAS_ZONE |MALLOC_LOG_TYPE_CLEARED
malloc_zone_valloc 分配内存给一个方针 MALLOC_LOG_TYPE_ALLOCATE |MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_realloc 重新分配内存 MALLOC_LOG_TYPE_ALLOCATE |MALLOC_LOG_TYPE_DEALLOCATE |MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_free 开释内存 MALLOC_LOG_TYPE_DEALLOCATE |MALLOC_LOG_TYPE_HAS_ZONE

3.6 获取单次内存分配大小并判别是否逾越阈值

依据源码我们知道malloc_logger函数的入参arg2,arg3代表内存分配大小,不同type代表意义不同,具体见下面表格分析。

参数称谓 说明
arg2 type值malloc_zone_realloc时为零,type值为malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc代表分配内存大小
arg3 type值malloc_zone_realloc时代表内存分配大小,type值为malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc为0

接下来判别是否逾越我们设定的阈值的大小,在iOS端依据经历单次内次分配8M便是普遍认为的大块内存,当然这个值由服务端下发可灵敏修改,客户端写个默认值即可,但是这个值不建议很小,太小会屡次触发大块内存监控逻辑影响我们手机app的功用,逾越阈值大小就进入下面的环节获取库房信息。

04 获取库房信息

4.1 百度App选用的技术方案

调用系统方法backtrace_symbols可直接获取库房信息,但是存在两个问题,榜首、方法具有线程特点,必须要在获取库房信息的当时线程调用;第二、耗时严峻,实测在中高端机(iPhone8以上)有30ms耗时,在低端机(iPhone8以下)有100ms的耗时。假如大块内存是在主线程分配的,上述耗时会引起主线程卡顿问题,故此方案无法针在线上出产环境运用。

针对这个问题,百度App选用的方案如下所示,结合客户端和服务端双端优势,首要运用dyld库生成了APP全部库的开端地址和结束地址,将获取库房函数地址信息概略进程获取完全放在独立子线程中完结,在客户端拼接成crash日志格局,充分运用服务端做库房概略反解析操作,客户端只需求在专属子线程实行耗时较少的库房函数地址比较操作即可,这样做不会影响所属线程任何操作,更不会引入功用问题,完全克服了系统方法的缺少,具体操作流程如下图所示:

百度APP iOS端内存优化实践-大块内存监控方案

4.2 生成全部库的地址规划(dyld)

生成APP中mach-o文件的全部库的信息,该信息包括库称谓、库开端地址和结束地址,该操作首要运用dyld库函数在子线程完结,不会占用主线程和内存分配所属线程任何资源。dyld库供应了丰盛的api可以获取上述数据,具体来说,_dyld_image_count可获取全部模块数目,dyld_get_image_name 可获取模块称谓,dyld_get_image_header可获取每个模块的开端地址,_dyld_get_image_vmaddr_slide获取单个模块的随机基址。

4.3 backtrace获取库房地址

当检测到大块内存时,在分配内存所属线程调用backtrace方法获取库房函数地址,代码示例:

//返回值depth表明实际上获取的库房的深度,stacks用来存储库房地址信息,20表明指定库房深度。
size_tdepth=backtrace((void**)stacks,20);

那么backtrace耗时毕竟怎样?通过实践数据证明,库房深度设置为20,实测高端机耗时在3ms以内,对功用影响基本可以疏忽,此外,不是每次内存分配都需求调用backtrace获取库房,只有单次内存分配大小符合大块内存标准才会去获取库房。

因此我们在线上出产环境库房深度设置为20并且只对高端机敞开,深度值太小获取的库房信息有限,太大会明显添加backtrace方法耗时,线上数据证明库房深度设置为20既能满意功用要求又能解分出合理的库房信息,在线下贱水线场景下库房深度为40以获取更丰盛的库房信息,两个场景各自发挥自己的优势并彼此补偿。

4.4获取每个地址具体信息

通过4.3进程,我们获取了库房地址,但是这个还不可,为了便利服务端直接可以解分出库房信息,我们在客户端需求将库房地址拼装成下图所示库房格局(类似Crash库房),libsystem_kernel.dylib 0x1b8a9dcf8 0x1b8a73000 + 175352 ,榜首项是库称谓,第二项是库房函数地址(十六进制),第三项是动态库的开端地址(十六进制),第四项是十进制偏移量,其间第二项库房函数地址是通过4.3进程的backtrace获取的,下面的关键是获取每个地址对应的动态库称谓和相关于动态库开端地址的偏移量。

关于上述具体信息的获取,本技术方案将该操作完全放在子线程去完结,由于我们已经在4.2构建好了每个库的开端地址和结束地址,只需求遍历一遍全量库,判别地址是否大于该库开端地址并小于该库的结束地址,那说明该地址便是归于这个库,从而得到该地址的具体信息,库房地址和动态库其实地址做差值可获取偏移量信息。

4.5 atos和dsym解析库房

通过前面的进程生成了类似crash日志格局的库房,上报服务端后,终究在服务端通过atos命令和dsym文件就可以反解复原出对应的库房内容,如:

BaiduBoxApp  0x000000010ff0ceb4 +[BBAJSONSerialization dataFromJSONObject:error:] + 256

通过这种方法可以把耗时较高的符号复原工作放到服务器端,客户端只需求实行耗时较少的库房函数地址比较操作即可,放在子线程队伍实行,不会影响所属线程任何操作,更不会引入功用问题。

05 总结

本文首要介绍百度APP大块内存监控方案,现在在出产环境和线下贱水线环境均已安置,通过该方案完结了如下三个政策:

  1. 下降OOM率:假如内存分配不合理,优化后对下降OOM率有协助;

  2. 数据了解,让我们清晰知道百度APP哪些场景有大块内存分配;

  3. 起到防备的效果,由于我们有清晰的监控机制,敦促每个开发同学创建内存方针时选用适量原则避免无节制分配。

06 参考链接

[1] libsystem_malloc.dylib源码

opensource.apple.com/source/libm…

[2] Mach-O文档介绍

developer.apple.com/library/arc…

[3] Mach-O源码

opensource.apple.com/source/xnu/…

[4]fishhook

github.com/facebook/fi…

——————END——————

引荐阅读:

百家号基于AE的视频烘托技术探究

百度工程师教你玩转规划形式(观察者形式)

Linux透明大页机制在云上大规划集群实践介绍

超高效!Swagger-Yapi的隐秘

百度直播iOS SDK途径化输出改造