写点什么

百度 APP iOS 端内存优化 - 原理篇

作者:百度Geek说
  • 2023-02-09
    上海
  • 本文字数:14278 字

    阅读完需:约 47 分钟

一、Mach 虚拟内存

1.1 Mach 内存简介

iOS 系统架构可分为内核驱动层(Kernel and Device Drivers Layer)、核心操作系统层(Core OS )、核心服务层(Core Services layer)、媒体层(Media layer 可触摸层 &应用层(Cocoa&Application layer),内核驱动层就是我们经常提到的 Darwin,Darwin 是苹果公司于 2000 年发布的一个开源操作系统,是由 XNU 和一些其他的 Darwin 库组成,XNU 是由苹果公司发布的操作系统内核,XNU 包含三部分:Mach、BSD、I/O Kit。



Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,是 XNU 内核,是作为 UNIX 内核的替代,主要解决 UNIX 一切皆文件导致抽象机制不足的问题,为现代操作系统做了进一步的抽象工作。Mach 负责操作系统最基本的工作,包括进程和线程抽象、处理器调度、进程间通信、消息机制、虚拟内存管理、内存保护等。在 iOS 系统架构中,内存管理是由在 Mach 层中进行的,BSD 只是对 Mach 接口进行了 POSIX 封装,方便用户态进程调用。

1.2 Mach 虚拟内存的特点

1.2.1 虚拟段页式内存管理

页是内存管理的基本单位, 在 Intel 和 ARM 中,通常为 4K,常用的查看虚拟内存的命令:hw.pagesize 查看默认页面大小; vm_page_free_count:当前空闲的 RAM 页数;vm_stat(1) - 从系统范围的角度提供有关虚拟内存的统计信息。



在 iOS ARM64 机型中 page size 是 16K,在 JetsamEvent 开头的系统日志里 pageSize 代表当前设备物理内存页的大小。


1.2.2 iOS 系统没有交换空间

手机自带的磁盘空间也很小,属于珍贵资源,同时跟桌面硬件比起来,手机的闪存 I/O 速度太慢,所以 iOS 系统没有交换空间;对于 Mac 系统,参考 Apple 官方文档 About the Virtual Memory System,Mac 上有交换空间有换页行为,也就是当物理内存不够了,就把不活跃的内存页暂存到磁盘上,以此换取更多的内存空间。

1.2.3 内存压缩技术

内存压缩技术是从 OS X Mavericks (10.9) 开始引入的 (iOS 则是 iOS 7.0 开始),可以参考官方文档: OS X Mavericks Core Technology Overview, 在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。简单理解为系统会在内存紧张的时候寻找 inactive memory pages 然后开始压缩,达到释放内存的效果,以 CPU 时间来换取内存空间,NSPurgeableData 是使用该技术典型的数据结构。所以衡量内存指标一定要记录 compressed 内存 ,另外还需要记录被压缩的 page 的信息。

1.2.4 内存报警

经过前面的内存压缩环节后,设备可用内存若仍处于危险状态,iOS 系统需要各个 App 进程配合处理,会向各进程发送内存报警要求配合释放内存,具体来说,Mach 内核系统的 vm_pageout 守护程序会查询进程列表及其驻留页面数,向驻留页面数最高的进程发送 NOTE_VM_PRESSURE ,被选中的进程会响应这个压力通知,实际表现就是 APP 收到系统的 didReceiveMemoryWarning 内存报警,释放部分内存以达到降低手机内存负载的目标。


在收到内存报警时,App 降低内存负载,可以在很大程度上避免出现 OOM,具体源码分析见第三节。

1.2.5 Jetsam 机制

当进程不能通过释放内存缓解内存压力时,Jestam 机制开始介入,这是 iOS 实现的一个低内存清理的处理机制,也称为 MemoryStatus,这个机制有点类似于 Linux 的“Out-of-Memory”杀手,最初的目的就是杀掉消耗太多内存的进程,这个机制只有在 iOS 系统有,在 Mac 系统是没有的。系统在强杀 App 前,会先做优先级判断,那么,这个优先级判断的依据是什么呢?


iOS 系统内核里有一个数组,专门用于维护线程的优先级。这个优先级规定就是:内核线程的优先级是最高的,操作系统的优先级其次,App 的优先级排在最后,并且,前台 App 程序的优先级是高于后台运行 App 的,线程使用优先级时,CPU 占用多的线程的优先级会被降低。

1.3 Mach 内存管理数据结构

Mach 虚拟内存这一层完全以一种机器无关的方式来管理虚拟内存,这一层通过 vm_map、vm_map_entry、vm_objec 和 vm_page 四种关键的数据结构来管理虚拟内存。


第一、vm_map:表示地址空间内的多个虚拟内存区域。每一个区域都由一个独立的条目 vm_map_entry 表示,这些条目通过一个双向链表 vm map links 维护,参考 XNU 开源代码(https://opensource.apple.com/source/xnu/),代码路径:osftnk/vm/vm_map.h,我们可以清楚地看到 vm_map 结构。


第二、vm_map_entry:这个数据结构向上承接了 vm_map,向下指向 vm_object,该数据结构有很多权限访问标志位,任何一个 vm_map entry 都表示了虚拟内存中一块连续的区域,每一个这样的区域都可以通过指定的访问保护权限进行保护,在 XNU 源代码路径(osftnk/vm/vm_map.h)可看到具体数据结构定义。


第三、vm_object:这是一个核心数据结构,将前面介绍的 vm_map_entry 与实际内存相关联,该数据结构主要包含一个 vm_page 的链表、memory object 分页器、标志位(用来表示底层的内存状态如初始化、已创建、已就绪或 pageout 等状态)和一些计数器(引用计数、驻留计数和联动计数等),XNU 源代码路径:osfmk/vm/vm_object.h;


第四、vm_page: 重点包含 offset 偏移量和很多状态位:驻留内存、正在清理、交换出、加密、重写、和脏等,XNU 源代码路径(osftnk/vm/vm_page.h)。

1.4 Mach 内核提供的内存操作接口

XNU 内存管理的核心机制是虚拟内存管理,在 Mach 层中进行的,Mach 控制了分页器,并且提供了各种 vm_ 和 mach_vm_ 消息接口。


Mach 内核是按照 page size 大小来分配的内存的,对于苹果的 arm64 机型来说的 page size 是 16K 大小,但是我们通常在应用程序中在堆上申请内存的时候,单位都是字节,很显然内核提供的函数不适合直接提供给上层使用,这儿存在一个 GAP,在 iOS 系统中 libsystem_malloc.dylib 就是用来弥补 GAP 的。


libsystem_malloc.dylib 是 iOS 内核之外的一个内存库,开源地址:https://opensource.apple.com/source/libmalloc/。当我们 App 进程需要的创建新的对象时,如调用[NSObject alloc],或释放对象调用 release 方法时,请求先会走到 libsystem_malloc.dylib 的 malloc()和 free()函数,然后 libsystem_malloc 会向 iOS 的系统内核发起内存申请或释放内存,具体来说就是调用操作系统 Mach 内核提供的内存分配接口去分配内存或释放内存,苹果操作系统 Mach 内核提供了如下内存操作的相关接口。


<table><tbody><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">函数名</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">说明</span></section></td></tr><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">mach_vm_allocate</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">allocates "zero fill" memory in the specfied map</span></section></td></tr><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">mach_vm_deallocate</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">deallocates the specified range of addresses in the specified address map</span></section></td></tr><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">mach_vm_protect</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">sets the protection of the specified range in the specified map</span></section></td></tr><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">mach_vm_map</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">maps a memory object to a task’s address space</span></section></td></tr><tr><td width="154" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">mach_vm_page_query</span></section></td><td width="396" valign="top" style="word-break: break-all;"><section style="text-align: justify;margin-left: 8px;margin-right: 8px;"><span style="font-size: 14px;">query page infomation</span></section></td></tr></tbody></table>


libsystem_malloc 就是通过 mach_vm_allocate 和 mach_vm_map 来申请 page size 整数倍大小的内存,然后缓存这些内存页,形成一个内存池。当 malloc 调用的时候,可以根据传入的 size 大小来应用不同的分配策略,从这些缓存的内存中,分配一个 size 大小的内存地址返回给上层调用,同时记录这次分配操作的元数据。当调用 free 的时候,可以根据分配的地址,找到元数据,进而回收分配的内存。

二、内存分配函数 alloc 源码分析

为了了解内存分配底层原理,我们从 alloc 函数源码分析说起,下载 objc 开源库,然后从 iOS 做内存分配的函数[NSObject alloc] 开始一起分析。

2.1 objc_rootAlloc 函数

+ (id)alloc {    return _objc_rootAlloc(self);}id  _objc_rootAlloc(Class cls){    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);}
复制代码



调用函数 callAlloc,并传入两个值 checkNil 为 false 以及 allocWithZone 为 true。

2.2 callAlloc 函数

static ALWAYS_INLINE idcallAlloc(Class cls, bool checkNil, bool allocWithZone=false){#if __OBJC2__    if (slowpath(checkNil && !cls)) return nil;    if (fastpath(!cls->ISA()->hasCustomAWZ())) {        return _objc_rootAllocWithZone(cls, nil);    }#endif   /* 省略 */ }
复制代码


首先_OBJC2_宏定义,代表 objc 的版本,现在编译器使用的都是 Objective-C2.0,进入 if 语句,slowpath(告诉编译器,传入的条件结果为假的可能性很大),因为 objc_rootAlloc 传入的 checkNil 为 false,所以不会返回 nil,接着执行 fastpath(告诉编译器,传入的条件结果为真的可能性很大), 这个判断就是去检测传入的这个类是否实现了 allocWithZone 方法, 如果没有实现进入下一个函数。

2.3 objc_rootAllocWithZone 函数

调用_class_createInstanceFromZone


NEVER_INLINE id  _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused){    // allocWithZone under __OBJC2__ ignores the zone parameter    return _class_createInstanceFromZone(cls, 0, nil,                                         OBJECT_CONSTRUCT_CALL_BADALLOC);}
复制代码

2.4 class_createInstanceFromZone 核心函数

static ALWAYS_INLINE id_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,                              int construct_flags = OBJECT_CONSTRUCT_NONE,                              bool cxxConstruct = true,                              size_t *outAllocatedSize = nil){    //断言机制,防止类并发创建    ASSERT(cls->isRealized());    //读取类的标志位,加速类对象的创建    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();    bool hasCxxDtor = cls->hasCxxDtor();    bool fast = cls->canAllocNonpointer();    size_t size;    // 计算内存空间大小    size = cls->instanceSize(extraBytes);    if (outAllocatedSize) *outAllocatedSize = size;    id obj;    if (zone) {        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);    } else {        obj = (id)calloc(1, size);    }     /* 省略 */ }
复制代码


我们可以看到先调用 instanceSize 函数计算出创建对象需要的内存空间大小,然后再调用 malloc_zone_calloc 或者 calloc 去分配内存空间。

2.5 instanceSize 计算内存空间大小

inline size_t instanceSize(size_t extraBytes) const {    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {        return cache.fastInstanceSize(extraBytes);    }    size_t size = alignedInstanceSize() + extraBytes;    if (size < 16) size = 16;    return size;}
复制代码


为了减少计算时间,先判断缓存是否有值,如果有先从缓存取值,否则需要进入计算逻辑,从上面的代码逻辑中我们看到入参 extraBytes 值为 0,返回值就是 alignedInstanceSize,源码如下:


uint32_t alignedInstanceSize() {    return word_align(unalignedInstanceSize());}uint32_t unalignedInstanceSize() const {    ASSERT(isRealized());    return data()->ro()->instanceSize;}
复制代码


我们知道 OC 类结构中,data 字段存储类相关信息,其中 ro 数据结构存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,所以从当前对象所属类的 ro 中获取 instanceSize 代表了分配对象所需内存空间。


#   define WORD_MASK 7UL   static inline uint32_t word_align(uint32_t x) {      return (x + WORD_MASK) & ~WORD_MASK;  }
复制代码


接下来调用 word_align 做内存对齐操作,从上述源码可以发现类对象的创建是按 16 字节对齐,不足 16 字节的返回 16,这是 iOS 在堆上分配 OC 对象基本原则,都是以 16 倍数做分配。

2.6 malloc_zone_calloc 函数

malloc_zone_calloc 或者 calloc 去分配内存空间,这两个函数正是 libmalloc 中重要的内存分配函数,libmalloc 库中路径 src/malloc 可以看到源码


https://opensource.apple.com/source/libmalloc/libmalloc-317.40.8/src/malloc.c.auto.html


void *calloc(size_t num_items, size_t size){  return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);}
MALLOC_NOINLINEstatic void *_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size, malloc_zone_options_t mzo){ MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr; if (malloc_check_start) { internal_check(); }
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) { malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone, (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0); }
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr); if (os_unlikely(ptr == NULL)) { malloc_set_errno_fast(mzo, ENOMEM); } return ptr;}
void *malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size){ return _malloc_zone_calloc(zone, num_items, size, MZ_NONE);}
复制代码

