Mac App Crash 異常捕獲、PLCrashreporter使用以及如何定位crash代碼位置


最近app一直crash,咦,我為什么說一直....

hmm 所以,要開始對crash的部分下手了。

於是學習百度了下,學到了很多大佬前輩的經驗~~知識樹又增長了~~😄

前一篇文章,理解 iOS 異常類型,講了一些異常相關的知識base.

這篇文章主要記錄一些方法, 怎樣獲取這些異常信息幫助我們debug.

一、Assert

最暴力的assert直接拋出來的異常。這些在oc層面由iOS庫或者各種第三方庫或者oc runtime驗證出錯誤而拋出的異常,就是oc異常了。在debug環境下,oc異常導致的崩潰log中都會輸出完整的異常信息,比如: Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: 'OC Exception'。包括這個Exception的類名和描述.

二、Try Catch

雖然可以獲取到當前拋出異常並且阻止異常繼續往外拋導致程序崩潰,不過蘋果不建議這樣做(接着往下看,最后有更好的方法)。對於程序真的往外拋出並且我們很難catch到的異常,比如界面和第三方庫中甩出來的異常,是沒辦法用try catch做到的。
下面舉一些我們可以通過try catch 捕獲的例子以及拓展。

    //創建可變數組
     NSMutableArray * arrM = @[].mutableCopy;
     // 創建nil對象
     NSString * str = nil;
     // 測試try cash
     @try {
         //此處寫可能出現崩潰的代碼
         //數組插入nil對象
         [arrM addObject:str];
     } @catch (NSException *exception) {
         //捕獲到異常要執行的代碼
         NSLog(@"exc == %@, 最后我彈了一個彈框說這樣不合適",exception);
     } @finally {
         //不管能不能捕獲到異常都會執行的方法
         NSLog(@"最后");
     }
//exc == *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil, 最后我彈了一個彈框說這樣不合適
拓展--Swizzle

runtime有一個機制,方法交換-->Swizzle,先簡單介紹下。

oc的方法調用,比如[self test]會轉換為objc_msgSend(self,@selfector(test))。objc_msgsend會以@selector(test)作為標識,在方法接收者(self)所屬類(以及所屬類繼承層次)方法列表找到Method,然后拿到imp函數入口地址,完成方法調用。

typedef struct objc_method *Method;

// oc2.0已廢棄,可以作為參考
struct objc_method {
    SEL _Nonnull method_name;
    char * _Nullable method_types;
    IMP _Nonnull method_imp;
}

基於以上鋪墊,那么有兩種辦法可以完成交換:

  • 一種是改變@selfector(test),不太現實,因為我們一般都是hook系統方法,我們拿不到系統源碼,不能修改。即便是我們自己代碼拿到源碼修改那也是編譯期的事情,並非運行時(跑題了。。。)
  • 所以我們一般修改imp函數指針。改變sel與imp的映射關系;
系統為我們提供的接口

typedef struct objc_method *Method;Method是一個不透明指針,我們不能夠通過結構體指針的方式來訪問它的成員,只能通過暴露的接口來操作。

接口如下,很簡單,一目了然:

#import <objc/runtime.h>

/// 根據cls和sel獲取實例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

/// 給cls新增方法,需要提供結構體的三個成員,如果已經存在則返回NO,不存在則新增並返回成功
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);

/// 替換
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)

/// 跟定兩個method,交換它們的imp:這個好像就是我們想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
簡單使用

假設交換UIViewController的viewDidLoad方法,提示一點,+load 方法一般也執行一次,但是有些代碼不規范的情況會多次load,所以,在拓展子類的時候要注意,如果有用到load,就不要[super load]。

/// UIViewController 某個分類

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}

+ (void)load {
    [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

為了沒盲點,我們擴展下load的調用:
  • load方法的調用時機在dyld映射image時期,這也符合邏輯,加載完調用load。
  • 類與類之間的調用順序與編譯順序有關,先編譯的優先調用,繼承層次上的調用順序則是先父類再子類;
  • 類與分類的調用順序是,優先調用類,然后是分類;
  • 分類之間的順序,與編譯順序有關,優先編譯的先調用;
  • 系統的調用是直接拿到imp調用,沒有走消息機制;

手動的[super load]或者[UIViewController load]則走的是消息機制,分類的會優先調用,如果你運氣好,另外一個程序員也實現了UIViewController的分類,且實現+load方法,還后編譯,則你的load方法也只執行一次;(分類同名方法后編譯的會“覆蓋”之前的)

為了保險起見,還是:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
    });
}

