作者:哈雷哈雷_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:

相關文章