使用者在使用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
- 物件接收到未知的訊息,即下圖中訊息未能處理的情況。
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:
方法:hookobserveValueForKeyPath
方法,增加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)“機制,通過這一機制,我們可以告訴物件如何處理未知的訊息。預設情況下,物件接收到未知的訊息,會導致程式崩潰。
上圖可以看出,在一個函式找不到時,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會釋放與例項相關聯的引用,但是並不釋放該例項的記憶體。