一. 什么是SIGKILL崩潰
很多時候,當我們在崩潰日志中看到 SIGKILL 關鍵信息的時候,這就表示操作系統從上層殺死了我們的進程,也就是我們常說的 kill -9 命令。
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace RUNNINGBOARD, Code 0xdead10cc
一般來說,Apple 崩潰日志里面通常都會包含應用程序被殺死的具體的原因。如上所示,Termination Reason 里面就包含了這個崩潰的錯誤代碼 0xdead10cc,就表示應用程序掛起的時候發生了文件和數據庫鎖操作而被操作系統殺死。
二. 怎么抓取 SIGKILL 崩潰
丨 1 為什么SIGKILL不能被捕獲
和其他信號不同,SIGKILL 是不可被捕獲的。這是 Linux / Mach 內核的限制,這種限制就是為了讓操作系統在程序無法響應的時候,可以從上一層控制進程的生命周期。
丨 2 使用 MetricKit 框架捕獲SIGKILL
丨 2.1 Metrickit 是什么
MetricKit 框架是蘋果在 iOS13 系統開始引入的用來匯總和分析有關異常和崩潰診斷以及電源和性能指標的動態庫。
丨 2.2 使用Metrickit 收集 SIGKILL信號量 的好處
- 不需要注冊信號量捕獲回調函數
- 不需要時刻監控,只需冷啟階段注冊獲取一次就行
丨 2.3 怎么使用 Metrickit 獲取崩潰信息
丨 2.3.1 添加 MetricKit 動態庫依賴
丨2.3.2 注冊 MetricKit 監聽者
if (@available(iOS 14.0, *)) {
MXMetricManager *manager = [MXMetricManager sharedManager];
if (self && manager && [manager respondsToSelector:@selector(addSubscriber:)]) {
[manager addSubscriber:self];
}
}
丨2.3.3 監聽者實現MXMetricManagerSubscriber協議方法,payloadDic里面包含著上次本應用發生的崩潰日志堆棧和信息
// 用戶如果有崩潰數據,注冊監聽之后就會回調
- (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload *> * _Nonnull)payloads API_AVAILABLE(ios(14.0)){
if (@available(iOS 14.0, *)) {
for (MXDiagnosticPayload *payload in payloads) {
NSDictionary *payloadDic = [payload dictionaryRepresentation];
});
}
}
}
丨2.3.4 當收到回調消息后,需要對關鍵信息做組裝,獲取崩潰堆棧和相關關鍵信息
NSArray *callStackRootFrames = [dicFrame ArrayValueForKey:@"callStackRootFrames"];
if (callStackRootFrames.count <= 0) {
continue;
}
NSDictionary *dicZero = [callStackRootFrames ObjectAtIndex:0];
int rootIndex = 0;
while (dicZero && dicZero.count > 0) {
//獲取Image 的 UUID
NSString *binaryUUID = [dicZero stringValueForKey:@"binaryUUID"];
//獲取Image 的 名稱
NSString *binaryName = [dicZero stringValueForKey:@"binaryName"];
//獲取Image 的加載地址
long long baseAdd = [[dicZero NumberValueForKey:@"offsetIntoBinaryTextSegment"] longLongValue];
//獲取崩潰函數的地址
long long address = [[dicZero numberValueForKey:@"address"] longLongValue];
//看上一層調用堆棧的
NSArray *subFrames = [dicZero arrayValueForKey:@"subFrames"];
[strStack appendFormat:@"%d %@ 0x%llx 0x%llx + %lld\n", rootIndex, binaryName, baseAdd, address, address - model.baseAddress];
rootIndex++;
if (subFrames && subFrames.count >= 0) {
dicZero = [subFrames ObjectAtIndex:0];
} else {
dicZero = nil;
}
}
丨2.3.5 使用 Metrickit 收集崩潰的不足
- 只支持 iOS14 以后的崩潰日志收集;PS:MetricKit是iOS13開始有的框架,但是崩潰日志的支持是iOS14開始支持的。
- 崩潰日志沒有返回具體的崩潰時間和啟動時間,崩潰場景信息除了堆棧外沒有其余信息,附加信息較少,需要另外的手段來收集
- 如果使用了段遷移編譯技術,主程序 Mach-O 的加載地址和 uuid MetricKit無法給出正確的值,需要例外處理??赏ㄟ^ Mach-O文件的LC-MAIN入口來獲取主程序main函數的地址,從而算出加載其起始地址。
- iOS14 的崩潰日志是24小時會回調通知一次,時效性低;iOS15 之后,崩潰日志會在下次啟動之后就返回,但經驗證,可能有例外情況。
丨 3 SIGKILL 日志中 Code 的含義解釋
- 0x8badf00d:
發音(ate bad food),意思就是吃了壞的食物。表示操作系統因為看門狗原因殺死了應用程序;具體可以參見蘋果文檔Addressing Watchdog Terminations (https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations)- 0xc00010ff:
發音 (cool off)。表示操作系統因為過熱殺死了應用程序, 關于怎么樣使你的程序更高效,更低消耗,可以觀看:
a. iOS Performance and Power Optimization with Instruments (https://developer.apple.com/videos/)
b. WWDC session (https://developer.apple.com/videos/)
- 0xbaadca11:
發音(bad call)。表示 應用程序響應 PushKit 的消息并且CallKit調用失敗,從而應用程序被系統殺死。
- 0xbad22222:
表示因為通過 VoIP 調起程序太頻繁而被系統殺死。
- 0xc51bad01:
watchOS 因為后臺任務耗費太多 CPU 時間而被殺死。這就需要優化和減少后臺任務的 CPU 時間,提高 CPU 使用效率,或者在后臺的時候減少大量任務。
- 0xc51bad02:
watchOS 因為后臺任務不能在初始化時間內完成而殺死 應用程序,減少在后臺任務的數量可以解決這個問題。
丨 4 百度App常見SIGKILL問題
丨 4.1 主線程執行耗時操作太久
當應用程序在阻塞主線程一段時間之后就會被看門狗殺死,一般的耗時事件可能有如下幾種情形:
- 弱網下同步的網絡請求
- 處理大量的數據的任務,比如大的JSON文件或者 3D 模型的加載和處理
- 觸發大量的 Core Data 同步保存操作
- 觸發大量的數據庫操作等
- 主線程解碼大圖片,解壓文件等操作
通用解決方案:
將耗時任務的處理放在子線程處理,等處理完成之后回調給主線程, 類似如下操作,在單獨的隊列處理任務,處理之后回調 block
- (void)getContentArray:(void (^)(NSArray *resultArray))completeBlock {
dispatch_barrier_async(self.readWriteQueue, ^{
if (completeBlock) {
NSArray *resultArray = [NSArray arrayWithArray:self.array];
completeBlock(resultArray);
}
});
}
**丨**4.2 主線程和子線程死鎖,陷入互相等待的循環
舉個栗子:
主線程和子線程在單例初始化的時候陷入死鎖 [xxxConfig sharedInstance],見如下崩潰堆棧
Thread 0 Crashed:
0 libsystem_kernel.dylib ___ulock_wait (in libsystem_kernel.dylib) 8
1 libdispatch.dylib __dlock_wait (in libdispatch.dylib) 56
2 libdispatch.dylib __dispatch_once_wait (in libdispatch.dylib) 120
3 BaiduBoxApp +[xxxConfig sharedInstance] (in BaiduBoxApp) 20
4 BaiduBoxApp +[xxxConfig updateABConfig] (in BaiduBoxApp) 0
5 BaiduBoxApp -[xxxManager startOnce] (in BaiduBoxApp) 20
子線程堆棧
Thread 33 :
0 libsystem_kernel.dylib ___ulock_wait (in libsystem_kernel.dylib) 8
1 libdispatch.dylib __dlock_wait (in libdispatch.dylib) 56
2 libdispatch.dylib __dispatch_thread_event_wait_slow (in libdispatch.dylib) 56
3 libdispatch.dylib ___DISPATCH_WAIT_FOR_QUEUE__ (in libdispatch.dylib) 364
4 libdispatch.dylib __dispatch_sync_f_slow (in libdispatch.dylib) 144
.........
9 BaiduBoxApp ___48+[xxxConfig sharedInstance]_block_invoke (in BaiduBoxApp) 0
10 libdispatch.dylib __dispatch_client_callout (in libdispatch.dylib) 20
11 libdispatch.dylib __dispatch_once_callout (in libdispatch.dylib) 32
12 BaiduBoxApp +[xxxConfig sharedInstance] (in BaiduBoxApp) 20
相關鏈接
[1] Addressing Watchdog Terminations
https://github.com/alibaba/dexposed
[2] Understanding the Exception Types in a Crash Report
https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report#EXC_CRASH-
[3] MetricKit Framework
https://developer.apple.com/documentation/metrickit?language=objc
[4] Examining the Fields in a Crash Report
https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report