從0構建APM系統-Crash&OOM監控

前言

應用程序發生 Crash 現象會給用戶帶來極差的使用體驗,本文將從 iOS 系統的底層出發,梳理核心知識點,講解各類 Crash 的收集以及 OOM(out of memory) 的監控與分析,以及在 APM 系統中所呈現出來的效果


一、異常處理

1.1 OSX/iOS 系統架構

蘋果給出的文檔中,所展示的抽象的系統架構分層圖中, OSX 跟 iOS 的系統架構分層是相同,分為4個層次:

  • 用戶體驗層:包括Aqua、Dashboard/Spotlight 等;
  • 應用框架層:Cocoa、Carbon、Java
  • 核心框架:有時候也稱之為圖形和媒體層。包括核心框架、OpenGL
  • Darwin:包括內核和 UNIX shell 環境

4個層次中,Darwin 是完全開源的,是整個系統的基礎,提供了底層API。

圖1 OSX 和 iOS 系統架構圖


OSX 跟 iOS 的系統框架在抽象上都是可以用圖1表示,但實際上他們之間還是有一些細節上的差異,這裡不過多介紹。這裡比較重要的是 Darwin 框架

圖2 Darwin 框架圖 來源《深入解析 Mac OS X & iOS 操作系統》

Darwin 的內核是 XUN,它也是 OS X 本身的核心。從圖2可知,XUN以下幾種組件構成:

  • Mach
  • BSD
  • LibKern
  • I/O Kit

這其中最為重要的是 Mach 跟 BSD。


1.2 Mach層

Mach是一個微內核,這個微內核僅能處理操作系統最基本的職責:

  • 進程和線程抽象
  • 虛擬內存管理
  • 任何調度
  • 進程間通信和消息傳遞機制

Mach 本身的 API 非常有限,但是這些 API 非常基礎,如果沒有這些 API,其他工作無法實施,而 Mach 的異常處理也是基於以上的4項能力進行設計的。

在Mach中,異常是通過內核中的消息傳遞,異常由出錯的任務或線程通過 msg_send() 拋出,由一個處理程序通過 msg_recv() 捕捉。處理程序可以處理異常,也可以清除異常(將異常標記為已完成並繼續),還可以終止線程。

Mach的異常處理程序在不同的上下文運行,出錯的線程向預先指定好的異常埠發送消息,然後等待應答。每一個任務都可以註冊一個異常埠,這個異常埠會對同一任務中的所有線程起效。此外,單個線程還可以通過 thread_set_exception_ports 註冊自己的異常埠。通常情況下,任務和線程的異常埠都是 NULL,也就是說異常不會被處理。而一旦創建異常埠,這些埠就像系統中的其他埠一樣,可以轉交給其他任務甚至其他主機。

在發生異常時,會按照以下步驟執行:

  1. 嘗試將異常拋給線程中的異常埠
  2. 嘗試拋給任務的異常埠
  3. 嘗試拋給主機的異常埠(即主機註冊的默認埠)。

如果沒有一個埠返回 KERN_SUCCESS,那麼整個任務被終止,根據前文的敘述,Mach 不提供異常處理邏輯——只是提供異常通知的框架。


1.3 BSD層

BSD 層建立在Mach之上,這一層是一個很可靠且更現代的 API,提供了 POSIX 兼容性,並提供了更高層次的抽象,包括但不限於:

  • UNIX 進程模型
  • POSiX 線程模型(pthread)以及相關的同步原語
  • UNIX 用戶和組
  • 網路協議棧
  • 文件系統訪問
  • 設備訪問

在處理異常上,Mach 已經通過異常機制提供了底層的陷阱處理,而 BSD 則在異常機制之上,構建了一個信號處理機制。硬體產生的信號被 Mach 層捕捉,然後轉換為對應的 UNIX 信號。為了維護一個統一的機制,操作系統和用戶產生的信號首先被轉換為 Mach 異常,然後再轉換為信號。

