原文連接html
如何去衡量一款應用的質量好壞?爲了回答這一問題,APM
這一目的性極強的工具向開發順應而生。最先的APM
開發只關注於crash
、cpu
這類的硬性指標。而隨着移動開發市場的成熟,愈來愈多的數據指標也被加入到了APM
的採集範疇中,包括感官體驗相關的數據和使用習慣等。node
然而,不管APM
最終如何發展,其最核心的採集指標必定是crash
數據。一套完善的crash
監控方案能夠快速的發現並協助完成問題定位,從而可以及時止損,避免更多的損失。而反過來講,若是crash
不能及時被發現,又或者由於採集鏈中出現異常致使了數據丟失,對於開發者和公司來講,這都會是一個噩夢。linux
細分之下,crash
分別存在mach exception
、signal
以及NSException
三種類型,每一種類型表示不一樣分層上的crash
,也擁有各自的捕獲方式。ios
mach exception
c++
mach異常
由處理器陷阱引起,在異常發生後會被異常處理程序轉換成Mach消息
,接着依次投遞到thread
、task
和host
端口。若是沒有一個端口處理這個異常並返回KERN_SUCCESS
,那麼應用將被終止。每一個端口擁有一個異常端口數組
,系統暴露了後綴爲_set_exception_ports
的多個API
讓咱們註冊對應的異常處理到端口中。git
mach異常
即使註冊了對應的處理,也不會致使影響原有的投遞流程。此外,即使不去註冊mach異常
的處理,最終通過一系列的處理,mach異常
會被轉換成對應的UNIX信號
,一種mach異常
對應了一個或者多個信號類型。所以在捕獲crash
要提防二次採集的可能。github
NSException
api
NSException
發生在CoreFoundation
以及更高抽象層,在CoreFoundation
層操做發生異常時,會經過__cxa_throw
函數拋出異常。在經過NSSetUncaughtExceptionHandler
註冊NSException
的捕獲函數以後,崩潰發生時會調用這個捕獲函數。但若是沒有任何函數去捕獲這個異常 若是在捕獲函數中沒有進行操做終止應用,最終異常會經過abort()
來拋出一個SIGABRT
信號。數組
因爲NSException
的抽象層次足夠高,相比較其餘的crash
類型,NSException
是能夠被人爲的阻止crash
的。好比@try-catch
機制可以捕獲塊中發生的異常,避免應用被殺死。但因爲try-catch
的開銷和回報不成正比,每每不會使用這種機制。其二是crash防禦
,這一手段經過hook
掉上層接口來規避crash
風險,可是隻建議用於線上防禦,並且hook
未必不會致使其餘的問題。安全
signal
signa
會致使crash
,這是多數iOS
開發者對於信號的印象。傳遞crash
信息其實只是信號的一部分功能,信號是一套基於POSIX標準
開發的通訊機制,具體能夠閱讀Signal-wikipedia。在signal.h
中聲明瞭32
種異常信號,下面列出一部分的信號異常對:
信號 | 異常 |
---|---|
SIGILL | 執行了非法指令,通常是可執行文件出現了錯誤 |
SIGTRAP | 斷點指令或者其餘trap指令產生 |
SIGABRT | 調用abort產生 |
SIGBUS | 非法地址。好比錯誤的內存類型訪問、內存地址對齊等 |
SIGSEGV | 非法地址。訪問未分配內存、寫入沒有寫權限的內存等 |
SIGFPE | 致命的算術運算。好比數值溢出、NaN數值等 |
雖然存在三種crash
,但因爲mach exception
會在BSD
層被轉換成UNIX信號
,NSException
在未被捕獲的狀況下會調用abort
拋出信號,所以即使是咱們只註冊了signal
的處理,只要註冊的signal
足夠多,理論上也是能捕獲到所有的crash
。
因爲crash
的捕獲機制只會保存最後一個註冊的handle
,所以若是項目中殘留或者存在另外的第三方框架採集crash
信息時,常常性的會存在衝突。解決衝突的作法是在註冊本身的handle
以前保存已註冊的處理函數,便於發生崩潰後能將crash
信息連續的傳遞下去。
struct sigaction my_action;
static struct sigaction registered_action;
static NSUncaughtExceptionHandler *previousHandle;
void signal_handler(int signal) {
......
}
void exception_handler(NSException *exception) {
......
}
void registerCrashHandle() {
previousHandle = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&exception_handler);
myAction.sa_handler = &signal_handler;
sigemptyset(&my_action.sa_mask);
sigaction(SIGABRT, &my_action, ®istered_action);
}
複製代碼
通常來講,一個經驗豐富的開發者在註冊crash
回調時都會主動的去保存其餘函數,避免由於衝突致使別人的數據丟失。可是即使按照這樣的方式來註冊你的回調,也不表明咱們的處理函數是安全的。最重要的緣由在於完成回調的註冊以後,咱們沒法保證後續會不會有其餘人繼續註冊,若是有就會存在被替換掉的風險
按照正常方式的作法,能保證先於咱們註冊的crash
回調不會被咱們攔截致使失敗,但若是在咱們後方存在另外的註冊,咱們須要一個有效的機制來保護咱們的採集數據。解決問題的收益是不變的,因此解決方案理當儘量的低開銷和低風險。
如何去判斷咱們的handle
是否安全?這要求咱們對已註冊的handle
進行檢測。首先檢測時機要選擇在哪?因爲crash
是可能發生在應用啓動階段的,所以crash
採集通常也是發生在didLaunch
這個時間,下圖是我繪製的應用啓動到徹底啓動的幾個重要階段:
applicationActive
這個階段基本上是能保證crash
相關的註冊都完成的,所以衝突檢測能夠放到這個階段進行。
利用已有的週期性機制或者使用定時器來進行handle
衝突檢測。能夠分別使用通知
和定時器
兩個機制來完成周期性檢測方案
監聽應用狀態
監聽UIApplicationDidBecomeActiveNotification
在應用進入活躍狀態時作檢測:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......
[[NSNotificationCenter defaultCenter] addObserver: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) name: UIApplicationDidBecomeActiveNotification object: nil];
......
}
static struct sigaction existActions[32];
static int fatal_signals[] = {
SIGILL,
SIGBUS,
SIGABRT,
SIGPIPE,
};
- (void)checkRegisterCrashHandler {
struct sigaction oldAction;
for (int idx = 0; idx < sizeof(fatal_signals) / sizeof(int); idx++) {
sigaction(fatal_signals[idx], NULL, &oldAction);
if (oldAction.sa_handler != &signal_handler) {
existActions[fatal_signals[idx]] = oldAction;
struct sigaction myAction;
myAction.sa_handler = &signal_handler;
sigemptyset(&myAction.sa_mask);
sigaction(SIGABRT, &myAction, NULL);
}
}
}
複製代碼
定時器檢測
建立定時器來進行週期性的檢測,相比通知的機制,能夠控制檢測間隔:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......
NSTimer *timer = [[NSTimer alloc] initWithFireDate: [NSDate date] interval: 30 target: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];
[timer fire];
......
}
複製代碼
經過hook
調用註冊handle
的對應函數,創建一個回調數組來保存非exception_handle
的全部回調,後續處理完咱們的採集,再逐個調起。因爲捕獲函數都是基於C
接口的,所以咱們須要fishhook來提供相應的hook
功能。
struct SignalHandler {
void (*signal_handler)(int);
struct SignalHandler *next;
}
struct SignalHandler *previousHandlers[32];
void append(struct SignalHandler *handlers, struct SignalHandler *node) {
......
}
static int (*origin_sigaction)(int, const struct sigaction *__restrict, struct sigaction * __restrict) = NULL;
int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
if (new_action.sa_handler != signal_handler) {
append(previousHandlers[signal], new_action);
return origin_sigaction(signal, NULL, old_action);
} else {
return origin_sigaction(signal, new_action, old_action);
}
}
複製代碼
在週期性檢測的方案下,假設存在handle
註冊鏈(依次從左到右):
previous
<- exception_handle
<- other
在檢測時發現當前回調是other
,因而從新註冊咱們的回調,保存other
。可是假如other
也保存了咱們的回調,這樣可能會致使崩潰發生的時候,調用順序變成一個死循環。
hook
方案則是由於在調用origin_sigaction
時會傳入old_action
,可能致使另外的註冊者保存了咱們的exception_handle
,並在最後處理的時候出現一樣的循環調用問題。對於hook
方案來講,解決方法要簡單不少,只須要在非咱們的註冊調用origin_sigaction
時不傳入old_action
就能保證其餘註冊者沒法獲取到咱們的回調:
int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
if (new_action.sa_handler != signal_handler) {
append(previousHandlers[signal], new_action);
return origin_sigaction(signal, NULL, NULL);
} else {
return origin_sigaction(signal, new_action, old_action);
}
}
複製代碼
而使用週期性監測,就須要考慮是否放棄other
的回調,最終只保證exception_handle
和previous
和更早以前的註冊可以被順利調起。
另外,hook
還存在一個風險是假如第三方一樣作了hook
掉註冊函數的處理,而且作了篩選處理,最終致使的結果是沒辦法完成任何一個註冊。兩害相較取其輕,我的的建議是使用週期性檢測方案。
上述的兩套方案都存在風險點,並且這些風險點對於應用來講都算是致命的。那麼有沒有幾乎沒有風險又能解決問題的辦法呢?答案是確定的,那就是不要用有潛在風險的第三方,或者和第三方開發者商量提供一個無需crash
採集的版本。
在應用發生崩潰的時候,此時的崩潰所在線程
是極不穩定的,不穩定性包括幾點:
內存不穩定
若是是內存相關錯誤引起的crash
,好比內存過載、野指針等,此時線程的內存是危險狀態。若是這時候在handle
中再次分配內存,極有可能致使二次crash
死鎖
大多數底層的的核心API
會涉及到加鎖處理,這一狀況在signal
錯誤中出現的較多。而做爲上層調用方的咱們是不自知的,此時錯誤的操做可能致使線程陷入死鎖狀態
理論上當咱們攔截了一個signal
的時候,此時的應用會陷入內核並中止工做,應用頁面卡死,這時候咱們可執行時長是無限的。若是處理鏈過長,耗時過多或者陷入某種循環,會形成一種應用卡死而非崩潰的錯覺,而通過我廠大量的統計,應用卡死
要比應用崩潰
更讓人難以接受。此外,過多的處理鏈會增長回調流程上的風險點。若是鏈條上的某個點發生了二次崩潰,會致使後續的處理都沒法執行。所以,不用第三方或者讓第三方去除crash
採集,是一種可行且高效的手段。
文中提到過一次如今比較流行的crash防禦
手段,這裏仍是想說兩句。在開發中,crash防禦
會形成依賴心理,下降對風險的敏感。而在線上,這種方案可能屏蔽了大量的低級錯誤,也是讓我不能容忍的,固然循環引用的防禦屬於例外。最後安利一波寒神的XXShield,除了容器類的防crash
都值得學習,尤爲是正確的method swizzling
姿式。