三、内存报警源码分析

3.1 总体流程图

3.2 系统启动初始化

mach 系统启动后先做一系列内核初始化工作,函数调用路径为 arm_init->machine_startup->kernel_bootstrap->kernel_bootstrap_thread,arm_init 函数,XNU 代码路径:/osfmk/arm/arm_init.c


void arm_init( boot_args       *args){ /* 省略 */     machine_startup(args);}
复制代码


machine_routines 函数,XNU 代码路径:/osfmk/arm/machine_routines.c


voidmachine_startup(__unused boot_args * args){  machine_conf();  /*   * Kick off the kernel bootstrap.   */  kernel_bootstrap();  /* NOTREACHED */}
复制代码


kernel_bootstrap 函数,XNU 代码路径: /osfmk/kern/startup.c


voidkernel_bootstrap(void){  /*   *  Create a kernel thread to execute the kernel bootstrap.   */  kernel_bootstrap_log("kernel_thread_create");  result = kernel_thread_create((thread_continue_t)kernel_bootstrap_thread, NULL, MAXPRI_KERNEL, &thread);    /* 省略 */ }
复制代码


kernel_bootstrap_thread 函数,XNU 代码路径:/osfmk/kern/startup.c,其中 vm_pageout 方法进行内存报警初始化,bsd_init 方法进行 Jetsam 机制初始化。


