1. 程式人生 > >iOS Crash閃退日誌的捕獲和上傳至伺服器

iOS Crash閃退日誌的捕獲和上傳至伺服器

今天我要講的是app的閃退資訊的捕獲,以及日誌上傳。

涉及的技術點

  • 異常處理
  • 捕獲方式
  • 訊號量
  • 閃退日誌上傳

在APP開發中,對於開發者或者使用者最不能接受的bug就是APP崩潰,所以對於APP閃退的問題追蹤非常重要,有利於儘快的修復這個問題。現在有許多的第三方崩潰日誌統計服務sdk,如:Bugtags,騰訊的Bugly,友盟等。這些服務商提供了非常便捷的整合方式。關於如何使用這些工具,請看以上的官網就可以,有很詳細的介紹,今天我們來講一下如何實現這一項技術。

提出問題:

如何捕獲app閃退的原因?如何將閃退的原因傳送到伺服器?

異常處理

在iOS中,異常的處理有兩種:預先設定捕獲的異常

未知異常

預先設定捕獲的異常的處理

該種異常是通過標準的@try @catch(id exception) @finally 來處理的。

虛擬碼如下:
@try {
// 有可能出現異常的程式碼塊,可以建立異常物件並且手動丟擲異常或者在執行可能出現的程式碼會自動觸發並丟擲異常

} @catch (id exception)  // 大部分情況下是通過直接指定異常引數@catch(NSException *e)
{
// 捕獲到異常的處理措施

} @finally {
// 無論有無異常都要執行的操作,例如某些資源的釋放等。

}

對於@try程式碼塊中出現的異常,會將異常傳送到@catch

程式碼塊中進行處理。在這種情況下出現的異常情況,不會將異常傳送到系統級別,因此不會引起系統的閃退情況。

示例程式碼:
- (void)funtion {

    NSMutableArray *mutaArray = [NSMutableArray arrayWithCapacity:0];
    NSString *str = nil;

    for (int i = 0; i < 8; i++) {
        @try {
            if (i == 7) {
                [mutaArray addObject:str];
            } else
{ [mutaArray addObject:@(i)]; } } @catch(NSException *e) { NSLog(@"%@",e); } @finally { NSLog(@"finally"); } } }

當系統執行到該函式的時候,系統並不會導致閃退,因為已經對丟擲的異常進行了異常捕獲。

未知異常的處理

所謂未知異常就是沒有對可能發生異常的程式採取異常捕獲機制,出現異常後引起APP的閃退。這類異常的出現是由於沒有對區域性對異常進行處理,則系統預設將異常傳遞個系統級別。
示例程式碼:

- (void)funtionWithUncaughtException {

NSMutableArray *mutaArray = [NSMutableArray arrayWithCapacity:0];
NSString *str = nil;

for (int i = 0; i < 8; i++) {
        if (i == 7) {
            [mutaArray addObject:str];
        } else {
            [mutaArray addObject:@(i)];
        }
}
}

上述程式碼引起APP閃退,在終端如下的提示:

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil’
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010eda11cb __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x000000010e703f41 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010ede0e8c _CFThrowFormattedException + 194
    3   CoreFoundation                      0x000000010ecd06f1 -[__NSArrayM insertObject:atIndex:] + 1233
    4   CrashLogDemo                        0x000000010ddf7ccb -[ViewController funtion] + 123
    5   CrashLogDemo                        0x000000010ddf766a -[ViewController TestCrash:] + 58
    6   UIKit                               0x00000001109779bd -[UIApplication sendAction:to:from:forEvent:] + 83
    7   UIKit                               0x0000000110aee183 -[UIControl sendAction:to:forEvent:] + 67
    8   UIKit                               0x0000000110aee4a0 -[UIControl _sendActionsForEvents:withEvent:] + 450
    9   UIKit                               0x0000000110aed3cd -[UIControl touchesEnded:withEvent:] + 618
    10  UIKit                               0x00000001109ebd4f -[UIWindow _sendTouchesForEvent:] + 2807
    11  UIKit                               0x00000001109ed472 -[UIWindow sendEvent:] + 4124
    12  UIKit                               0x0000000110992802 -[UIApplication sendEvent:] + 352
    13  UIKit                               0x000000012851e7b3 -[UIApplicationAccessibility sendEvent:] + 85
    14  UIKit                               0x00000001112c4a50 __dispatchPreprocessedEventFromEventQueue + 2809
    15  UIKit                               0x00000001112c75b7 __handleEventQueueInternal + 5957
    16  CoreFoundation                      0x000000010ed442b1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    17  CoreFoundation                      0x000000010ede3d31 __CFRunLoopDoSource0 + 81
    18  CoreFoundation                      0x000000010ed28c19 __CFRunLoopDoSources0 + 185
    19  CoreFoundation                      0x000000010ed281ff __CFRunLoopRun + 1279
    20  CoreFoundation                      0x000000010ed27a89 CFRunLoopRunSpecific + 409
    21  GraphicsServices                    0x0000000113dad9c6 GSEventRunModal + 62
    22  UIKit                               0x0000000110975d30 UIApplicationMain + 159
    23  CrashLogDemo                        0x000000010ddf859f main + 111
    24  libdyld.dylib                       0x0000000110585d81 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

