iOS Crash不崩潰

用戶在使用App的過程中,經(jīng)常遇到閃退的情況,體驗(yàn)不太好,本文嘗試探索引發(fā)閃退的原因,以及在遇到crash的情況下,盡可能的保持程序運(yùn)行,并及時(shí)上報(bào)錯(cuò)誤。

一、crash類型

1.OC層面的crash

1.1 普通類型

  • NSInvalidArgumentException:非法參數(shù)異常,傳入非法參數(shù)導(dǎo)致異常,nil參數(shù)比較常見。
  • NSRangeException:下標(biāo)越界導(dǎo)致的異常。
  • NSGenericException: foreach的循環(huán)當(dāng)中修改元素導(dǎo)致的異常。

1.2 KVO

KVO Crash常見原因:

  • 移除未注冊的觀察者
  • 重復(fù)移除觀察者
  • 添加了觀察者但是沒有實(shí)現(xiàn)-observeValueForKeyPath:ofObject:change:context:方法
  • 添加移除keypath=nil
  • 添加移除observer=nil

1.3 unrecognized selector sent to instance

  • 對象接收到未知的消息,即下圖中消息未能處理的情況。


    image

    <figcaption></figcaption>

2.Signal層面的crash

除了OC層面的異常捕獲之外,很多內(nèi)存錯(cuò)誤、訪問錯(cuò)誤的地址產(chǎn)生的crash則需要利用unix標(biāo)準(zhǔn)的signal機(jī)制,注冊SIGABRT, SIGBUS, SIGSEGV等信號發(fā)生時(shí)的處理函數(shù)。該函數(shù)中我們可以輸出棧信息,版本信息等其他一切我們所想要的。

  • SIGKILL:用來立即結(jié)束程序的運(yùn)行的信號。
  • SIGSEGV:試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)。
  • SIGABRT:調(diào)用abort函數(shù)生成的信號。
  • SIGTRAP:由斷點(diǎn)指令或其它trap指令產(chǎn)生。
  • SIGBUS:非法地址, 包括內(nèi)存地址對齊(alignment)出錯(cuò)。比如訪問一個(gè)四個(gè)字長的整數(shù), 但其地址不是4的倍數(shù)。它與SIGSEGV的區(qū)別在于后者是由于對合法存儲(chǔ)地址的非法訪問觸發(fā)的(如訪問不屬于自己存儲(chǔ)空間或只讀存儲(chǔ)空間)。

二、存在問題

  • 程序閃退,用戶體驗(yàn)不好

三、監(jiān)聽crash

1.任憑程序閃退并上報(bào)

1.1 NSSetUncaughtExceptionHandler 捕獲OC層面的crash

參考文章

(1)AppDelegate中添加捕獲監(jiān)聽

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
  return YES;
  }
復(fù)制代碼

(2)解析堆棧信息并上報(bào)

void UncaughtExceptionHandler(NSException *exception) {
  /**
   *  獲取異常崩潰信息
   */
  NSArray *callStack = [exception callStackSymbols];
  NSString *reason = [exception reason];
  NSString *name = [exception name];
}
復(fù)制代碼

1.2 Appdelegate中注冊SIGABRT, SIGBUS, SIGSEGV等信號發(fā)生時(shí)的處理函數(shù),處理Signal層面的crash。

void InstallSignalHandler(void)
{
  signal(SIGHUP, SignalExceptionHandler);
  signal(SIGINT, SignalExceptionHandler);
  signal(SIGQUIT, SignalExceptionHandler);

  signal(SIGABRT, SignalExceptionHandler);
  signal(SIGILL, SignalExceptionHandler);
  signal(SIGSEGV, SignalExceptionHandler);
  signal(SIGFPE, SignalExceptionHandler);
  signal(SIGBUS, SignalExceptionHandler);
  signal(SIGPIPE, SignalExceptionHandler);
}
復(fù)制代碼
void SignalExceptionHandler(int signal)
{

  NSMutableString *mstr = [[NSMutableString alloc] init];
  [mstr appendString:@"Stack:\n"];
  void* callstack[128];
  int i, frames = backtrace(callstack, 128);
  char** strs = backtrace_symbols(callstack, frames);
  for (i = 0; i <frames; ++i) {
      [mstr appendFormat:@"%s\n", strs[I]];
  }
  [SignalHandler saveCreash:mstr];

}
復(fù)制代碼

2.Crash自動(dòng)修復(fù)+捕獲上報(bào)

2.1 針對普通類型Crash的處理機(jī)制

hook相關(guān)的方法,增加保護(hù)機(jī)制。 以NSArray越界為例,hook objectAtIndex方法,在方法中捕獲越界異常,并在最后返回一個(gè)nil對象。

[self exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(avoidCrashObjectAtIndex:)];
復(fù)制代碼
- (id)avoidCrashObjectAtIndex:(NSUInteger)index {
  id object = nil;

  @try {
      object = [self avoidCrashObjectAtIndex:index];
  }
  @catch (NSException *exception) {
     //捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
  }
  @finally {
      return object;
  }
}
復(fù)制代碼

注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。