BSD 進程被 bsdinit_task() 函數啟動時,還調用了 ux_handle_init() 函數,這個函數設置了一個名為ux_handle 的 Mach 內核線程。只有在 ux_handle_init() 函數返回之後,bsdinit_task() 才能夠註冊使用ux_Exception_port。bsdinit_task() 將所有的Mach異常消息都重定向到ux_exception_port,這個埠由 ux_handle 線程持有。遵循 Mach 異常消息傳遞的方式,PID 為1的進程異常處理會在進程之外由 ux_handle() 線程處理。由於所有後創建的用戶態進程都是 PID1 的後代,所以這些進程會自動繼承這個異常埠,相當於 ux_handle() 線程要負責處理系統上 UNIX 進程產生的每一個Mach異常。ux_handle() 函數非常簡單,這個函數在進入時會首先設置好 ux_handle_port,然後進入一個無限循環的 Mach 消息循環。消息循環接受 Mach 異常消息,然後調用mach_exc_server() 處理異常,整個流程圖如下所示:

圖3 Mach 異常處理以及轉換為 UNIX 信號的流程

二、Crash收集方式

要了解 Crash,我們應該先清楚幾個基本概念以及它們之間的關係:

  • 軟體異常:主要來源於兩個API的調用kill()、pthread_kill(),而 iOS 中常遇到的 NSException 未捕獲、abort()函數調用都屬於這種情況。
  • 硬體異常:此類異常始於處理器陷阱,如訪問野指針崩潰。
  • Mach異常:Mach 異常處理流程的簡稱
  • UNIX信號:SIGBUSSIGSEGVSIGABRTSIGKILL 等。

......

Exception Type: EXC_CRASH (SIGABRT)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note: EXC_CORPSE_NOTIFY

Triggered by Thread: 0

Last Exception Backtrace:

0 CoreFoundation 0x1843765ac __exceptionPreprocess + 220 (NSException.m:199)

1 libobjc.A.dylib 0x1983f042c objc_exception_throw + 60 (objc-exception.mm:565)

......

這是一個 App 的崩潰日誌,從日誌中的 Exception Type: EXC_CRASH (SIGABRT) 可以知道這是 Mach 層發生了EXC_CRASH異常,被轉換 SIGABRT 信號。那麼你可能有一個疑問?既然 Mach 層可以捕獲異常,註冊 UNIX 信號也能捕獲異常,那麼這兩種方法系統是如何選擇?而且從圖3中可以看出 Mach 異常最終都會轉換成 UNIX 信號,那麼是不是只需要攔截 UNIX 信號就可以了?

其實不是的,這裡面有兩個原因:

  1. 因為並不是所有的 Mach 異常類型都有相對應的 UNIX 信號進行映射
  2. UNIX 信號在崩潰線程回調,如果遇到棧溢出,那麼就沒有棧空間可以執行回調代碼了。

那麼是否只需要攔截 Mach 異常?答案一樣是否定的,因為用戶態的軟體異常是直接走信號流程的,如果不攔截,會導致這部分 Crash 丟失。

因此,在 Crash 的收集上,監控系統應該具備多種異常處理能力的,市面上有許多諸如此類的工具,其中一款有 KSCrash,該工具也是目前最熱門,最完善的 Crash 收集工具,大部分源碼基於C語言編寫,微信的開源項目 Matrix 也是基於 KSCrash 開發,而我們的 APM 系統中的 iOS 崩潰監控也是基於該工具上進行編寫的。

2.1 Mach 層異常處理

讀源碼是了解工具最快的方式,讓我們看一下 KSCrash 是如何處理Mach層異常的核心源碼(KSCrashMonitor_MachException.c)如下:

static bool installExceptionHandler()

{

......

//獲取當前task

const task_t thisTask = mach_task_self();

exception_mask_t mask = EXC_MASK_BAD_ACCESS |

EXC_MASK_BAD_INstructION |

EXC_MASK_ARITHMETIC |

EXC_MASK_SOFTWARE |

EXC_MASK_BREAKPOINT;

//獲取當前task所有的異常埠並保存在kr屬性中

kr = task_get_exception_ports(thisTask,

mask,

g_previousExceptionPorts.masks,

&g_previousExceptionPorts.count,

g_previousExceptionPorts.ports,

g_previousExceptionPorts.behaviors,

g_previousExceptionPorts.flavors);

if(kr != KERN_SUCCESS)

{

KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));

goto failed;

}

if(g_exceptionPort == MACH_PORT_NULL)

{

KSLOG_DEBUG("Allocating new port with receive rights.");

//申請一個異常埠

kr = mach_port_allocate(thisTask,

MACH_PORT_RIGHT_RECEIVE,

&g_exceptionPort);

if(kr != KERN_SUCCESS)

{

KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));

goto failed;

}

KSLOG_DEBUG("Adding send rights to port.");

//設置埠許可權

kr = mach_port_insert_right(thisTask,

g_exceptionPort,

g_exceptionPort,

MACH_MSG_TYPE_MAKE_SEND);

if(kr != KERN_SUCCESS)

{

KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));

goto failed;

}

}

......

//創建對應的線程,捕獲異常

error = pthread_create(&g_secondaryPThread,

&attr,

&handleExceptions,

kThreadSecondary);

......

根據源碼可以總結出以下的流程圖:

在1.2中,提到過 Mach 層對異常的處理流程,因此在 Mach 層的異常捕獲也是根據這一處理流程進行的,思路是先申請一個異常處理埠,為該埠申請許可權,再設置異常埠,新建一個內核線程,在線程中循環等待異常,但發生異常時,會掛起線程並組裝發生 Crash 時的信息進 JSON 文件。但為了防止自己註冊的異常埠搶佔其他 SDK、或者開發者設置的邏輯,需要先保存其他異常埠,等到收集邏輯結束後將異常處理交給其他埠內的邏輯處理。


2.2 信號異常處理

信號異常的捕獲是在 KSCrashMonitor_signal.c 中的 installSignalHandler 函數中,具體的方案為首先先使用 sigaltstack 函數在堆上分配一塊內存,設置信號棧區域,目的是替換信號處理函數的棧,因為一個進程可能有n個線程,每個線程都有自己的任務,假如某個線程執行出錯,會導致整個進程奔潰,所以為了信號處理異常函數正常運行,需要設置單獨的運行空間。

其次設置信號處理函數 sigaction,然後遍歷需要處理的信號數組,將每個信號的處理函數綁定到 sigaction,另外用 g_previousSignalHandlers 保存當前信號的處理函數,在信號處理時,保存線程的上下文信息。

最後等到 KSCrash 信號處理後還原之前的信號處理許可權。

核心代碼如下:

static bool installSignalHandler()

{

KSLOG_DEBUG("Installing signal handler.");

#if KSCRASH_HAS_SIGNAL_STACK

// 在堆上分配一塊內存,

if(g_signalStack.ss_size == 0)

{

KSLOG_DEBUG("Allocating signal stack area.");

g_signalStack.ss_size = SIGSTKSZ;

g_signalStack.ss_sp = malloc(g_signalStack.ss_size);

}

KSLOG_DEBUG("Setting signal stack area.");

// 信號處理函數的棧挪到堆中,不和進程共用一塊棧區

if(sigaltstack(&g_signalStack, NULL) != 0)

{

KSLOG_ERROR("signalstack: %s", strerror(errno));

goto failed;

}

#endif

const int* fatalSignals = kssignal_fatalSignals();

int fatalSignalsCount = kssignal_numFatalSignals();

if(g_previousSignalHandlers == NULL)

{

KSLOG_DEBUG("Allocating memory to store previous signal handlers.");

g_previousSignalHandlers = malloc(sizeof(*g_previousSignalHandlers)

* (unsigned)fatalSignalsCount);

}

// 設置信號處理函數 sigaction 的第二個參數,類型為 sigaction 結構體

struct sigaction action = {{0}};

action.sa_flags = SA_SIGINFO | SA_ONSTACK;

#if KSCRASH_HOST_APPLE && defined(__LP64__)

action.sa_flags |= SA_64REGSET;

#endif

sigemptyset(&action.sa_mask);

action.sa_sigaction = &handleSignal;

for(int i = 0; i < fatalSignalsCount; i++)

{

KSLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);

// 將每個信號的處理函數綁定到上面聲明的 action 去,另外用 g_previousSignalHandlers 保存當前信號的處理函數

if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) != 0)