static voidkernel_bootstrap_thread(void){    /* 省略 */    //Jetsam机制初始化  bsd_init();  //内存报警机制  vm_pageout();}
复制代码

3.3 报警线程创建时机

系统启动时在完成内核初始化工作后,会调用 vm_pageout( )方法,创建 vm_pageout 守护程序,在 vm_pageout 函数主要功能是管理页面交换的策略,判断哪些页面需要写回到磁盘,此外的一项功能就是通过 kernel_thread_start_priority 初始化内存报警线程,刚创建的 VM_pressure 线程设置为阻塞状态,等待唤醒,XNU 代码路径:osfmk/vm/vm_pageout.c。


voidvm_pageout(void){   /* 省略 */    result = kernel_thread_start_priority((thread_continue_t)vm_pressure_thread, NULL,      BASEPRI_DEFAULT,      &thread);
if (result != KERN_SUCCESS) { panic("vm_pressure_thread: create failed"); } thread_deallocate(thread); /* 省略 */ }
复制代码

3.4 创建内存报警线程

创建内存报警线程,线程名称为 VM_pressure,XNU 代码路径:osfmk/vm/vm_pageout.c,具体实现如下所示:


#if VM_PRESSURE_EVENTSvoidvm_pressure_thread(void){  static boolean_t thread_initialized = FALSE;
if (thread_initialized == TRUE) { vm_pageout_state.vm_pressure_thread_running = TRUE; consider_vm_pressure_events(); vm_pageout_state.vm_pressure_thread_running = FALSE; }
thread_set_thread_name(current_thread(), "VM_pressure"); thread_initialized = TRUE; assert_wait((event_t) &vm_pressure_thread, THREAD_UNINT); thread_block((thread_continue_t)vm_pressure_thread);}#endif /* VM_PRESSURE_EVENTS */
复制代码