這部分主要摘自OC方法交換swizzle詳細介紹——不再有盲點,作者針對方法交換,一些特殊情況做了詳細的分析介紹,有興趣的同學建議去看下~

拓展Try-Catch +Swizzle<一>

前面提到了try catch 在一般情況下是可以抓到異常防止crash的,根據swizzle的特性,我們可以對類方法進行交換通過try catch來捕獲crash異常。代碼如下,親測有效。

這里舉的例子是,create一個NSMutableArray的分類,通過swizzle 一個內部方法,addobject來做try catch


#import <objc/runtime.h>

@implementation NSMutableArray (Extension)

+ (void)load {
    NSLog(@"enter load ");
    
    static dispatch_once_t onceToken;
       dispatch_once(&onceToken, ^{
           Class arrayMClass = NSClassFromString(@"__NSArrayM");
           
           //獲取系統的添加元素的方法
           Method addObject = class_getInstanceMethod(arrayMClass, @selector(addObject:));
           
           //獲取我們自定義添加元素的方法
           Method avoidCrashAddObject = class_getInstanceMethod(arrayMClass, @selector(avoidCrashAddObject:));
           
           //將兩個方法進行交換
           //當你調用addObject,其實就是調用avoidCrashAddObject
           //當你調用avoidCrashAddObject,其實就是調用addObject
           method_exchangeImplementations(addObject, avoidCrashAddObject);
       });
    
   
}

- (void)avoidCrashAddObject:(id)anObject {
    @try {
        [self avoidCrashAddObject:anObject];//其實就是調用addObject
    }
    @catch (NSException *exception) {
        
        //能來到這里,說明可變數組添加元素的代碼有問題
        //你可以在這里進行相應的操作處理
        
        NSLog(@"異常名稱:%@   異常原因:%@",exception.name, exception.reason);
    }
    @finally {
        //在這里的代碼一定會執行,你也可以進行相應的操作
    }
}

@end

測試代碼:

  //創建可變數組
     NSMutableArray * arrM = @[].mutableCopy;
     // 創建nil對象
     NSString * str = nil;
    [arrM addObject:str];

//結果如下:
2020-11-12 09:18:25.117085+0800 testCCC[48400:934118] enter load
2020-11-12 09:18:25.307957+0800 testCCC[48400:934118] Metal API Validation Enabled
2020-11-12 09:18:27.570020+0800 testCCC[48400:934118] 異常名稱:NSInvalidArgumentException   異常原因:*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil

這說明,我們可以通過給調用方法進行方法交換自動添加一個try catch的機制來捕獲常規異常。

拓展Try-Catch +Swizzle<二>

前面是修改內建的類別,現在我們自己create一個分類,順便探索下try catch能捕獲的幾個例子。

新建類testMethod,新增方法如下:


#import "testMethod.h"

@implementation testMethod

-(void)testPointer{
    NSLog(@"enter normal class method ");

  //測試一
//    int *x = 0;
//    *x = 200;
    //測試二
    [self performSelector:@selector(doSome:)];
    
  //測試三
//    ((char *)NULL)[1] = 0;

}

新建該類別category如下:

#import "testMethod+Extension.h"
#import <objc/runtime.h>

@implementation testMethod (Extension)

- (void)avoidCrashFunc{
    @try {
        NSLog(@"enter swizzle method ");
        [self avoidCrashFunc];//實質是調用交換方法
    }
    @catch (NSException *exception) {

        //能來到這里,說明可變數組添加元素的代碼有問題
        //你可以在這里進行相應的操作處理

        NSLog(@"異常名稱:%@   異常原因:%@",exception.name, exception.reason);
    }
    @finally {
        return;
        //在這里的代碼一定會執行,你也可以進行相應的操作
    }

}

@end

自定義swizzle 類別,頭文件:

#import <AppKit/AppKit.h>

#if TARGET_OS_OSX
#import <objc/runtime.h>
#import <objc/message.h>
#else
#import <objc/objc-class.h>
#endif


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (MethodSwizzlingCategory)



+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel;
+ (BOOL)swizzleClassMethod:(SEL)origSel withClassMethod:(SEL)altSel;


@end

NS_ASSUME_NONNULL_END

.m文件如下:


#import <AppKit/AppKit.h>


@implementation NSObject (MethodSwizzlingCategory)


 
+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel
{
    Method origMethod = class_getInstanceMethod(self, origSel);
    if (!origSel) {
        NSLog(@"original method %@ not found for class %@", NSStringFromSelector(origSel), [self class]);
        return NO;
    }
    
    Method altMethod = class_getInstanceMethod(self, altSel);
    if (!altMethod) {
        NSLog(@"original method %@ not found for class %@", NSStringFromSelector(altSel), [self class]);
        return NO;
    }
    
    class_addMethod(self,
                    origSel,
                    class_getMethodImplementation(self, origSel),
                    method_getTypeEncoding(origMethod));
    class_addMethod(self,
                    altSel,
                    class_getMethodImplementation(self, altSel),
                    method_getTypeEncoding(altMethod));
    
    method_exchangeImplementations(class_getInstanceMethod(self, origSel), class_getInstanceMethod(self, altSel));
 
    return YES;
}
 
+ (BOOL)swizzleClassMethod:(SEL)origSel withClassMethod:(SEL)altSel
{
    Class c = object_getClass((id)self);
    return [c swizzleMethod:origSel withMethod:altSel];
}

@end

測試代碼如下:

 testMethod *test = [[testMethod alloc]init];
 [testMethod swizzleMethod:@selector(testPointer) withMethod:@selector(avoidCrashFunc)];
 [test   testPointer];
 NSLog(@"========");
//結果:
2020-11-12 09:36:35.058873+0800 testCCC[49366:969253] Metal API Validation Enabled
2020-11-12 09:36:37.223973+0800 testCCC[49366:969253] enter swizzle method
2020-11-12 09:36:37.224026+0800 testCCC[49366:969253] enter normal class method
2020-11-12 09:36:37.224071+0800 testCCC[49366:969253] -[testMethod doSome:]: unrecognized selector sent to instance 0x6000024649f0
2020-11-12 09:36:37.224163+0800 testCCC[49366:969253] 異常名稱:NSInvalidArgumentException   異常原因:-[testMethod doSome:]: unrecognized selector sent to instance 0x6000024649f0
2020-11-12 09:36:37.224190+0800 testCCC[49366:969253] ========

前面testMethod中,只有測試二可以捕獲,其他都沒辦法,因此我們需要別的方法來抓取這些異常。

三、自定義獲取系統異常Signal和UncaughtExceptionHandler

這里主要講如何自己抓取異常消息,crash主要分signal和Exception相關。

  • 自定義獲取Signal


#import "SignalHandler.h"
#include <execinfo.h>
#import <AppKit/AppKit.h>
#import "sys/utsname.h"

NSMutableString *tempStr;
@implementation SignalHandler



+(void)saveCrash:(NSString *)exceptionInfo
{
    NSString * _libPath  = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"SigCrash"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
        [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    NSDate *date=[NSDate date];
    NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
    [dateformatter setDateFormat:@"YYYYMMdd-HHmmss"];
    
    NSString *dateString=[dateformatter stringFromDate:date];
    NSString * savePath = [_libPath stringByAppendingFormat:@"/Crash%@.log",dateString];
    NSLog(@"savePath :%@",savePath);
    
    exceptionInfo = [exceptionInfo stringByAppendingString:getAppInfo()];
    BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
    NSLog(@"YES sucess:%d",sucess);
}





void SignalExceptionHandler(int signal)
{
    //獲取thread棧信息
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Stack:\n"];
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    int i;
    for (i = 0; i <frames; ++i) {
        [mstr appendFormat:@"%s\n", strs[i]];
    }
    free(strs);
//    tempStr = mstr;
    [SignalHandler saveCrash:mstr];



}

void InstallSignalHandler(void)
{
    tempStr = [NSMutableString string];
    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);
//    [SignalHandler saveCrash:tempStr];

}