{

char sigNameBuff[30];

const char* sigName = kssignal_signalName(fatalSignals[i]);

if(sigName == NULL)

{

snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);

sigName = sigNameBuff;

}

KSLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));

// Try to reverse the damage

for(i--;i >= 0; i--)

{

sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);

}

goto failed;

}

}

KSLOG_DEBUG("Signal handlers installed.");

return true;

failed:

KSLOG_DEBUG("Failed to install signal handlers.");

return false;

}

......

2.3 C++異常處理

c++異常處理依靠了標準庫的 std::set_terminate(CPPExceptionTerminate) 函數。

在iOS中,如果C跟C++異常如果能被轉換成NSException,則會走Objective-C異常處理,如果不能,則是default_terminate_handler。這個C++異常默認default_terminate_handler函數調用了abort_message函數,系統產生一個SIGABRT信號。


static void CPPExceptionTerminate(void)

{

......

// 之前判斷是否是cpp exception的條件,繼承自NSException的NSException會被當做cpp exception處理

// if (name == NULL || strcmp(name, "NSException") != 0

if (g_capturedStackCursor && (name == NULL || strcmp(name, "NSException") != 0))

{

kscm_notifyFatalExceptionCaptured(false);

KSCrash_MonitorContext* crashContext = &g_monitorContext;

memset(crashContext, 0, sizeof(*crashContext));

char descriptionBuff[DESCRIPTION_BUFFER_LENGTH];

const char* description = descriptionBuff;

descriptionBuff[0] = 0;

KSLOG_DEBUG("Discovering what kind of exception was thrown.");

g_captureNextStackTrace = false;

try

{

throw;

}

catch(std::exception& exc)

{

strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff));

}

......

2.4 Objective-C 異常處理

對於 OC 層面的 NSException 異常處理較為容易,可以通過註冊 NSUncaughtExceptionHandler 來捕獲異常信息,通過 NSException 參數來做 Crash 信息的收集,交給數據上報組件。如

KSCrash.sharedInstance.uncaughtExceptionHandler = &handleException;


三、OOM 相關概念

OOM 就是 out of memory 的簡稱,指的是在 iOS 設備上當前應用因為內存佔用過高而被操作系統強制終止,在用戶側的感知就是 App 一瞬間的閃退,與普通的 Crash 沒有明顯差異。但是當我們在調試階段遇到這種崩潰的時候,在設備中分析與改進是找不到普通的崩潰日誌。可以找到以Jetsam開頭的日誌,這種日誌就是 OOM 崩潰之後系統生成的一種專門反映內存異常問題的日誌。

按照程序的運行狀態一般把OOM分為以下兩種類型:

  • Foreground Out Of Memory

應用正在前台運行的狀態而出現OOM崩潰

  • Background Out Of Memory

應用程序在後台發生的 OOM 崩潰


Jetsam

Jetsam 是 iOS 操作系統為了控制內存資源過度使用而採用的一種資源管理機制。不同於 MacOSLinux,Windows等桌面操作系統,出於性能方面的考慮,iOS 系統並沒有設計內存交換空間的機制,所以在 iOS 中,如果設備整體內存緊張的話,系統只能將一些優先順序不高或佔用內存過大的進程直接終止掉。

下面截取的部分日誌信息:

{

"uuid" : "a02fb850-9725-4051-817a-8a5dc0950872",

"states" : [

"frontmost" //應用狀態:前台運行

],

"lifetimeMax" : 92802,

"purgeable" : 0,

"coalition" : 68,

"rpages" : 92802, //佔用內存頁

"reason" : "per-process-limit", //崩潰原因:超過單進程上限

"name" : "MyCoolApp"

}

詳細說明可以參考官方文檔

Jetsam機制清理策略分為兩種情況:

  1. 單個 App 進程超過內存上線
  2. 設備的物理內存佔用受到壓力時會按照優先順序完成清理:
    - 後台應用 > 前台應用
    - 內存佔用高的應用 > 內存佔用低的應用
    - 用戶應用 > 系統應用

功能介紹&原理

OOM預警

OOM預警功能主要是在內存到達預定的閥值時,上報APM平台內存狀態相關信息。流程圖如下:

基於系統內核提供一個表示內存信息的結構體

通過 task_info 方法可以獲得內存的相關使用情況

kern_return_t task_info

(

task_name_t target_task,

task_flavor_t flavor,

task_info_t task_info_out,

mach_msg_type_number_t *task_info_outCnt

);

監控內存大小代碼如下:

int64_t memoryUsageInByte = 0;

task_vm_info_data_t vmInfo;

mach_msg_type_number_t count = TASK_VM_INFO_COUNT;

kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);

if(kernelReturn == KERN_SUCCESS) {

memoryUsageInByte = (int64_t) vmInfo.phys_footprint;

}

觸頂處理邏輯如下:

即當超過設定的閥值時,就上報當時的內存信息!

-(void)saveLastSingleLoginMaxMemory{

if(_hasUpoad){

NSString* currentMemory = [NSString stringWithFormat:@"%f", _singleLoginMaxMemory];

NSString* overflowMemoryLimit =[NSString stringWithFormat:@"%f", overflow_limit];

if(_singleLoginMaxMemory > overflow_limit){

static BOOL isFirst = YES;

if(isFirst){

_firstOOMTime = [[NSDate date] timeIntervalSince1970];

isFirst = NO;

}

}

NSDictionary *minidumpdata = [NSDictionary dictionaryWithObjectsAndKeys:currentMemory,@"singleMemory",overflowMemoryLimit,@"threshold",[NSString stringWithFormat: @"%.2lf", _firstOOMTime],@"LaunchTime",nil];

NSString *fileDir = [self singleLoginMaxMemoryDir];

if (![[NSFileManager defaultManager] fileExistsAtPath:fileDir])

{

[[NSFileManager defaultManager] createDirectoryAtPath:fileDir withIntermediateDirectories:YES attributes:nil error:nil];

}

NSString *filePath = [fileDir stringByAppendingString:@"/apmLastMaxMemory.plist"];

if(minidumpdata != nil){

if([[NSFileManager defaultManager] fileExistsAtPath:filePath]){

[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];

}

[minidumpdata writeToFile:filePath atomically:YES];

}

}

}

模擬內存觸頂得到日誌記錄:



OOM監控

OOM監控是在App由於OOM導致的崩潰時,及時記錄當時的堆棧信息,上報到APM平台進行後續的問題分析。

Jetsam機制終止進程的時候是通過發送SIKILL異常信號,但它是不可以被當前進程捕獲,用監聽異常信號常規的Crash捕獲方案是不行的。那如何監控呢?2015年facebook提出一個思路,利用排除法。


每次App 啟動的時候判斷上一次啟動進程終止的原因,已知的有:

  • App 更新了版本
  • App 發生了崩潰
  • 用戶手動退出
  • 操作系統更新了版本
  • App 切換後台之後進程終止

如果上一次的啟動進程終止不是以上的原因,就判定為上次啟動發生了 OOM 崩潰。

核心代碼邏輯如下:

-(NSDictionary *)parseFoomData:(NSDictionary *)foomDict

{

......

if(appState == APPENTERFORGROUND){

BOOL isExit = [[foomDict objectForKey:@"isExit"] boolValue];

BOOL isDeadLock = [[foomDict objectForKey:@"isDeadLock"] boolValue];

NSString *lastSysVersion = [foomDict objectForKey:@"systemVersion"];

NSString *lastAppVersion = [foomDict objectForKey:@"appVersion"];

if(!isCrashed && !isExit && [_systemVersion isEqualToString:lastSysVersion] && [_appVersion isEqualToString:lastAppVersion]){

if(isDeadLock){

OOM_Log("The app ocurred deadlock lastTime,detail info:%s",[[foomDict description] UTF8String]);

[result setObject:@deadlock_crash forKey:@"crash_type"];

NSDictionary *stack = [foomDict objectForKey:@"deadlockStack"];

if(stack && stack.count > 0){

[result setObject:stack forKey:@"stack_deadlock"];

OOM_Log("The app deadlock stack:%s",[[stack description] UTF8String]);

}

}

else {

OOM_Log("The app ocurred foom lastTime,detail info:%s",[[foomDict description] UTF8String]);

[result setObject:@foom_crash forKey:@"crash_type"];

NSString *uuid = [foomDict objectForKey:@"uuid"];

NSArray *oomStack = [[OOMDetector getInstance] getOOMDataByUUID:uuid];

if(oomStack && oomStack.count > 0)

{

NSData *oomData = [NSJSONSerialization dataWithJSONObject:oomStack options:0 error:nil];

if(oomData.length > 0){

// NSString *stackStr = [NSString stringWithUTF8String:(const char *)oomData.bytes];

OOM_Log("The app foom stack:%s",[[oomStack description] UTF8String]);

}

[result setObject:[self getAPMOOMStack:oomStack] forKey:@"stack_oom"];

}

}

return result;

}

}

......

}


內存畫像

內存畫像,就是程序在達到觸頂情況時,對內存進行快照,導出內存節點引用情況,從而找到內存大的原因在哪!要做的事情有兩個:

1.內存節點的獲取

內存節點的獲取要通過mach內核的vm_region_recurse/vm_region_recure64函數掃描進程中的所有VM Region,通過vm_region_submap_info_64結構體獲取詳細信息。

2.分析節點之間的引用關係

這裡又分為兩種情況:libmalloc維護的堆所在的VM Region包含的OC對象、C/C++對象、buffer等可以獲取詳細的引用關係,需要單獨處理。而非libmalloc維護的VM Region單獨的內存節點,僅記錄了起始地址和Dirty、Swapped內存大小,以及與其他節點之間的引用關係。

獲取節點的核心代碼如下:

void VMRegionCollect::startCollet() {

......

while (1) {

struct vm_region_submap_info_64 info;

mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;

krc = vm_region_recurse_64(mach_task_self(), &address, &size, &depth, (vm_region_info_64_t)&info, &count);

if (krc == KERN_INVALID_ADDRESS){

break;

}

if (info.is_submap){

depth++;

} else {

//do stuff

proc_regionfilename(pid, address, buf, sizeof(buf));

printf("Found VM Region: %08x to %08x (depth=%d) user_tag:%s name:%s\n", (uint32_t)address, (uint32_t)(address+size), depth, [visualMemoryTypeString(info.user_tag) cStringUsingEncoding:NSUTF8StringEncoding], buf);

address += size;

}

}

}

掃描節點 case 數據信息如下:

堆內存節點引用關係的核心代碼如下:

通過類成員變數的地址與引用類的isa指針地址進行匹配,從而發現是否存在引用關係!

static void range_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount) {

if (!context) {

return;

}

for (unsigned int i = 0; i < rangeCount; i++) {

vm_range_t range = ranges[i];

flex_maybe_object_t *tryObject = (flex_maybe_object_t *)range.address;

Class tryClass = NULL;

#ifdef __arm64__

// See http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html

extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;

tryClass = (__bridge Class)((void *)((uint64_t)tryObject->isa & objc_debug_isa_class_mask));

#else

tryClass = tryObject->isa;

#endif

// If the class pointer matches one in our set of class pointers from the runtime, then we should have an object.

if (CFSetContainsValue(registeredClasses, (__bridge const void *)(tryClass))) {

(*(object_enumeration_block_t __unsafe_unretained *)context)((__bridge id)tryObject, tryClass);

}

}

}

static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_address, __unused vm_size_t size, void **local_memory) {

*local_memory = (void *)remote_address;

return KERN_SUCCESS;

}

+ (void)enumerateLiveObjectsUsingBlock:(object_enumeration_block_t)block {

if (!block) {

return;

}

[self updateRegisteredClasses];

vm_address_t *zones = NULL;

unsigned int zoneCount = 0;

kern_return_t result = malloc_get_all_zones(TASK_NULL, reader, &zones, &zoneCount);

if (result == KERN_SUCCESS) {

for (unsigned int i = 0; i < zoneCount; i++) {

malloc_zone_t *zone = (malloc_zone_t *)zones[i];

malloc_introspection_t *introspection = zone->introspect;

if (!introspection) {

continue;

}

void (*lock_zone)(malloc_zone_t *zone) = introspection->force_lock;

void (*unlock_zone)(malloc_zone_t *zone) = introspection->force_unlock;

object_enumeration_block_t callback = ^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {

unlock_zone(zone);

block(object, actualClass);

lock_zone(zone);

};

BOOL lockZoneValid = PointerIsReadable(lock_zone);

BOOL unlockZoneValid = PointerIsReadable(unlock_zone);

if (introspection->enumerator && lockZoneValid && unlockZoneValid) {

lock_zone(zone);

introspection->enumerator(TASK_NULL, (void *)&callback, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, &range_callback);

unlock_zone(zone);

}

}

}

}

獲取引用關係的 case 數據如下:


採用了倒序輸出引用關係,所以看上去是階梯型形式!

取出其中部分數據借用工具分析其引用關係如圖:


這樣可以很清晰的看到其堆內存節點間的引用關係及所佔內存大小。


總結

以上便是APM系統中關於 OOM 的功能的介紹,主要包含三大功能點:

  • OOM 預警可以發現線上App發生超過內存閥值時記錄,以標識存在 OOM 導致 crash 的風險。
  • OOM 監控則在發生 OOM 時及時記錄案發現場,給後續開發者問題查找提供線索。
  • 內存畫像則在發生OOM 時導出其引用關係,記錄節點大小等信息,更直觀的查找內存大在何處。


四、Crash日誌

4.1 APM上報Crash日誌流程

項目集成 APM,SDK 初始化時,會默認打開 Crash 監控,當發生 Crash 時,將按照以下步驟執行:

  1. KSCrash 收集到崩潰日誌後執行 APM 的 crashCallBack函數
  2. 在 crashCallBack 函數中將日誌寫入 APMLog 並緩存起來
  3. 當下次啟動時,APM 初始化成功後按照上報流程上報Crash 文件到伺服器中



4.2 日誌解析

APM的 iOS 端SDK只需要將日誌成功上報後,服務端會根據Crash 的信息,如版本號,binaryImage 以及 UUID 等信息進行符號工作,符號化成功後,就可以在管理後台查看到相應的日誌。


參考文獻

1.Black, David L. The mach Exception Handing Facility.

2.iOS Crash 分析攻略 https://developer.aliyun.com/article/766088

3.《深入解析Mac OSX & iOS操作系統》

作者:鄭更濠、藍海庭

來源:微信公眾號:映客技術

出處:https://mp.weixin.qq.com/s/WGod1JhojaWhuOap45QxaA