3.5 唤醒报警线程

3.5.1 内存发生变化时调用

在手机的内存发生变化的时候就会调用 memorystatus_pages_update 函数,XNU 代码路径:bsd/kern/kern_memorystatus.c, 其中调用核心函数 vm_pressure_response,这是内存报警机制的核心模块。


#if VM_PRESSURE_EVENTSvoidvm_pressure_thread(void){  static boolean_t thread_initialized = FALSE;
if (thread_initialized == TRUE) { vm_pageout_state.vm_pressure_thread_running = TRUE; consider_vm_pressure_events(); vm_pageout_state.vm_pressure_thread_running = FALSE; }
thread_set_thread_name(current_thread(), "VM_pressure"); thread_initialized = TRUE; assert_wait((event_t) &vm_pressure_thread, THREAD_UNINT); thread_block((thread_continue_t)vm_pressure_thread);}#endif /* VM_PRESSURE_EVENTS */
复制代码

3.5.2 确定新的内存状态值

在 vm_pressure_response 方法中,通过衡量内存指标来确定是否唤起内存报警线程,进而向各 APP 发送 didReceiveMemoryWarning ,这是内存报警源码的核心模块,XNU 代码路径:osfmk/vm/vm_pageout.c。


void  vm_pressure_response(void){      /* 省略 */    old_level = memorystatus_vm_pressure_level;  switch (memorystatus_vm_pressure_level) {  case kVMPressureNormal:  {    if (VM_PRESSURE_WARNING_TO_CRITICAL()) {      new_level = kVMPressureCritical;    } else if (VM_PRESSURE_NORMAL_TO_WARNING()) {      new_level = kVMPressureWarning;    }    break;  }
case kVMPressureWarning: case kVMPressureUrgent: { if (VM_PRESSURE_WARNING_TO_NORMAL()) { new_level = kVMPressureNormal; } else if (VM_PRESSURE_WARNING_TO_CRITICAL()) { new_level = kVMPressureCritical; } break; }
case kVMPressureCritical: { if (VM_PRESSURE_WARNING_TO_NORMAL()) { new_level = kVMPressureNormal; } else if (VM_PRESSURE_CRITICAL_TO_WARNING()) { new_level = kVMPressureWarning; } break; } default: return; } if (new_level != -1) { memorystatus_vm_pressure_level = (vm_pressure_level_t) new_level; if ((memorystatus_vm_pressure_level != kVMPressureNormal) || (old_level != memorystatus_vm_pressure_level)) { if (vm_pageout_state.vm_pressure_thread_running == FALSE) { thread_wakeup(&vm_pressure_thread); } if (old_level != memorystatus_vm_pressure_level) { thread_wakeup(&vm_pageout_state.vm_pressure_changed); } } }}
复制代码


