百度APP iOS端記憶體優化-原理篇

語言: CN / TW / HK

圖片

一、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開原始碼(http://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核心之外的一個記憶體庫,開源地址:http://opensource.apple.com/source/libmalloc/。當我們App程序需要的建立新的物件時,如呼叫[NSObject alloc],或釋放物件呼叫release方法時,請求先會走到libsystem_malloc.dylib的malloc()和free()函式,然後libsystem_malloc會向iOS的系統核心發起記憶體申請或釋放記憶體,具體來說就是呼叫作業系統Mach核心提供的記憶體分配介面去分配記憶體或釋放記憶體,蘋果作業系統Mach核心提供瞭如下記憶體操作的相關介面。

函式名
說明
mach_vm_allocate
allocates "zero fill" memory in the specfied map
mach_vm_deallocate
deallocates the specified range of addresses in the specified address map
mach_vm_protect
sets the protection of the specified range in the specified map
mach_vm_map
maps a memory object to a task’s address space
mach_vm_page_query
query page infomation

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 id callAlloc(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可以看到原始碼

http://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_NOINLINE static 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

void machine_startup(__unused boot_args * args) { machine_conf(); /* * Kick off the kernel bootstrap. */ kernel_bootstrap(); /* NOTREACHED */ }

kernel_bootstrap函式,XNU程式碼路徑: /osfmk/kern/startup.c

void kernel_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 void kernel_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。

``` void vm_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_EVENTS

void vm_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_EVENTS

void vm_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_t memorystatus_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_t is_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原始碼:http://opensource.apple.com/tarballs/objc4/

[2] libsystem_malloc.dylib原始碼:http://opensource.apple.com/source/libmalloc/

[3] XNU原始碼:http://github.com/apple/darwin-xnu

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

[5] Mach核心介紹:

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

[6] Mach系統結構:

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

[7] Mach虛擬記憶體系統:

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

[8] Mach記憶體交換空間:

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

推薦閱讀

百度APP iOS端記憶體優化實踐-記憶體管控方案

百度APP iOS端記憶體優化實踐-大塊記憶體監控方案