当前位置:   article > 正文

IOS —— Crash分析_ios开发--crash分析,让崩溃应用起死回生

ios开发--crash分析,让崩溃应用起死回生

1. Crash

应用崩溃是影响 APP 体验的重要一环, 而崩溃定位也常常让开发者头疼。Crash的出现就是做了一些违背代码规则的操作,常见crash类型有:

  • 容器越界
  • 使用未初始化的变量
  • 用户授权问题
  • 选择器方法未定义
  • 子线程刷新ui
  • KVO
  • 数据类型不匹配
  • 内存溢出
  • 野指针
  • 死循环
    那么该如何高效处理Crash呢?

2. 奔溃处理

2.1 选择器方法未定义

下面这个代码明显的会崩溃,因为没有logicEdu方法。

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
- (IBAction)btnAction:(UIButton *)sender {
    [sender performSelector:@selector(logicEdu:)];
}
@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

那么添加下面这个方法之后还是崩溃。

- (void) logicEdu:(UIButton *)sender {
    
}
  • 1
  • 2
  • 3

首先看一下堆栈。这里exception_throw是异常抛出的意思,而抛出异常的地方是+[NSObject(NSObject) instanceMethodSignatureForSelector:] 也就是慢速消息转发里面。消息发送中会通过对象的isa找到类去类的方法列表里面寻找selector,如果没有找到则去类的父类里面查找,直到找到NSObject还没有找到的话就会进入消息转发流程,
在这里插入图片描述
消息转发流程里面有三个补救的机会:

  • 动态方法决议(添加方法)
  • 快速消息转发(转发给另一个对象来处理)
  • 慢速消息转发(动态签名签名一个sel)
    如果这三个都没有处理,那么就会崩溃。
    这里来创建一个NSObject分类来进行快速消息转发 -forwardingTargetForSelector;
@implementation NSObject (method)
- (id)forwardingTargetForSelector:(SEL)aSelector {
    id result = [self forwardingTargetForSelector:aSelector];
    return result;
}
@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里会报警告,因为分类方法名字和主类的方法名字一样,这时候就需要用到method-Swizzing进行imp交换。这里运行后发现还是崩溃的。method-Swizzing一般是在load里面实现,load在main函数之前就会被调用,并且是主动调用,但是在load写函数会影响启动速度,应该尽量不要在load中写耗时的操作。并且类和分类是懒加载的,但是如果实现了load,那么类的加载就会提前到main函数之前。影响启动速度的原因是会在底层调用更多的方法比如attchCategory等。 有的把方法交换写到initialize里面,这也是可以的,因为initialize是第一次调用方法的时候被调用的。但是不建议在initialize里面进行方法交换,因为可能不会调用方法。

+ (void)load {
 
    Method originalMethod = class_getInstanceMethod(self, @selector(forwardingTargetForSelector:));
    Method swizzleMethod = class_getInstanceMethod(self, @selector(LSForwardingTargetForSelector:));
    method_exchangeImplementations(originalMethod, swizzleMethod);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后创建一个ForwardingTarget类,来重写消息转发方法。

id newDynamicMethod(id self,SEL _cmd){
    return [NSNull null];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    class_addMethod(self.class, sel, (IMP) newDynamicMethod, "@@:");
    [super resolveInstanceMethod:sel];
    return YES;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    id result = [super forwardingTargetForSelector:aSelector];
    return result;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    id result = [super methodSignatureForSelector:aSelector];
    return result;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    [super forwardInvocation:anInvocation];
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在分类中添加静态变量target然后在load里面初始化。

static ForwardingTarget *target = nil;
  • 1

这样运行后点击button就不会崩溃了,这里还可以在newDynamicMethod里面打印crash的sel。

id newDynamicMethod(id self,SEL _cmd){
    NSLog(@"%@",NSStringFromSelector(_cmd));
    return [NSNull null];
}
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

2.2 容器越界

下面这个代码运行后点击button会造成容器越界的崩溃。

@interface ViewController ()
{
    NSArray *dataArr;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    dataArr = @[@"第1个",@"第2个",@"第3个",@"第4个"];
}
- (IBAction)btnAction:(UIButton *)sender {
    NSLog(@"%@" ,dataArr[4]);
}


@end

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

那么这里如何处理容器越界的崩溃呢?
这里需要写一个NSArray的分类,然后还是使用method-Swizzing来进行处理。看到堆栈里面是objectAtIndexedSubscript报错的,所以需要重写objectAtIndexedSubscript方法。
在这里插入图片描述
下面这个方法其实是不正确的,这里还需要调用class_addMethod来查看是否有objectAtIndexedSubscript方法了。

@implementation NSArray (LSArray)
+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
    Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
    method_exchangeImplementations(originalMethod, swizzleMethod);
    
}

- (id)lsobjectAtIndexedSubscript:(NSUInteger)idx{
    if(idx < self.count) {
                 return [self lsobjectAtIndexedSubscript:idx];
    }
    NSLog(@"越界了%lu >= %lu",idx,self.count);
    return nil;
}
@end

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里用didAddMethod判断是不是有了objectAtIndexedSubscript方法,如果有了则替换方法,如果没有就交换imp。

+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
    Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
    
    bool didAddMethod = class_addMethod(self, @selector(objectAtIndexedSubscript:), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    if (didAddMethod) {
        class_replaceMethod(self, @selector(lsobjectAtIndexedSubscript:),method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里其实还有问题,需要加一个单例来确保只交换一次。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
        Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
        
        bool didAddMethod = class_addMethod(self, @selector(objectAtIndexedSubscript:), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(lsobjectAtIndexedSubscript:),method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzleMethod);
        }
    });
  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

但是这里运行后发现还是崩溃,这是为什么呢?方法的本质是消息,消息包含接受者和消息的主体(SEl和参数),这里的self是NSArray,但是奔溃信息里面的是__NSArrayI,所以需要把originalMethod里面的class换一下

Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
  • 1

这样就没有数据越界的问题了。当然,NSArray其他的类簇也需要这样的处理,比如可变数组__NSArrayM, 不可变空数组__NSArray0等。

2.3 NSSetUncaughtExceptionHandler

crash种类很多,那么有没有办法可以捕获所有的crash呢?苹果提供了一个API. NSSetUncaughtExceptionHandler。

写一个ExceptionHandler类,并添加类方法installUncaughtExceptionHandler,然后在这个方法里面调用NSSetUncaughtExceptionHandler。

+ (void)installUncaughtExceptionHandler {
    NSSetUncaughtExceptionHandler(&lsExceptionHandlers);
}

  • 1
  • 2
  • 3
  • 4

NSSetUncaughtExceptionHandler里面的参数是一个c函数。


void lsExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);
    
    int32_t exceptionCount = atomic_fetch_add_explicit(&LGUncaughtExceptionCount,1,memory_order_relaxed);
    if (exceptionCount > LGUncaughtExceptionMaximum){
        return;
    }
    NSArray *callStack = [ExceptionHandler lsBackTrace];
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
    [userInfo setObject:exception.name forKey:LGUncaughtExceptionHandlerSignalExceptionName];
    [userInfo setObject:exception.reason forKey:LGUncaughtExceptionHandlerSignalExceptionReason];
    [userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
    [userInfo setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolKey];
    [userInfo setObject:@"LSException" forKey:LGUncaughtExceptionHandlerFileKey];
     [[[ExceptionHandler alloc] init]   performSelectorOnMainThread:@selector(ls_handleException:)  withObject:[NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo] waitUntilDone:YES];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

然后在AppDelegate里面的didFinishLaunchingWithOptions调用这个类方法,确保足够早的收集到crash信息。

[ExceptionHandler  installUncaughtExceptionHandler];
  • 1

那么现在如果程序奔溃的话,那么就会到lsExceptionHandlers里面,这里面可以根据获得的exception来进行崩溃的记录,然后上传到服务器。
这里运行后点击Button会有数组越界的崩溃。看到保存的崩溃的地址,然后去到这个地址。
在这里插入图片描述
打开日志文件,看到崩溃信息保存了下来,这样就可以把这个文件传到服务器了。
在这里插入图片描述
接下来去看崩溃的堆栈。看到这里有个函数_objc_terminate。
在这里插入图片描述
在源码中搜索_objc_terminate,发现实现如下:
在这里插入图片描述

之前的文章中iOS 底层探索篇 —— dyld加载流程(上)写到_objc_init也调用了exception_init方法,那么exception_init对异常进行了什么处理呢?

看到这里调用了set方法,这里就是说,这个terminate会一直在跑,如果发生了异常,就会调用_objc_terminate回调函数。所以_objc_init就做了异常的回调处理。

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}
  • 1
  • 2
  • 3
  • 4

回到_objc_terminate,看到上面写的如果是objc object 就会调用uncaught_handler,那么就会调用uncaught_handler方法,那么就寻找uncaught_handler,方法进行了赋值。

static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
  • 1

往下看还有对uncaught_handler赋值的地方,但是搜索确没有地方调用它,所以是上层的API。

objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

看到NSSetUncaughtExceptionHandler和objc_setUncaughtExceptionHandler名字非常相似,这里就知道NSSetUncaughtExceptionHandler是对objc_setUncaughtExceptionHandler的封装。之前的调用 (*uncaught_handler)((id)e), 也就是调用外面传进来的回调函数,所以lsExceptionHandlers方法里面也有一个NSException。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/125038
推荐阅读
相关标签
  

闽ICP备14008679号