memorystatus_vm_pressure_level 是全局变量,代表上一次内存状态,接下来根据其不同的值,调用如下方法确定新的内存状态值 new_level ,分如下四种情况。


第一、上次处于 kVMPressureNormal 状态,判断函数 VM_PRESSURE_WARNING_TO_CRITICAL()的值,若为 true 新内存值为 kVMPressureCritical,否则判断函数 VM_PRESSURE_NORMAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureWarning,否则为默认值-1;


第二、上次处于 kVMPressureWarning、kVMPressureUrgent 状态,判断函数 VM_PRESSURE_WARNING_TO_NORMAL()的值,若为 true,新内存值为 kVMPressureNormal,否则判断函数 VM_PRESSURE_NORMAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureCritical,否则为默认值-1;


第三、上次处于 kVMPressureCritical 状态,判断函数 VM_PRESSURE_WARNING_TO_NORMAL()的值,若为 true,新内存值为 kVMPressureNormal,否则判断函数 VM_PRESSURE_CRITICAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureWarning,否则为默认值-1;

3.5.3 水位等级详情

在 XNU 代码路径:osfmk/vm/vm_compressor.h,有如下宏定义


#define AVAILABLE_NON_COMPRESSED_MEMORY         (vm_page_active_count + vm_page_inactive_count + vm_page_free_count + vm_page_speculative_count)#define AVAILABLE_MEMORY                        (AVAILABLE_NON_COMPRESSED_MEMORY + VM_PAGE_COMPRESSOR_COUNT)
#define VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_minorcompact_threshold_divisor ? vm_compressor_minorcompact_threshold_divisor : 10))#define VM_PAGE_COMPRESSOR_SWAP_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_majorcompact_threshold_divisor ? vm_compressor_majorcompact_threshold_divisor : 10))
#define VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_unthrottle_threshold_divisor ? vm_compressor_unthrottle_threshold_divisor : 10))#define VM_PAGE_COMPRESSOR_SWAP_RETHROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 11) / (vm_compressor_unthrottle_threshold_divisor ? vm_compressor_unthrottle_threshold_divisor : 11))
#define VM_PAGE_COMPRESSOR_SWAP_HAS_CAUGHTUP_THRESHOLD (((AVAILABLE_MEMORY) * 11) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 11))#define VM_PAGE_COMPRESSOR_SWAP_CATCHUP_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 10))#define VM_PAGE_COMPRESSOR_HARD_THROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 9) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 9))
复制代码


