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,其实牵扯到的知识太多啦。

对于分析定位问题而言,这篇文章就到这里吧....

posted @ 2020-11-12 10:54  萧蔷ink  阅读(2676)  评论(0编辑  收藏  举报