以上介紹了app開發的兩種異常,總結下:

1.預先設定對異常的捕獲,不會引起app的閃退。沒有對可能發生異常的地方進行處理機制,會引起app的閃退。
2.出現app閃退有一個比較好的方面就是容易排查系統存在的bug。
3.若是不採用區域性異常捕獲機制,又想要減少系統閃退的概率只能採用更多的邏輯判斷,
    例如:if(str) {
        [mutaArray addObject:str];
    }

捕獲方式

預先設定捕獲的異常的捕獲

預先設定捕獲的異常的捕獲在@cathch程式碼塊內捕獲處理的。

未知異常的捕獲

在預設情況下,系統發生了未知異常,系統會捕獲該異常並且退出app。發生異常後,系統會建立一個NSExcetion物件,並且在異常出丟擲,等待有接受者,若沒有傳遞給系統處理。那麼如何來獲取這個未知異常呢?該問題問的好!利用如下函式來解決這個問題:

FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

該函式是一個全域性的函式,該函式一般在app開始的時候呼叫,該函式的意義:設定未知異常的捕獲函式,引數是未知異常處理函式的函式名,該未知異常處理函式的模式如下:
typedef void NSUncaughtExceptionHandler(NSException *exception);
未知異常處理函式的示例程式碼:

void uncaughtExceptionHandler(NSException *exception) {
   // 在app退出前的一些處理任務,系統會等待該函式的執行完畢 
   // NSLog(@“CRASH: %@“, exception);
   // NSLog(@“Stack Trace: %@“, [exception callStackSymbols]);
   // Internal error reporting 
}

呼叫方式:

NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler); // 該函式可在任務未知呼叫。

完成上述的異常處理函式的定義和呼叫後,若再發生系統的未知異常的情況下,系統首先將異常傳遞個該函式,執行完該函式後app退出。因此,我們可以在這個函式內做一些業務處理,例如記錄或者傳遞異常等。

訊號量機制

訊號量的內容並不是這篇文章的重點,由於使用到了,所以要提及一下。先做一個簡單的介紹,訊號量主要用作多個執行緒對某項資源的使用,訊號量代表資源的可利用數目,該數目為非負數,當某個執行緒要使用某項資源的時候,資源若是大於0,則可以使用,否則需要等待其他的執行緒釋放該資源後才可使用。
在ios開發中使用GCD技術的dispacth semaphore,具體的請自行google:ios semaphore,會有諸多的描述,稍後我也會在關於GCD的部落格中講述.

閃退日誌上傳

在未知異常處理的部分說過,在異常處理的處理函式中可以對異常做一些處理,例如對異常的記錄,上傳。對於異常的記錄我就不做額外的贅述,如同一般的資料儲存一樣。關鍵是需要進行同步操作,避免在操作的過程中跳出異常處理函式。
對於異常的上傳也是這樣的,需要通過同步來完成對資料的上傳操作。現在的一個問題:

現在ios的伺服器上傳需要同步操作,而NSURLSession現在伺服器請求都是非同步操作,就會發生程式已經退出,將停止對資料的傳送

因此解決的方式就是使用訊號量機制,程式碼如下:

// 通過post 或者 get 方式來將異常資訊傳送到伺服器

- (void)sendCrashLog:(NSString *)crashLog {

dispatch_semaphore_t semophore = dispatch_semaphore_create(0); // 建立訊號量
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:nil];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"your url"]];
[request setHTTPMethod:@"POST"];
request.HTTPBody = [[NSString stringWithFormat:@"crash=%@",crashLog]dataUsingEncoding:NSUTF8StringEncoding];
[[session dataTaskWithRequest:request 
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"response%@",response);
    dispatch_semaphore_signal(semophore); // 傳送訊號
}] resume];
dispatch_semaphore_wait(semophore, DISPATCH_TIME_FOREVER); // 等待

}

異常處理呼叫的地方

void uncaughtExceptionHandler(NSException *exception) {
    NSLog(@“CRASH: %@“, exception);
    NSLog(@“Stack Trace: %@“, [exception callStackSymbols]);
    NSLog(@“oh,app,you dead”);
    CrashLogSender *logSender = [[CrashLogSender alloc]init];
    [logSender sendCrashLog:exception.description];
}

到此,對該部分內容的描述已經結束,稍後會把相關的程式碼上傳到git上,敬請批評指正與交流。