在 XNU 代码路径:osfmk/vm/vm_compressor.c,有如下赋值,对于 iOS 系统,走 !XNU_TARGET_OS_OSX 分支


#if !XNU_TARGET_OS_OSX  vm_compressor_minorcompact_threshold_divisor = 20;  vm_compressor_majorcompact_threshold_divisor = 30;  vm_compressor_unthrottle_threshold_divisor = 40;  vm_compressor_catchup_threshold_divisor = 60;#else /* !XNU_TARGET_OS_OSX */  /* 省略 */
复制代码

3.5.3.1 VM_PRESSURE_WARNING_TO_CRITICAL

VM_PRESSURE_WARNING_TO_CRITICAL() 判断内存状态是否从报警到严重,XNU 代码路径:osfmk/vm/vm_pageout.c ,在 iOS 系统中,VM_CONFIG_COMPRESSOR_IS_ACTIVE 为 YES, 走 else 逻辑。


boolean_t VM_PRESSURE_WARNING_TO_CRITICAL(void){  if (!VM_CONFIG_COMPRESSOR_IS_ACTIVE) {    ****    return FALSE;  } else {    return vm_compressor_low_on_space() || (AVAILABLE_NON_COMPRESSED_MEMORY < ((12 * VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD) / 10)) ? 1 : 0;  }}
复制代码


通过前面的宏定义和赋值带入计算表达式,得出如下结论:非压缩可用内存小于总可用内存的 12/40。

3.5.3.2 VM_PRESSURE_NORMAL_TO_WARNING

VM_PRESSURE_NORMAL_TO_WARNING()判断内存状态是否从正常到报警,代码路径:osfmk/vm/vm_pageout.c


boolean_t  VM_PRESSURE_NORMAL_TO_WARNING(void){  /* 省略 */  return (AVAILABLE_NON_COMPRESSED_MEMORY < VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD) ? 1 : 0;}
复制代码


同理,带入计算表达式,得出如下结论:非压缩可用内存小于总可用内存的 1/2。

3.5.3.3 VM_PRESSURE_WARNING_TO_NORMAL

VM_PRESSURE_WARNING_TO_NORMAL()判断内存状态是否从报警到正常


boolean_t  VM_PRESSURE_WARNING_TO_NORMAL(void){  /* 省略 */  return (AVAILABLE_NON_COMPRESSED_MEMORY > ((12 * VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD) / 10)) ? 1 : 0;}
复制代码


同理,带入计算表达式,得出如下结论: 非压缩可用内存大于总可用内存(压缩+非压缩)的 3/5。

3.5.3.4 VM_PRESSURE_CRITICAL_TO_WARNING

VM_PRESSURE_CRITICAL_TO_WARNING()判断内存状态是否从严重到报警


boolean_t VM_PRESSURE_CRITICAL_TO_WARNING(void){  /* 省略 */  return (AVAILABLE_NON_COMPRESSED_MEMORY > ((14 * VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD) / 10)) ? 1 : 0;}
复制代码


同理,带入计算表达式,得出如下结论:非压缩可用内存大于总可用内存(压缩+非压缩)的 7/20。

3.5.4 判断是否唤起报警线程

如下两个条件满足一个就会唤起 vm_pressure_thread。


第一、新的内存状态值不等于 kVMPressureNormal。


第二、新的内存状态和老的内存状态不一样。

3.6 报警线程操作

3.6.1 memorystatus_update_vm_pressure 实现

从 3.3 节中我们知道内存报警线程唤醒后执行 consider_vm_pressure_events(),XNU 代码路径:/bsd/kern/kern_memorystatus_notify.c


void consider_vm_pressure_events(void){  vm_dispatch_memory_pressure();}static void vm_dispatch_memory_pressure(void){  memorystatus_update_vm_pressure(FALSE);}
复制代码


最终会调用函数 memorystatus_update_vm_pressure,XNU 代码路径:/bsd/kern/kern_memorystatus_notify.c


kern_return_tmemorystatus_update_vm_pressure(boolean_t target_foreground_process){    /* 省略 */  if (level_snapshot != kVMPressureNormal) {    /*         * 是否处于上一个报警周期         * next_warning_notification_sent_at_ts代表下一次发送报警通知的最短时间     */    level_snapshot = memorystatus_vm_pressure_level;    if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) {      if (next_warning_notification_sent_at_ts) {                 /                 * curr_ts表示当前时间,小于下一次发送报警通知的最短时间                 * 延后执行                 */        if (curr_ts < next_warning_notification_sent_at_ts) {          delay(INTER_NOTIFICATION_DELAY * 4 /* 1 sec */);          return KERN_SUCCESS;        }                //下一次发送报警通知的最短时间设置为零        next_warning_notification_sent_at_ts = 0;        memorystatus_klist_reset_all_for_level(kVMPressureWarning);      }    } else if (level_snapshot == kVMPressureCritical) {      /* 省略 */    }  }
while (1) { level_snapshot = memorystatus_vm_pressure_level;
if (prev_level_snapshot > level_snapshot) { /*prev_level_snapshot:表示上一一次的等级 * 上一次等级小于本次等级,启用滑动窗口逻辑 */ if (smoothing_window_started == FALSE) { smoothing_window_started = TRUE; microuptime(&smoothing_window_start_tstamp); } /* 省略 */ }
prev_level_snapshot = level_snapshot; smoothing_window_started = FALSE;
memorystatus_klist_lock(); //从task列表里选取一个task,准备发起内存警告通知 kn_max = vm_pressure_select_optimal_candidate_to_notify(&memorystatus_klist, level_snapshot, target_foreground_process); //没有获取可以发起警告的task if (kn_max == NULL) { memorystatus_klist_unlock();
if (level_snapshot != kVMPressureNormal) { //延后通知 if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) { nanoseconds_to_absolutetime(WARNING_NOTIFICATION_RESTING_PERIOD * NSEC_PER_SEC, &curr_ts);
/* Next warning notification (if nothing changes) won't be sent before...*/ next_warning_notification_sent_at_ts = mach_absolute_time() + curr_ts; } if (level_snapshot == kVMPressureCritical) { nanoseconds_to_absolutetime(CRITICAL_NOTIFICATION_RESTING_PERIOD * NSEC_PER_SEC, &curr_ts);
/* Next critical notification (if nothing changes) won't be sent before...*/ next_critical_notification_sent_at_ts = mach_absolute_time() + curr_ts; } } return KERN_FAILURE; } //获取选中进程信息 target_proc = knote_get_kq(kn_max)->kq_p; target_pid = target_proc->p_pid; task = (struct task *)(target_proc->task); //调用is_knote_registered_modify_task_pressure_bits //通知选中进程内存报警 if (level_snapshot != kVMPressureNormal) { if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) { if (is_knote_registered_modify_task_pressure_bits(kn_max, NOTE_MEMORYSTATUS_PRESSURE_WARN, task, 0, kVMPressureWarning) == TRUE) { found_candidate = TRUE; } } else { if (level_snapshot == kVMPressureCritical) { if (is_knote_registered_modify_task_pressure_bits(kn_max, NOTE_MEMORYSTATUS_PRESSURE_CRITICAL, task, 0, kVMPressureCritical) == TRUE) { found_candidate = TRUE; } } } } else { if (kn_max->kn_sfflags & NOTE_MEMORYSTATUS_PRESSURE_NORMAL) { task_clear_has_been_notified(task, kVMPressureWarning); task_clear_has_been_notified(task, kVMPressureCritical);
found_candidate = TRUE; } }
if (found_candidate == FALSE) { proc_rele(target_proc); memorystatus_klist_unlock(); continue; } /* 省略 */ }
return KERN_SUCCESS;}
复制代码