NSString* getAppInfo()
{
    struct utsname systemInfo;
    uname(&systemInfo);
    NSString *machine = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
    machine = [SignalHandler Devicemachine:machine];
    NSString *appInfo = [NSString stringWithFormat:@"App :%@ %@(%@)\nDevice : %@,\nDateTime:%@,\nOS Version: %@ (%@)",
                         [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"],
                         [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
                         [[NSBundle mainBundle] objectForInfoDictionaryKey:@"DTPlatformName"],
                         machine,
                         [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle],
                         [[NSBundle mainBundle] objectForInfoDictionaryKey:@"DTPlatformVersion"],
                         [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BuildMachineOSBuild"]
                         ];
    NSLog(@"Crash!!!! \n%@", appInfo);
    
    return appInfo;
    
}

+(NSString *)Devicemachine:(NSString *)machine
{
    if ([machine isEqualToString:@"iPhone1,1"]) return @"iPhone 2G (A1203)";
    
    if ([machine isEqualToString:@"iPhone1,2"]) return @"iPhone 3G (A1241/A1324)";
    
    if ([machine isEqualToString:@"iPhone2,1"]) return @"iPhone 3GS (A1303/A1325)";
    
    if ([machine isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
    
    if ([machine isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
    
    if ([machine isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
    
    if ([machine isEqualToString:@"iPhone4,1"]) return @"iPhone 4S (A1387/A1431)";
    
    if ([machine isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
    
    if ([machine isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
    
    if ([machine isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
    
    if ([machine isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
    
    if ([machine isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
    
    if ([machine isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
    
    if ([machine isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
    
    if ([machine isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
    
    if ([machine isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
    if ([machine isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
    
    if ([machine isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
    
    if ([machine isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
    
    if ([machine isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";
    
    if ([machine isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";
    
    if ([machine isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
    
    if ([machine isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
    
    if ([machine isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
    
    if ([machine isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
    
    if ([machine isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
    
    if ([machine isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
    
    if ([machine isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";
    
    if ([machine isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
    
    if ([machine isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
    
    if ([machine isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
    
    if ([machine isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
    
    if ([machine isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
    
    if ([machine isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";
    
    if ([machine isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
    
    if ([machine isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
    
    if ([machine isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
    
    if ([machine isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
    
    if ([machine isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
    
    if ([machine isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";
    
    if ([machine isEqualToString:@"i386"]) return @"iPhone Simulator";
    
    if ([machine isEqualToString:@"x86_64"]) return @"AMD64";
    
    
    return machine;

}
@end

測試代碼:

在AppDelegate添加注冊signal 方法,然后寫一個button 方法觸發crash case

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {

InstallSignalHandler();

在view controller里面新增button.

- (IBAction)Exception:(id)sender {
    ((char *)NULL)[1] = 0;
}
- (IBAction)Pointer:(id)sender {
    int *x = 0;
    *x = 200;   
}

不過這個方法有一個問題,它雖然能抓捕到異常,但是它似乎不會停(等了好久,一直循環dump生成crash 文件)....😂 不清楚原因,不過,這個方法只是探究,不是我最終想實現的。

//測試結果如下
2020-11-12 09:53:37.305849+0800 testCCC[50053:987325] savePath :/Users/xiaoqiang/Library/Caches/SigCrash/Crash20201112-095337.log
2020-11-12 09:53:37.305978+0800 testCCC[50053:987325] Crash!!!! 
App :testCCC 1.0(macosx)
Device : AMD64,
DateTime:2020/11/12, 9:53:37 AM,
OS Version: 10.15.6 (19F101)
2020-11-12 09:53:37.306382+0800 testCCC[50053:987325] YES sucess:1

//Crash log 內容如下:
Stack:
0   testCCC                             0x00000001046d8c6e SignalExceptionHandler + 107
1   libsystem_platform.dylib            0x00007fff6fc195fd _sigtramp + 29
2   ???                                 0x0000000000000000 0x0 + 0
3   CoreFoundation                      0x00007fff359ed89f __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
4   CoreFoundation                      0x00007fff359ed833 ___CFXRegistrationPost1_block_invoke + 63
5   CoreFoundation                      0x00007fff359ed7a8 _CFXRegistrationPost1 + 372
6   CoreFoundation                      0x00007fff359ed414 ___CFXNotificationPost_block_invoke + 80
7   CoreFoundation                      0x00007fff359bd58d -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1554
8   CoreFoundation                      0x00007fff359bca39 _CFXNotificationPost + 1351
9   Foundation                          0x00007fff38037786 -[NSNotificationCenter postNotificationName:object:userInfo:] + 59
10  AppKit                              0x00007fff32c75ce3 -[NSApplication _postDidFinishNotification] + 312
11  AppKit                              0x00007fff32c75a22 -[NSApplication _sendFinishLaunchingNotification] + 208
12  AppKit                              0x00007fff32c72ae3 -[NSApplication(NSAppleEventHandling) _handleAEOpenEvent:] + 549
13  AppKit                              0x00007fff32c72728 -[NSApplication(NSAppleEventHandling) _handleCoreEvent:withReplyEvent:] + 688
14  Foundation                          0x00007fff38062a26 -[NSAppleEventManager dispatchRawAppleEvent:withRawReply:handlerRefCon:] + 308
15  Foundation                          0x00007fff38062890 _NSAppleEventManagerGenericHandler + 98
16  AE                                  0x00007fff36d69203 _AppleEventsCheckInAppWithBlock + 18103
17  AE                                  0x00007fff36d68929 _AppleEventsCheckInAppWithBlock + 15837
18  AE                                  0x00007fff36d60bd7 aeProcessAppleEvent + 449
19  HIToolbox                           0x00007fff346367fa AEProcessAppleEvent + 54
20  AppKit                              0x00007fff32c6cac1 _DPSNextEvent + 1547
21  AppKit                              0x00007fff32c6b070 -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1352
22  AppKit                              0x00007fff32c5cd7e -[NSApplication run] + 658
23  AppKit                              0x00007fff32c2eb86 NSApplicationMain + 777
24  libdyld.dylib                       0x00007fff6fa20cc9 start + 1
App :testCCC 1.0(macosx)
Device : AMD64,
DateTime:2020/11/12, 9:53:37 AM,
OS Version: 10.15.6 (19F101)

  • 自定義捕獲UncaughtException

不過這個代碼我沒有測試,同signal一樣,在delegate添加函數 InstallUncaughtExceptionHandler();即可


#import "UncaughtExceptionHandler.h"


// 我的捕獲handler
static NSUncaughtExceptionHandler custom_exceptionHandler;
static NSUncaughtExceptionHandler *oldhandler;

@implementation UncaughtExceptionHandler

+(void)saveCreash:(NSString *)exceptionInfo
{
    NSString * _libPath  = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"OCCrash"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
        [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    NSDate* dat = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval a=[dat timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", a];
    
    NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];
    NSLog(@"Un savePath:%@",savePath);
    
    BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
    NSLog(@"YES sucess:%d",sucess);
}

// 注冊
void InstallUncaughtExceptionHandler(void)
{
    
    
    if(NSGetUncaughtExceptionHandler() != custom_exceptionHandler)
    oldhandler = NSGetUncaughtExceptionHandler();
    
    NSSetUncaughtExceptionHandler(&custom_exceptionHandler);
    
}

// 注冊回原有的
void Uninstall()
{
    NSSetUncaughtExceptionHandler(oldhandler);
}

void custom_exceptionHandler(NSException *exception)
{
    // 異常的堆棧信息
    NSArray *stackArray = [exception callStackSymbols];
    
    // 出現異常的原因
    NSString *reason = [exception reason];
    
    // 異常名稱
    NSString *name = [exception name];
    
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    
    NSLog(@"--->%@", exceptionInfo);

    [UncaughtExceptionHandler saveCreash:exceptionInfo];
    
    // 注冊回之前的handler
    Uninstall();
}
@end

上面的2個同時配合使用,一般來說是可以抓到預期的crash信息的,但是不推薦的原因是......

(singal那邊crash的時候,會一直無限循環dunp生成保存的crash log,想不明白,也有做嘗試修改,但是不行)望有緣人幫忙解惑,我也是小白白阿

鑒於此,接下來還要在引入一個自動收集crash的庫,plcrashreporter

測試過了,比較喜歡。

Plcrashreporter

前面總結了一些常見的crash 捕獲,推薦下面的一個文章,寫的很好,一定要看看。

Baymax:網易iOS App運行時Crash自動防護實踐

一般來說,mac上面app的crash log產生在(其他的os布吉島,沒接觸過):

~/Library/Logs/DiagnosticReports

可是有時候,我們不方便去拿,於是就想說有沒有辦法做到自動收集crash,首先看到推薦的是幾個庫,這里只研究了一個plcrashreporter.

庫相關文件獲取地址

這個庫初衷很直觀,通過收集crash信息打包log 自定義方式回傳給你,使用起來也很簡單方便。

流程是:

-->注冊聲明該方法-->App crash-->再次打開app的時候會直接調用該方法進行抓取保存crash log

看到有對plc源碼分析的一個文章,也挺好的,有興趣可以去看看

關於PLCrashreporter源碼分析

測試實例

新建一個類別CrashRep:

head文件就是單純的聲明一個方法而已:

void enable_crash_reporter_service (void);

.m文件如下:


#import "CrashRep.h"
#import "PLCrashReporter.h"
#import "PLCrashReport.h"
#import "PLCrashReportTextFormatter.h"

#import <sys/types.h>
#import <sys/sysctl.h>

@implementation CrashRep

/*
 * On iOS 6.x, when using Xcode 4, returning *immediately* from main()
 * while a debugger is attached will cause an immediate launchd respawn of the
 * application without the debugger enabled.
 *
 * This is not documented anywhere, and certainly occurs entirely by accident.
 * That said, it's enormously useful when performing integration tests on signal/exception
 * handlers, as it means we can use the standard Xcode build+run functionality without having
 * the debugger catch our signals (thus requiring that we manually relaunch the app after it has
 * installed).
 *
 * This may break at any point in the future, in which case we can remove it and go back
 * to the old, annoying, and slow approach of manually relaunching the application. Or,
 * perhaps Apple will bless us with the ability to run applications without the debugger
 * enabled.
 */
static bool debugger_should_exit (void) {
#if !TARGET_OS_OSX
    return false;
#endif
    
    struct kinfo_proc info;
    size_t info_size = sizeof(info);
    int name[4];
    
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
    
    if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) {
        NSLog(@"sysctl() failed: %s", strerror(errno));
        return false;
    }
    
    if ((info.kp_proc.p_flag & P_TRACED) != 0)
    return true;
    
    return false;
}

// APP啟動將crash日志保存到新目錄,並設置為iTunes共享
static void save_crash_report (PLCrashReporter *reporter) {
//    if (![reporter hasPendingCrashReport])
//        NSLog(@"no crash");
//        return;
    
#if TARGET_OS_OSX
    NSFileManager *fm = [NSFileManager defaultManager];
    NSError *error;
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSLog(@"path:%@",paths);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    if (![fm createDirectoryAtPath: documentsDirectory withIntermediateDirectories: YES attributes:nil error: &error]) {
        NSLog(@"Could not create documents directory: %@", error);
        return;
    }
    
    
    NSData *data = [reporter loadPendingCrashReportDataAndReturnError: &error];
    if (data == nil) {
        NSLog(@"Failed to load crash report data: %@", error);
        return;
    }
    
    NSString *outputPath = [documentsDirectory stringByAppendingPathComponent: @"demo.plcrash"];
    if (![data writeToFile: outputPath atomically: YES]) {
        NSLog(@"Failed to write crash report");
    }
    
    NSLog(@"Saved crash report to: %@", outputPath);
#endif
}

// 將plcrash格式的日志解析成log
static void analysis_crashTolog (PLCrashReporter *reporter) {
    NSError *outError;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
//    NSString *outputPath = [documentsDirectory stringByAppendingPathComponent: @"demo.plcrash"];
    NSData *data = [reporter loadPendingCrashReportDataAndReturnError: &outError];
    if (data == nil) {
        NSLog(@"Failed to load crash report data: %@", outError);
        return;
    }
    
//  利用generateLiveReport 獲得當前stack 調用的信息.
//    NSData *lagData = [reporter generateLiveReport];
//    PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
//    NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//    //將字符串上傳服務器
//    NSLog(@"lag happen, detail below: \n %@",lagReportString);
//    NSLog(@"Crashed on %@", lagReport.systemInfo.timestamp);
//    NSLog(@"PLCrashReport  %@", lagReport);
////exceptionInfo exceptionName exceptionReason stackFrames
//    NSLog(@"exceptionInfo  %@", lagReport.exceptionInfo);
//    NSLog(@"exceptionInfo.name :%@,exceptionInfo.reason :%@, exceptionInfo.name : %@",lagReport.exceptionInfo.exceptionName, lagReport.exceptionInfo.exceptionReason,lagReport.exceptionInfo.stackFrames);
//
//
    
//    NSData *data = [NSData dataWithContentsOfFile:outputPath];
    PLCrashReport *report = [[PLCrashReport alloc] initWithData: data error: &outError];
    NSLog(@"Crashed on %@", report.systemInfo.timestamp);
    NSLog(@"PLCrashReport  %@", report);
//exceptionInfo exceptionName exceptionReason stackFrames
    NSLog(@"machExceptionInfo  %@", report.machExceptionInfo);
    NSLog(@"machExceptionInfo.codes :%@,exceptionInfo.reason :%llu",report.machExceptionInfo.codes, report.machExceptionInfo.type);



    NSLog(@"Crashed with signal %@ (code %@, address=0x%" PRIx64 ")", report.signalInfo.name,
              report.signalInfo.code, report.signalInfo.address);
    if (report){
        NSString *text = [PLCrashReportTextFormatter stringValueForCrashReport: report
                                                                withTextFormat: PLCrashReportTextFormatiOS];
        NSString *logPath = [documentsDirectory stringByAppendingString:@"/crash.log"];
        [text writeToFile:logPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }
//    [report purgePendingCrashReport];
    

}

/* A custom post-crash callback */
static void post_crash_callback (siginfo_t *info, ucontext_t *uap, void *context) {
    // this is not async-safe, but this is a test implementation
   
    NSLog(@"post crash callback: signo=%d, uap=%p, context=%p", info->si_signo, uap, context);
}

void enable_crash_reporter_service ()
{
    NSError *error = nil;
    
    if (!debugger_should_exit()) {
// Configure our reporter
        NSLog(@"not debug");
//
//        NSData *lagData = [[[PLCrashReporter alloc]
//                              initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
//        PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
//        NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//        //將字符串上傳服務器
//        NSLog(@"lag happen, detail below: \n %@",lagReportString);
        //PLCrashReporterSignalHandlerTypeMach
        
        
        PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType: PLCrashReporterSignalHandlerTypeMach
                                                                           symbolicationStrategy: PLCrashReporterSymbolicationStrategyAll] ;
        PLCrashReporter *reporter = [[PLCrashReporter alloc] initWithConfiguration: config];
    
        
        // APP啟動將crash日志保存到新目錄
        // 如果做了解析 這步可以省略
//        save_crash_report(reporter);
        
        // 解析
        analysis_crashTolog(reporter);
        
        
        //設置回調函數,這里可以自定義想要獲取的東西
//        /* Set up post-crash callbacks */
//        PLCrashReporterCallbacks cb = {
//            .version = 0,
//            .context = (void *) 0xABABABAB,
//            .handleSignal = post_crash_callback
//        };
//        [reporter setCrashCallbacks: &cb];
//
        // TODO 發送。。
        
        /* Enable the crash reporter */
        if (![reporter enableCrashReporterAndReturnError: &error]) {
            NSLog(@"Could not enable crash reporter: %@", error);
        }
        [reporter purgePendingCrashReport];
        
    }

}

@end

在AppDelegate導入CrashRep.h,在下面引入該方法

一樣在view controller 自定義button觸發crash case,crash觸發的例子參考前面的try catch。

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    enable_crash_reporter_service();

crash后再次打開app,crash log會在自定義路徑這里是document產生,里面的信息能夠讓我們看到問題在哪。

ersion:         1.0 (1)
Code Type:       X86-64
Parent Process:  Xcode [9184]

Date/Time:       2020-11-12 02:26:31 +0000
OS Version:      Mac OS X 10.15.5 (19F101)
Report Version:  104

Exception Type:  SIGILL
Exception Codes: ILL_NOOP at 0x0
Crashed Thread:  0

Thread 0 Crashed:
0   testCCC                             0x00000001016e125e -[ViewController Pointer:] + 13
1   AppKit                              0x00007fff32eaefc7 -[NSApplication sendAction:to:from:] + 299
2   AppKit                              0x00007fff32eaee62 -[NSControl sendAction:to:] + 86
3   AppKit                              0x00007fff32eaed94 __26-[NSCell _sendActionFrom:]_block_invoke + 136
4   AppKit                              0x00007fff32eaec96 -[NSCell _sendActionFrom:] + 171
5   AppKit                              0x00007fff32eaebdd -[NSButtonCell _sendActionFrom:] + 96
6   AppKit                              0x00007fff32eaaebb NSControlTrackMouse + 1745
7   AppKit                              0x00007fff32eaa7c2 -[NSCell trackMouse:inRect:ofView:untilMouseUp:] + 130
8   AppKit                              0x00007fff32eaa681 -[NSButtonCell trackMouse:inRect:ofView:untilMouseUp:] + 691
9   AppKit                              0x00007fff32ea99fd -[NSControl mouseDown:] + 748
10  AppKit                              0x00007fff32ea7e10 -[NSWindow _handleMouseDownEvent:isDelayedEvent:] + 4914
11  AppKit                              0x00007fff32e12611 -[NSWindow _reallySendEvent:isDelayedEvent:] + 2612
12  AppKit                              0x00007fff32e119b9 -[NSWindow sendEvent:] + 349
13  AppKit                              0x00007fff32e0fd44 -[NSApplication sendEvent:] + 352
14  AppKit                              0x00007fff32c5cdaf -[NSApplication run] + 707
15  AppKit                              0x00007fff32c2eb86 NSApplicationMain + 777
16  libdyld.dylib                       0x00007fff6fa20cc9 start + 1

拓展--DSYM 分析crash log

不過有一點,可以發現,我們看不到代碼出現問題在第幾行,其實不是很必須,但是有方法可以看到。

DSYM 地址

什么是 dSYM 文件

Xcode編譯項目后,我們會看到一個同名的 dSYM 文件,dSYM 是保存 16 進制函數地址映射信息的中轉文件,我們調試的 symbols 都會包含在這個文件中,並且每次編譯項目的時候都會生成一個新的 dSYM 文件,位於 /Users/<用戶名>/Library/Developer/Xcode/Archives 目錄下,對於每一個發布版本我們都很有必要保存對應的 Archives 文件 ( AUTOMATICALLY SAVE THE DSYM FILES 這篇文章介紹了通過腳本每次編譯后都自動保存 dSYM 文件)。

-->不過我配置之后產生的dSYM文件是在xcode project build folder下面..@@

dSYM 文件有什么作用

當我們軟件 release 模式打包或上線后,不會像我們在 Xcode 中那樣直觀的看到用崩潰的錯誤,這個時候我們就需要分析 crash report 文件了,iOS 設備中會有日志文件保存我們每個應用出錯的函數內存地址,通過 Xcode 的 Organizer 可以將 iOS 設備中的 DeviceLog 導出成 crash 文件,這個時候我們就可以通過出錯的函數地址去查詢 dSYM 文件中程序對應的函數名和文件名。大前提是我們需要有軟件版本對應的 dSYM 文件,這也是為什么我們很有必要保存每個發布版本的 Archives 文件了。

如何將文件一一對應

每一個 xx.app 和 xx.app.dSYM 文件都有對應的 UUID,crash 文件也有自己的 UUID,只要這三個文件的 UUID 一致,我們就可以通過他們解析出正確的錯誤函數信息了。

1.查看 xx.app 文件的 UUID,terminal 中輸入命令 :

dwarfdump --uuid xx.app/xx (xx代表你的項目名)

2.查看 xx.app.dSYM 文件的 UUID ,在 terminal 中輸入命令:
dwarfdump --uuid xx.app.dSYM 

3.crash 文件內 Binary Images: 下面一行中 <> 內的 e86bcc8875b230279c962186b80b466d  就是該 crash 文件的 UUID,而第一個地址 0x1000ac000 便是 slide address:
Binary Images:
0x1000ac000 - 0x100c13fff Example arm64  <e86bcc8875b230279c962186b80b466d> /var/containers/Bundle/Application/99EE6ECE-4CEA-4ADD-AE8D-C4B498886D22/Example.app/Example
如何在自己的項目生成dSYM文件

網上搜到的較多解決方法是如下配置
XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File

配置完之后打包發現還是沒有的話,上面的配置修改之后還有一個地方注意一下

XCode -> Build Settings -> Apple Clang - Code Generation -> Generate Debug Symbols -> Yes

如何通過dSYM tool來分析定位問題的具體行

首先需要在官網中下載一個tool,鏈接在上面有寫。

拿到源碼后build一下tool即可使用,界面如下:

需要的東西:

  • 拖拽App的dSYM文件到上面頁面,選擇右邊的CPU類型
  • crash文件的UUID, 在crash log里面,crash 文件內 Binary Images: 下面一行中 <> 內的 e86bcc8875b230279c962186b80b466d 就是該 crash 文件的 UUID
  • slide address, Binary Images: 第一個地址 0x1000ac000 便是 slide address
  • 錯誤信息內存地址,即為crash 地方的地址,如下為0x000000010cc0dfc1
  • 偏移地, 如下為4
Date/Time:       2020-11-12 02:31:07 +0000
OS Version:      Mac OS X 10.15.5 (19F101)
Report Version:  104

Exception Type:  SIGSEGV
Exception Codes: SEGV_MAPERR at 0x1
Crashed Thread:  0

Thread 0 Crashed:
0   testCCC                             0x000000010cc0dfc1 -[ViewController Exception:] + 4

最后,就可以看到crash具體對應的行數了,這些只是輔佐幫助分析定位問題,關於crash,其實牽扯到的知識太多啦。

對於分析定位問題而言,這篇文章就到這里吧....


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM