前言
應用程序發生 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,也就是說異常不會被處理。而一旦創建異常端口,這些端口就像系統中的其他端口一樣,可以轉交給其他任務甚至其他主機。
在發生異常時,會按照以下步驟執行:
- 嘗試將異常拋給線程中的異常端口
- 嘗試拋給任務的異常端口
- 嘗試拋給主機的異常端口(即主機註冊的默認端口)。
如果沒有一個端口返回 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信號:如 SIGBUS、SIGSEGV、SIGABRT、SIGKILL 等。
...... 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 信號就可以了?
其實不是的,這裡面有兩個原因:
- 因為並不是所有的 Mach 異常類型都有相對應的 UNIX 信號進行映射
- 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 操作系統為了控制內存資源過度使用而採用的一種資源管理機制。不同於 MacOS, Linux,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機制清理策略分為兩種情況:
- 單個 App 進程超過內存上線
- 設備的物理內存佔用受到壓力時會按照優先級完成清理:
- 後台應用 > 前台應用
- 內存佔用高的應用 > 內存佔用低的應用
- 用戶應用 > 系統應用
功能介紹&原理
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 時,將按照以下步驟執行:
- KSCrash 收集到崩潰日誌後執行 APM 的 crashCallBack函數
- 在 crashCallBack 函數中將日誌寫入 APMLog 並緩存起來
- 當下次啟動時,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