3.6.2 is_knote_registered_modify_task_pressure_bits 通知线程报警

is_knote_registered_modify_task_pressure_bits 通知线程报警,XNU 代码路径:/bsd/kern/kern_memorystatus_notify.c


static boolean_tis_knote_registered_modify_task_pressure_bits(struct knote *kn_max, int knote_pressure_level, task_t task, vm_pressure_level_t pressure_level_to_clear, vm_pressure_level_t pressure_level_to_set){  if (kn_max->kn_sfflags & knote_pressure_level) {    if (pressure_level_to_clear && task_has_been_notified(task, pressure_level_to_clear) == TRUE) {      task_clear_has_been_notified(task, pressure_level_to_clear);    }    task_mark_has_been_notified(task, pressure_level_to_set);    return TRUE;  }
return FALSE;}
复制代码


task_mark_has_been_notified,XNU 代码路径:/bsd/kern/task_policy.c


void task_mark_has_been_notified(task_t task, int pressurelevel){  if (task == NULL) {    return;  }  if (pressurelevel == kVMPressureWarning) {    task->low_mem_notified_warn = 1;  } else if (pressurelevel == kVMPressureCritical) {    task->low_mem_notified_critical = 1;  }}
复制代码

四、总结

本文介绍了 Mach 虚拟内存的特点、内存管理的数据结构以及 Mach 内核提供的内存操作接口,同时对 OC 内存分配核心函数 alloc 做了源码分析,此外对 iOS 端内存报警机制做了详细的源码分析,关于 Jestam 机制和 libmalloc 源码在后续文章做详细介绍,敬请期待。


——END——


参考资料:


[1] objc 源码:https://opensource.apple.com/tarballs/objc4/


[2] libsystem_malloc.dylib 源码:https://opensource.apple.com/source/libmalloc/


[3] XNU 源码:https://github.com/apple/darwin-xnu


[4] 《深入解析 Mac OS X & iOS 操作系统》


[5] Mach 内核介绍:


https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/Mach/Mach.html


[6] Mach 系统结构:


https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemTechnology/SystemTechnology.html


[7] Mach 虚拟内存系统:


https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemTechnology/SystemTechnology.html


[8] Mach 内存交换空间:


https://images.apple.com/media/us/osx/2013/docs/OSX_Mavericks_Core_Technology_Overview.pdf


推荐阅读


百度APP iOS端内存优化实践-内存管控方案


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

用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度APP iOS端内存优化-原理篇_ios_百度Geek说_InfoQ写作社区