iOS Crash收集与分析详解和防护处理

前言

  • Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;另一种是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃
  • 本文主要介绍APP运行时第二种Crash自动防护功能,暂时涵盖容器,字符串,没找到对应的函数,后台返回NSNull导致的崩溃,计时器,kvo,不在主线程中更新UI等崩溃
  • 有空再来慢慢补充其他类型

Demo地址:KJExceptionDemo

设计原理

利用 Objective-C 语言的动态特性,采用AOP面向切面编程的设计思想,交换方法然后拦截处理崩溃信息


image
方法 功能
class_getInstanceMethod 获取实例方法
class_getClassMethod 获取类方法
method_getImplementation 获取一个方法的实现
method_setImplementation 设置一个方法的实现
method_getTypeEncoding 获取方法实现的编码类型
class_addMethod 添加方法实现
class_replaceMethod 用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP
method_exchangeImplementations 交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP

交换实例方法

void kExceptionMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getInstanceMethod(clazz, original);
    Method swizzledMethod = class_getInstanceMethod(clazz, swizzled);
    if (class_addMethod(clazz, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(clazz, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

交换类方法

void kExceptionClassMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getClassMethod(clazz, original);
    Method swizzledMethod = class_getClassMethod(clazz, swizzled);
    Class metaclass = objc_getMetaClass(NSStringFromClass(clazz).UTF8String);
    if (class_addMethod(metaclass, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(metaclass, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

常见Crash和解决方案

一、没找到对应的函数 - Unrecognized Selector Sent to Instance

类型 原因
SEL unrecognized selector sent to instance .h定义但.m没实现
SEL performSelector: 调用不存在的方法
SEL delegate 回调前没有判空而是直接调用
SEL id 类型没有判断类型,强行调用真实类型不存在的方法
SEL copy 修饰的可变的字符串 \ 字典 \ 数组 \ 集合 \ Data,调用可变的方法

消息转发流程图:

image.png

解决方案:

  • 交换方法methodSignatureForSelector:forwardInvocation:
  • 对象调用方法经过三个阶段
    1. 消息发送:查询cache和方法列表,找到了直接调用,找不到方法会进入下个阶段
    2. 动态解析: 调用实例方法resolveInstanceMethod或类方法resolveClassMethod里面可以有一次动态添加方法的机会
    3. 消息转发:首先会判断是否有其他对象可以处理方法forwardingTargetForSelector返回一个新的对象,如果没有新的对象进行处理,会调用methodSignatureForSelector方法返回方法签名,然后调用forwardInvocation
  • 选择在消息转发的最后一步来做处理,methodSignatureForSelector:消息获得函数的参数和返回值,然后[self respondsToSelector:aSelector]判断是否有该方法,如果没有返回函数签名,创建一个NSInvocation对象并发送给forwardInvocation

二、容器越界 - 数组和字典

类型 原因
NSArray 数组索引越界、插入空对象
NSDictionary key、value 为空

备注:可变的都继承自不可变的,所有可变的分类中,重复的方法就不用再次替换

解决方案:

  • 交换方法,然后防护处理,简单举个例子,NSArray 是一个类簇,它真正的类型是__NSArrayI,交换方法如下
Class __NSArrayI = objc_getClass("__NSArrayI");
/// 越界崩溃方式一:[array objectAtIndex:0];
kExceptionMethodSwizzling(__NSArrayI, @selector(objectAtIndex:), @selector(kj_objectAtIndex:));
/// 越界崩溃方式二:array[0];
kExceptionMethodSwizzling(__NSArrayI, @selector(objectAtIndexedSubscript:), @selector(kj_objectAtIndexedSubscript:));

交换后的处理

- (instancetype)kj_objectAtIndex:(NSUInteger)index{
    NSArray *temp = nil;
    @try {
        temp = [self kj_objectAtIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @"🍉🍉 crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}

三、KVO

类型 原因
KVO 添加了监听,没有移除

解决方案:

  • 交换removeObserver:forKeyPath:方法,
- (void)kj_removeObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath{
    @try {
        [self kj_removeObserver:observer forKeyPath:keyPath];
    }@catch (NSException *exception) {
        NSString *string = @"🍉🍉 crash:添加观察者后没有移除观察者导致";
        [KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
    }@finally {
        
    }
}

四、NSTimer

类型 原因
NSTimer 没有 invalidate,直接销毁
NSTimer NStimer 与 target 强引用,内存泄漏

解决方案:

  • 交换scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法
  • 定义一个抽象类KJProxyProtector,NSTimer实例强引用抽象类,而在抽象类中弱引用target,这样target和NSTimer之间的关系也就是弱引用,意味着target可以自由的释放,从而解决循环引用的问题

五、后台返回NSNull导致的崩溃

类型 原因
NSNull 后台返回NSNull导致的崩溃

解决方案:

  • 交换方法methodSignatureForSelector:forwardInvocation:

六、UIKit Called on Non-Main Thread

解决方案:

  • 交换setNeedsLayoutlayoutIfNeededlayoutSubviewssetNeedsUpdateConstraints方法
  • [NSThread isMainThread]判断当前线程是否为主线程,如果不是则在主线程执行

异常收集

一、防护类型

目前提供以下七种

typedef NS_OPTIONS(NSInteger, KJCrashProtectorType) {
    KJCrashProtectorTypeContainer = 1 << 0,// 数组和字典
    KJCrashProtectorTypeString = 1 << 1,// 字符串
    KJCrashProtectorTypeUnrecognizedSelector = 1 << 2,// 没找到对应的函数
    KJCrashProtectorTypeNSNull = 1 << 3,// 后台返回NSNull导致的崩溃
    KJCrashProtectorTypeTimer = 1 << 4,// 计时器
    KJCrashProtectorTypeKVO = 1 << 5,// kvo
    KJCrashProtectorTypeUINonMain = 1 << 6,// 不在主线程中刷新UI
};

二、开启防护

采用多枚举方式,来快速设置需要开发的防护

/// 开启全部防护
+ (void)kj_openAllCrashProtectorManager:(kExceptionBlock)block{
    if (block) [KJCrashManager kj_crashBlock:block];
    [self kj_openCrashProtectorType:
     KJCrashProtectorTypeContainer |
     KJCrashProtectorTypeString |
     KJCrashProtectorTypeUnrecognizedSelector |
     KJCrashProtectorTypeNSNull |
     KJCrashProtectorTypeTimer |
     KJCrashProtectorTypeKVO];
}
/// 开启指定类型防护
+ (void)kj_openCrashProtectorType:(KJCrashProtectorType)type{
    if (type & KJCrashProtectorTypeContainer) {
        [NSArray kj_openCrashExchangeMethod];
        [NSMutableArray kj_openCrashExchangeMethod];
        [NSDictionary kj_openCrashExchangeMethod];
        [NSMutableDictionary kj_openCrashExchangeMethod];
    }
    if (type & KJCrashProtectorTypeString) {
        [NSString kj_openCrashExchangeMethod];
        [NSMutableString kj_openCrashExchangeMethod];
        [NSAttributedString kj_openCrashExchangeMethod];
        [NSMutableAttributedString kj_openCrashExchangeMethod];
    }
    if (type & KJCrashProtectorTypeUnrecognizedSelector) {
        [NSObject kj_openUnrecognizedSelectorExchangeMethod];
    }
    if (type & KJCrashProtectorTypeNSNull) {
        [NSNull kj_openNullExchangeMethod];
    }
    if (type & KJCrashProtectorTypeTimer) {
        [NSTimer kj_openCrashExchangeMethod];
    }
    if (type & KJCrashProtectorTypeKVO) {
        [NSObject kj_openKVOExchangeMethod];
    }
    if (type & KJCrashProtectorTypeUINonMain) {
        [UIView kj_openCrashExchangeMethod];
    }
}

三、解析异常消息

采用正则表达式来匹配出来方法名

/// 解析异常消息
+ (NSString*)kj_analysisCallStackSymbols:(NSArray<NSString*>*)callStackSymbols{
    __block NSString *msg = nil;
    NSString *pattern = @"[-\\+]\\[.+\\]";// 匹配出来的格式为 +[类名 方法名] 或者 -[类名 方法名]
    NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    for (NSInteger i = 2; i < callStackSymbols.count; i++) {
        NSString *matchesString = callStackSymbols[i];
        [regularExp enumerateMatchesInString:matchesString options:NSMatchingReportProgress range:NSMakeRange(0, matchesString.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
            if (result) {
                NSString *tempMsg = [matchesString substringWithRange:result.range];
                NSString *className = [tempMsg componentsSeparatedByString:@" "].firstObject;
                className = [className componentsSeparatedByString:@"["].lastObject;
                if (![className hasSuffix:@")"] && [NSBundle bundleForClass:NSClassFromString(className)] == [NSBundle mainBundle]) {
                    msg = tempMsg;
                }
                *stop = YES;
            }
        }];
        if (msg.length) break;
    }
    return msg;
}

熟悉又讨厌的崩溃

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]'
*** First throw call stack:(
    0   CoreFoundation                      0x0000000103dca126 __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x0000000103c54f78 objc_exception_throw + 48
    2   CoreFoundation                      0x0000000103e46cdb _CFThrowFormattedException + 194
    3   CoreFoundation                      0x0000000103e5221e -[__NSPlaceholderDictionary initWithCapacity:].cold.1 + 0
    4   CoreFoundation                      0x0000000103e351f7 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 227
    5   CoreFoundation                      0x0000000103dc8da3 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 49
    6   KJExtensionHandler                  0x00000001033b715f -[ViewController viewDidLoad] + 815
    7   UIKitCore                           0x000000010d7ac73b -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 88
    8   UIKitCore                           0x000000010d7b1022 -[UIViewController loadViewIfRequired] + 1084
    9   UIKitCore                           0x000000010d6e800e -[UINavigationController _updateScrollViewFromViewController:toViewController:] + 162
    10  UIKitCore                           0x000000010d6e82f8 -[UINavigationController _startTransition:fromViewController:toViewController:] + 154
    11  UIKitCore                           0x000000010d6e9371 -[UINavigationController _startDeferredTransitionIfNeeded:] + 851
    12  UIKitCore                           0x000000010d6ea6dc -[UINavigationController __viewWillLayoutSubviews] + 150
    13  UIKitCore                           0x000000010d6caf1e -[UILayoutContainerView layoutSubviews] + 217
    14  UIKitCore                           0x000000010e43d9ce -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2874
    15  QuartzCore                          0x0000000105546d87 -[CALayer layoutSublayers] + 258
    16  QuartzCore                          0x000000010554d239 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 575
    17  QuartzCore                          0x0000000105558f91 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 65
    18  QuartzCore                          0x0000000105499078 _ZN2CA7Context18commit_transactionEPNS_11TransactionEdPd + 496
    19  QuartzCore                          0x00000001054cfe13 _ZN2CA11Transaction6commitEv + 783
    20  UIKitCore                           0x000000010defe27a __34-[UIApplication _firstCommitBlock]_block_invoke_2 + 81
    21  CoreFoundation                      0x0000000103d385db __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    22  CoreFoundation                      0x0000000103d379ef __CFRunLoopDoBlocks + 434
    23  CoreFoundation                      0x0000000103d3240c __CFRunLoopRun + 899
    24  CoreFoundation                      0x0000000103d31b9e CFRunLoopRunSpecific + 567
    25  GraphicsServices                    0x000000010c7ebdb3 GSEventRunModal + 139
    26  UIKitCore                           0x000000010dee0af3 -[UIApplication _run] + 912
    27  UIKitCore                           0x000000010dee5a04 UIApplicationMain + 101
    28  KJExtensionHandler                  0x00000001033ea92a main + 122
    29  libdyld.dylib                       0x00000001065eb415 start + 1
    30  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

防崩处理之后的效果

2020-12-29 15:49:27.649011+0800 KJExceptionDemo[7987:427289] 
************ crash 日志 ************
标题:🍉🍉 crash:ViewController 类出现未找到类方法
异常原因:test_UnrecognizedSelector 🚗🚗类方法未找到🚗🚗
异常地址:-[ViewController testUnrecognizedSelector]
2020-12-29 15:49:27.651701+0800 KJExceptionDemo[7987:427289] 
************ crash 日志 ************
标题:🍉🍉 crash:ViewController 类出现未找到实例方法
异常原因:testCrash:xx: 🚗🚗实例方法未找到🚗🚗
异常地址:-[ViewController testUnrecognizedSelector]
2020-12-29 15:49:27.654808+0800 KJExceptionDemo[7987:427289] 
************ crash 日志 ************
标题:🍉🍉 crash:数组插入数据为空
异常原因:*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil
异常地址:-[ViewController testContainer]
2020-12-29 15:49:27.657423+0800 KJExceptionDemo[7987:427289] 
************ crash 日志 ************
标题:🍉🍉 crash:数组更改索引越界
异常原因:*** -[__NSArrayM setObject:atIndexedSubscript:]: index 4 beyond bounds [0 .. 2]
异常地址:-[ViewController testContainer]
2020-12-29 15:49:27.661423+0800 KJExceptionDemo[7987:427289]
************ crash 日志 ************
标题:🍉🍉 crash:字符串长度不够
异常原因:*** -[__NSCFConstantString kj_substringFromIndex:]: Index 10 out of bounds; string length 3
异常地址:-[ViewController testString]

<a id="Cocoapods安装"></a>Cocoapods安装

pod 'KJExceptionDemo'

崩溃处理介绍就到此完毕,后面有相关再补充,写文章不容易,还请点个小星星传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342