iOS Crash不崩潰

tingdongli發表於2018-10-10

       使用者在使用App的過程中,經常遇到閃退的情況,體驗不太好,本文嘗試探索引發閃退的原因,以及在遇到crash的情況下,儘可能的保持程式執行,並及時上報錯誤。

一、crash型別

1.OC層面的crash

1.1 普通型別

  • NSInvalidArgumentException:非法引數異常,傳入非法引數導致異常,nil引數比較常見。
  • NSRangeException:下標越界導致的異常。
  • NSGenericException: foreach的迴圈當中修改元素導致的異常。

1.2 KVO

KVO Crash常見原因:

  • 移除未註冊的觀察者
  • 重複移除觀察者
  • 新增了觀察者但是沒有實現-observeValueForKeyPath:ofObject:change:context:方法
  • 新增移除keypath=nil
  • 新增移除observer=nil

1.3 unrecognized selector sent to instance

  • 物件接收到未知的訊息,即下圖中訊息未能處理的情況。
    iOS Crash不崩潰

2.Signal層面的crash

除了OC層面的異常捕獲之外,很多記憶體錯誤、訪問錯誤的地址產生的crash則需要利用unix標準的signal機制,註冊SIGABRT, SIGBUS, SIGSEGV等訊號發生時的處理函式。該函式中我們可以輸出棧資訊,版本資訊等其他一切我們所想要的。

  • SIGKILL:用來立即結束程式的執行的訊號。
  • SIGSEGV:試圖訪問未分配給自己的記憶體, 或試圖往沒有寫許可權的記憶體地址寫資料。
  • SIGABRT:呼叫abort函式生成的訊號。
  • SIGTRAP:由斷點指令或其它trap指令產生。
  • SIGBUS:非法地址, 包括記憶體地址對齊(alignment)出錯。比如訪問一個四個字長的整數, 但其地址不是4的倍數。它與SIGSEGV的區別在於後者是由於對合法儲存地址的非法訪問觸發的(如訪問不屬於自己儲存空間或只讀儲存空間)。

二、存在問題

  • 程式閃退,使用者體驗不好

三、監聽crash

1.任憑程式閃退並上報

1.1 NSSetUncaughtExceptionHandler 捕獲OC層面的crash

參考文章

(1)AppDelegate中新增捕獲監聽

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
  return YES;
  }
複製程式碼

(2)解析堆疊資訊並上報

void UncaughtExceptionHandler(NSException *exception) {
  /**
   *  獲取異常崩潰資訊
   */
  NSArray *callStack = [exception callStackSymbols];
  NSString *reason = [exception reason];
  NSString *name = [exception name];
}
複製程式碼

1.2 Appdelegate中註冊SIGABRT, SIGBUS, SIGSEGV等訊號發生時的處理函式,處理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);
}
複製程式碼
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];

}
複製程式碼

2.Crash自動修復+捕獲上報

2.1 針對普通型別Crash的處理機制

hook相關的方法,增加保護機制。 以NSArray越界為例,hook objectAtIndex方法,在方法中捕獲越界異常,並在最後返回一個nil物件。

[self exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(avoidCrashObjectAtIndex:)];
複製程式碼
- (id)avoidCrashObjectAtIndex:(NSUInteger)index {
  id object = nil;
  
  @try {
      object = [self avoidCrashObjectAtIndex:index];
  }
  @catch (NSException *exception) {
     //捕獲異常,根據exception列印出堆疊資訊,同時也避免了程式崩潰
  }
  @finally {
      return object;
  }
}
複製程式碼

注意:使用方法進行捕獲異常之後,第三方工具將不會蒐集到崩潰資訊並上報,需要在catch中手動上報。

2.2 針對KVO Crash的處理機制

新建一個物件,用來記錄target,observer,context,keypath等,每新增一個監聽,增加一個物件,用一個陣列維護。新增和刪除的時候做判斷,同時hook dealloc函式,dealloc的同時移除我的觀察者和我觀察的物件。dealloc時遍歷陣列,陣列中不應該存在物件,如果存在物件,應該丟擲異常並接收,提示使用者KVO的釋放存在問題。

  • 移除未註冊的觀察者:在移除A物件的觀察者時,先判斷陣列中是否有A物件的觀察者,如果有,再移除。
  • 重複移除觀察者:同上
  • 新增了觀察者但是沒有實現-observeValueForKeyPath:ofObject:change:context:方法:hook observeValueForKeyPath方法,增加try-catch即可。
  • 新增移除keypath=nil:hook新增移除觀察者的方法,在新方法中過濾keypath=nil的情況。
  • 新增移除observer=nil:hook新增移除觀察者的方法,在新方法中過濾observer=nil的情況。

注意:使用方法進行捕獲異常之後,第三方工具將不會蒐集到崩潰資訊並上報,需要在catch中手動上報。

2.3 針對unrecognized selector解決方案

通常,當我們不能確定一個物件是否能接收某個訊息時,會先呼叫respondsToSelector:來判斷一下。如下程式碼所示:

if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
複製程式碼

當一個物件無法接收某一訊息時,就會啟動所謂”訊息轉發(message forwarding)“機制,通過這一機制,我們可以告訴物件如何處理未知的訊息。預設情況下,物件接收到未知的訊息,會導致程式崩潰。

iOS Crash不崩潰

上圖可以看出,在一個函式找不到時,Objective-C提供了三種方式去補救:

1、呼叫resolveInstanceMethod給個機會讓類新增這個實現這個函式

2、呼叫forwardingTargetForSelector讓別的物件去執行這個函式

3、呼叫methodSignatureForSelector(函式符號製造器)和forwardInvocation(函式執行器)靈活的將目標函式以其他形式執行。

如果都不中,呼叫doesNotRecognizeSelector丟擲異常。

- (void)forwardInvocation:(NSInvocation *)anInvocation

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

方法一:hook上述兩個方法,在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:)];
複製程式碼
- (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) {
      //捕獲異常,根據exception列印出堆疊資訊,同時也避免了程式崩潰
      //上報
    } @finally {
        
    }
}
複製程式碼

方法二:直接hook doesNotRecognizeSelector也可實現,doesNotRecognizeSelector起到丟擲異常的作用,自己增加try-catch進行捕獲即可,程式碼如下:

[self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)];
複製程式碼
- (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{
    @try {
        [self avoidCrashDoesNotRecognizeSelector:aSelector];
        
    } @catch (NSException *exception) {
       //捕獲異常,根據exception列印出堆疊資訊,同時也避免了程式崩潰
       //上報
    } @finally {
        
    }
}
複製程式碼

效果如下:

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.
複製程式碼

列印出了堆疊資訊,同時避免了程式崩潰。

注意:使用方法進行捕獲異常之後,第三方工具將不會蒐集到崩潰資訊並上報,需要在catch中手動上報。

2.4 針對野指標的處理機制

模仿Xcode的zombie機制:

1.Swizzle原有allocWithZone方法,新增野指標防護標記。

2.Swizzle原有dealloc方法,如果有野指標防護標記,呼叫 objc_destructInstance方法,修改例項isa使其指向zombieObject,儲存原始 類名,以便上報使用。

3.Swizzle訊息轉發機制forwardingTargetForSelector方法,處理所 有原始類originObject的方法,收集錯誤資訊並上報。

4.及時釋放zombieObject。

注: objc_destructInstance會釋放與例項相關聯的引用,但是並不釋放該例項的記憶體。

參考文章

相關文章