2.2 針對KVO Crash的處理機(jī)制

新建一個(gè)對象,用來記錄target,observer,context,keypath等,每添加一個(gè)監(jiān)聽,增加一個(gè)對象,用一個(gè)數(shù)組維護(hù)。添加和刪除的時(shí)候做判斷,同時(shí)hook dealloc函數(shù),dealloc的同時(shí)移除我的觀察者和我觀察的對象。dealloc時(shí)遍歷數(shù)組,數(shù)組中不應(yīng)該存在對象,如果存在對象,應(yīng)該拋出異常并接收,提示用戶KVO的釋放存在問題。

  • 移除未注冊的觀察者:在移除A對象的觀察者時(shí),先判斷數(shù)組中是否有A對象的觀察者,如果有,再移除。
  • 重復(fù)移除觀察者:同上
  • 添加了觀察者但是沒有實(shí)現(xiàn)-observeValueForKeyPath:ofObject:change:context:方法:hook observeValueForKeyPath方法,增加try-catch即可。
  • 添加移除keypath=nil:hook添加移除觀察者的方法,在新方法中過濾keypath=nil的情況。
  • 添加移除observer=nil:hook添加移除觀察者的方法,在新方法中過濾observer=nil的情況。

注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。

2.3 針對unrecognized selector解決方案

通常,當(dāng)我們不能確定一個(gè)對象是否能接收某個(gè)消息時(shí),會(huì)先調(diào)用respondsToSelector:來判斷一下。如下代碼所示:

if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
復(fù)制代碼

當(dāng)一個(gè)對象無法接收某一消息時(shí),就會(huì)啟動(dòng)所謂”消息轉(zhuǎn)發(fā)(message forwarding)“機(jī)制,通過這一機(jī)制,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下,對象接收到未知的消息,會(huì)導(dǎo)致程序崩潰。

image

<figcaption></figcaption>

上圖可以看出,在一個(gè)函數(shù)找不到時(shí),Objective-C提供了三種方式去補(bǔ)救:

1、調(diào)用resolveInstanceMethod給個(gè)機(jī)會(huì)讓類添加這個(gè)實(shí)現(xiàn)這個(gè)函數(shù)

2、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個(gè)函數(shù)

3、調(diào)用methodSignatureForSelector(函數(shù)符號制造器)和forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行。

如果都不中,調(diào)用doesNotRecognizeSelector拋出異常。

- (void)forwardInvocation:(NSInvocation *)anInvocation

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

方法一:hook上述兩個(gè)方法,在methodSignatureForSelector中返回有效的NSMethodSignature,在forwardInvocation中添加try-catch即可,代碼如下:

 [self exchangeInstanceMethod:[self class] method1Sel:@selector(methodSignatureForSelector:) method2Sel:@selector(avoidCrashMethodSignatureForSelector:)];
 [self exchangeInstanceMethod:[self class] method1Sel:@selector(forwardInvocation:) method2Sel:@selector(avoidCrashForwardInvocation:)];
復(fù)制代碼
- (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
    if ([self respondsToSelector:aSelector] || ms){
        return ms;
    }
    else{
        return [SafeProxy instanceMethodSignatureForSelector:@selector(safe_crashLog)];
    }
}

- (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {

    @try {
        [self avoidCrashForwardInvocation:anInvocation];

    } @catch (NSException *exception) {
      //捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
      //上報(bào)
    } @finally {

    }
}
復(fù)制代碼

方法二:直接hook doesNotRecognizeSelector也可實(shí)現(xiàn),doesNotRecognizeSelector起到拋出異常的作用,自己增加try-catch進(jìn)行捕獲即可,代碼如下:

[self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)];
復(fù)制代碼
- (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{
    @try {
        [self avoidCrashDoesNotRecognizeSelector:aSelector];

    } @catch (NSException *exception) {
       //捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
       //上報(bào)
    } @finally {

    }
}
復(fù)制代碼

效果如下:

NSInvalidArgumentException
*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]
Error Place:-[ViewController NSArray_Test_InstanceArray]
AvoidCrash default is to remove nil object and instance a array.
復(fù)制代碼

打印出了堆棧信息,同時(shí)避免了程序崩潰。

注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。

2.4 針對野指針的處理機(jī)制

模仿Xcode的zombie機(jī)制:

1.Swizzle原有allocWithZone方法,添加野指針防護(hù)標(biāo)記。

2.Swizzle原有dealloc方法,如果有野指針防護(hù)標(biāo)記,調(diào)用 objc_destructInstance方法,修改實(shí)例isa使其指向zombieObject,保存原始 類名,以便上報(bào)使用。

3.Swizzle消息轉(zhuǎn)發(fā)機(jī)制forwardingTargetForSelector方法,處理所 有原始類originObject的方法,收集錯(cuò)誤信息并上報(bào)。

4.及時(shí)釋放zombieObject。

注: objc_destructInstance會(huì)釋放與實(shí)例相關(guān)聯(lián)的引用,但是并不釋放該實(shí)例的內(nèi)存。

作者:乳豬嘯谷
鏈接:https://juejin.im/post/6844903688608153614

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容