RunLoop總結:RunLoop的應用場景(五)阻止App崩潰一次
作者:哈雷哈雷_Wong
今天要介紹的RunLoop應用場景感覺很酷炫,我們可能不常用到,但是對於做Crash 收集的 SDK可能會用得比較頻繁吧。相比關於RunLoop 可以讓應用起死回生,大家都聽說過,可是怎麼實現呢?今天我就來實際試驗一下。
原理
iOS應用崩潰,常見的崩潰信息有
EXC_BAD_ACCESS
、
SIGABRT XXXXXXX
,而這裏分爲兩種情況,一種是未被捕獲的異常,我們只需要添加一個回調函數,並在應用啓動時調用一個 API即可;
另一種是直接發送的
SIGABRT XXXXXXX
,這裏我們也需要監聽各種信號,然後添加回調函數。
針對情況一,其實我們都見過。
我們在收集App崩潰信息時,需要添加一個函數
NSSetUncaughtExceptionHandler(&HandleException)
,參數 是一個回調函數,在回調函數里獲取到異常的原因,當前的堆棧信息等保存到 dump文件,然後供下次打開App時上傳到服務器。
其實,我們在HandleException回調函數中,可以獲取到當前的RunLoop,然後獲取該RunLoop中的所有Mode,手動運行一遍。
針對情況二,首先針對多種要捕獲的信號,設置好回調函數,然後也是在回調函數中獲取RunLoop,然後拿到所有的Mode,手動運行一遍。
代碼實現
第一步,我創建了一個處理類,並添加一個單例方法。(代碼見末尾的Demo)
第二步,在單例中對象實例化時,添加 異常捕獲 和 signal 處理的 回調函數。
- (void)setCatchExceptionHandler
{
// 1.捕獲一些異常導致的崩潰
NSSetUncaughtExceptionHandler(&HandleException);
// 2.捕獲非異常情況,通過signal傳遞出來的崩潰
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
第三步,分別實現 異常捕獲的回調 和 signal 的回調。
void HandleException(NSException *exception)
{
// 獲取異常的堆棧信息
NSArray *callStack = [exception callStackSymbols];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
CrashHandler *crashObject = [CrashHandler sharedInstance];
NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
void SignalHandler(int signal)
{
// 這種情況的崩潰信息,就另某他法來捕獲吧
NSArray *callStack = [CrashHandler backtrace];
NSLog(@"信號捕獲崩潰,堆棧信息:%@",callStack);
CrashHandler *crashObject = [CrashHandler sharedInstance];
NSException *customException = [NSException exceptionWithName:kSignalExceptionName
reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
第四步,添加讓應用起死回生的 RunLoop 代碼
- (void)handleException:(NSException *)exception
{
NSString *message = [NSString stringWithFormat:@"崩潰原因如下:\n%@\n%@",
[exception reason],
[[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
NSLog(@"%@",message);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"程序崩潰了"
message:@"如果你能讓程序起死回生,那你的決定是?"
delegate:self
cancelButtonTitle:@"崩就蹦吧"
otherButtonTitles:@"起死回生", nil];
[alert show];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!ignore) {
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:kSignalExceptionName]) {
kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
} else {
[exception raise];
}
}
因爲我這裏弄了一個AlertView彈窗,所以必須要回到主線程來處理。實際上,RunLoop 相關的代碼:
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!ignore) {
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
完全可以寫在 上面的 HandleException 回調 和 SignalHandler回調中。
第五步,寫一段會導致崩潰的代碼
我是在ViewController 中添加了一個點擊事件,弄了一個數組越界的Bug:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSArray *array =[NSArray array];
NSLog(@"%@",[array objectAtIndex:1]);
}
動態效果圖:
Demo 鏈接
https://github.com/Haley-Wong/RunLoopDemos
如果感覺這篇文章不錯可以點擊在看:point_down: