欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS App Crash 防护框架之 MKCrashGuard

程序员文章站 2022-04-09 10:30:02
...

github 传送门

MKCrashGuard

App 运行时 Crash 自动修复 + 捕获上传

1、使用

  • 添加组件
pod 'MKAppKit/MKCrashGuard'
  • 使用
// 启用防护
[MKCrashGuardManager executeAppGuard];
// 设置 crash 回调
[MKCrashGuardManager registerCrashHandle:self];

2、守护的情形

  • unrecognized selector sent to instance
  • KVO 添加观察者后没有清除、重复添加 (移除) 观察者 (keyPath) 导致的 Crash
  • KVC
  • NStimer 与 Target 强引用,内存泄漏
  • NSNotification iOS9 之前添加通知后,没有移除会导致 Crash
  • NSString,NSArray,NSDictonary,NSAttributedString,NSSet 以及对应的可变形式
  • Zombie Pointer 暂未支持
  • UINavigationController 重复跳转的问题

3、设计原理

  • 利用 Objective-C 语言的动态特性, 采用 AOP面向切面编程的设计思想, 做到无痕植入。对业务代码的零侵入性地将原本会导致 app 崩溃的 crash 抓取住, 消灭掉, 保证 app 继续正常地运行, 再将 crash 的具体信息提取出来, 实时返回给用户。

  • 为了避免冲突,一些 hook 操作前会判断对象的类型,比如 KVO 会判断 NSStringFromClass(object_getClass(object) 如果包含 AMap、RACKVOProxy,就取消 hook 操作。

  • 可变的都继承自不可变的, 所有可变的分类中, 重复的方法就不用替换了。

3.1 监听实例 dealloc

销毁的步骤

> 销毁实例对象时,会调用 dealloc 方法,从子类往父类,依次调用各个的 dealloc,直到 NSObject.
> NSObject 的 dealloc 会调用 object_dispose() 函数,然后释放内存。
> object_dispose 会
    * 析构 C++ 的实例变量
    * 移除 objc_setAssociatedObject 方法关联的对象
    * ARC 下调用实例变量们 (iVars) 的 release 方法,移除 weak 引用

objc_setAssociatedObject 策略:

OBJC_ASSOCIATION_ASSIGN	没有内存管理; 简单地赋值。 == assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC	非原子地保留对象。 ==  nonatomic retain
OBJC_ASSOCIATION_COPY_NONATOMIC	以非原子方式复制对象。 ==  nonatomic copy
OBJC_ASSOCIATION_RETAIN	原子地保留对象。  ==    retain
OBJC_ASSOCIATION_COPY	以原子方式复制对象。 ==    copy

AssociatedObject 原理:
关联对象的实现不复杂,保存的方式为一个全局的哈希表,存取都通过查询表找到关联来执行。哈希表的特点就是牺牲空间换取时间,所以执行速度也可以保证。
  • 由于 dealloc 方法最后会移除 Associated Object,所以当一个对象(Host)释放后,其关联的对象(Associated Object)也会被释放。
  • objc_setAssociatedObject(被添加对象,key,value,AssociationPolicy / 策略) 给当前对象添加一个关联的中间对象,策略用 OBJC_ASSOCIATION_RETAIN,在关联的中间对象的 dealloc 方法中执行一些销毁相关操作。
  • 需要注意的是,调用中间对象的 dealloc 时,Host 对象已经释放了。

3.2 NSTimer 防护原理

主要解决: NSTimer 与 target  相互强引用时, 内存泄漏的问题.

防护措施: hook scheduledTimerWithTimeInterval:target:selector:userInfo:repeats, 在执行时, 当 repeats 为 NO 走原始方法, 当 repeats 为 YES, 创建一个中间对象弱引用 target, 当中间对象的 target 为空时, 清理 NSTimer。从而解决了循环引用的问题。

3.3 NSNotification 防护原理

主要解决: 添加通知后, 没有移除导致 Crash 的问题。
iOS9 之后专门针对于这种情况做了处理, 所以在 iOS9 之后, 即使开发者没有移除 observer,Notification crash 也不会再产生了

防护措施: hook addObserver:selector:name:object: 和 dealloc 方法, 添加通知时, 给 observer 设置标记, 当调用 dealloc 方法时, 检查有标记则移除 observer 即可。
不过 Swizzle dealloc 影响面相对偏广,一般不建议开启。

3.4 KVO 防护原理

主要解决: 添加监听后没有清除、清除不存在的 key、添加重复的 key 导致的 crash

防护措施: hook addObserver:forKeyPath:options:context: \ removeObserver:forKeyPath: \ removeObserver:forKeyPath:context:
在注册监听后, 关联一个中间对象,来维护添加的观察者和 keypath 防止重复添加或移除, 当被观察者释放时, 清除还在集合中的观察者, 从而保护 key 不存在的情况和保护重复添加的情况

3.5 Unrecognized Selector Sent to Instance 防护原理

iOS App Crash 防护框架之 MKCrashGuard

-1. 动态决议 resolveInstanceMethod:(SEL)sel;/ resolveClassMethod:(SEL)sel ;
Forward 最先执行的函数,首先会流转到这里来,返回值是 BOOL, 没有找到就是 NO, 找到就返回 YES. 在当前的实例中加入不存在的 Selector, 并绑定 IMP。return YES
    +(BOOL)resolveInstanceMethod:(SEL)sel {
        NSString *methodName = NSStringFromSelector(sel);
        if ([methodName isEqualToString:@"test"]) {
            class_addMethod([self class], sel, (IMP)myTest,"aaa@qq.com:");
            return YES;
        } // "aaa@qq.com:" 方法的签名,代表没有参数的方法。"aaa@qq.com:@" 有参数的方法
    return [super resolveInstanceMethod:sel];
    }
    void myMethod(id self, SEL _cmd) {
        NSLog(@"我被调用了");
    }

-2.Target 重定向 forwardingTargetForSelector:(SEL)aSelector
如果 resolveInstanceMethod 没有处理,将进行到 forwardingTargetForSelector 这步来,这时候可以返回 nil,也可以创建用一个对象来接收消息流程流,然后在你的对象中添加不存在的 Selector,这样就不会 crash 了

    -(id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *selectorName = NSStringFromSelector(aSelector);
        if ([selectorName isEqualToString:@"testMyObject"]) {
            myObject *myobject = [[myObject alloc] init];
            return myobject;
        }
        return [super forwardingTargetForSelector:aSelector];
    }

    @interface myObject : NSObject
    -(void)testMyObject;
    @end
    @implementation myObject
    -(void)testMyObject {
        NSLog(@"测试成功");
    }
    @end

-3. 转发 methodSignatureForSelector:(SEL)aSelector + forwardInvocation:(NSInvocation *)anInvocation
先调用 methodSignatureForSelector 来请求一个签名,从而生成一个 NSInvocation,对消息进行完全转发。
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
        if (!signature) {
            if([myObject instancesRespondToSelector:aSelector]) {
                signature = [myObject instanceMethodSignatureForSelector:aSelector];
            }
        }
        return signature;
    }

    -(void)forwardInvocation:(NSInvocation *)anInvocation {
        if ([myObject instancesRespondToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:[[myObject alloc] init]];
        }
    }

如果上述 3 步都没找到实现,便调用 doesNotRecognizeSelector 抛出异常。

4、crash 解读 : 线程回溯

iOS App Crash 防护框架之 MKCrashGuard

形如上图, 是 crash 常见的形式。解读如下

1、2、3 略去。4 是线程回溯, 基本可以帮助定位到 crash 点:
    形如下列的线程信息:
    2   ysklib  0x0347b488          0x83000 -[CrashTest executeAllTest] + 8740
    6   MKApp   0x000000010a07b46f   -[CrashTest executeAllTest]        + 47
    依次对应: 帧编号 - 二进制库名称 - 调用方法的起始地址 - (基本地址 (指向文件中的地址))+ 调用的函数名称 + 偏移地址 (在文件中的位置)

5 是线程状态, 闪退时, 寄存器的值。基本用不到。
6 是退出时, 加载的二进制文件。
  • 异常编码
Exception Codes: 不同的值, 标识不同的 crash 类型, 如下:
0x8badf00d: ate bad food: 该编码表示应用是因为发生 watchdog 超时而被 iOS 终止的。  通常是应用花费太多时间而无法启动、终止或响应用系统事件。
0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。
0xdead10cc: 该代码表明应用因为在后台运行时占用系统资源, 如通讯录数据库不释放而被终止 。
0xdeadfa11: dead fall: 该代码表示应用是被用户强制退出的。
0xc00010ff: cool off 因为太烫了被干掉
  • 分析 demo

iOS App Crash 防护框架之 MKCrashGuard

1、Exception Codes 对应是 0x000000008badf00d. crash 原因就是 watchdog 超时而被 iOS 终止的。
2、线程回溯:
从最开始调用的第 25 帧, 一帧帧的开始往上看(忽略系统库和框架)
应用执行到 第 8 帧时, 出现 crash

iOS App Crash 防护框架之 MKCrashGuard

解读后, 错误出现的位置:
-[AppDelegate application:didFinishLaunchingWithOptions:] + 116      MKApp 调用了 [CrashTest executeAllTest]
-[CrashTest executeAllTest] + 47                                     MKApp 调用了  [CrashTest testArray]
-[CrashTest testArray] + 216                                         MKApp 调用了  [NSArray arrayWithObjects:count:] 导致 crash
+[NSArray arrayWithObjects:count:] + 52  这个是实际报错的位置           CoreFoundation
-[__NSPlaceholderArray initWithObjects:count:] + 237 crash 原因       CoreFoundation
_CFThrowFormattedException + 194                                    CoreFoundation
objc_exception_throw + 48                                           libobjc.A.dylib

5、常见 crash

类型 方法 备注
SEL(unrecognized selector sent to instance) .h 定义但. m 没实现 -
SEL delegate 回调前没有判空而是直接调用 -
SEL id 类型没有判断类型, 强行调用了真实类型不存在的方法 -
SEL copy 修饰的可变的字符串 \ 字典 \ 数组 \ 集合 \ Data, 调用了可变的方法 -
SEL 低版本调用高版本 api
SEL performSelector 访问不存在的方法 -
Array 数组越界、插入空对象 -
Array for 遍历的同时, 移除元素 使用 enumerateObjectsUsingBlock 遍历
String\Array\Dic 等 不可变对象调用可变对象的方法 -
Dict key、value 为空 -
Dict value 为 nil 时, setObject:ForKey: 会 crash, 而 setValue:ForKey: 不会 -
KVO 添加了监听,没有移除;添加重复的 key; 对同一 keypath 多次 removeObserver: 父类有一个 KVO, 父类在 dealloc 中 remove 了一次, 子类又 remove 了一次 -
KVO 观察者或被观察者是局部变量、没有实现 observeValueForKeyPath: 方法 参考 FBKVOController
KVC value 为 nil、key 不存在 -
多线程 一个线程访问的对象被另一个线程修改了 \ 释放了 保证多线程中读写操作的原子性: 加锁, 信号量, GCD 串行队列等
NSTimer 没有 invalidate, 直接销毁 -
野指针 (EXC_BAD_ACCESS) property:strong/weak 修饰误用成 assign -
野指针 objc_setAssociatedObject 属性修饰词误用成 ASSIGN -
野指针 NSNotification\KVO 只 addObserver 并没有 removeObserver -
野指针 delegate\block 回调前没有判空而是直接调用 -
野指针 CoreFoundation 对象到 Foundation 中, 已用__bridge_transfer 转移了对象的所有权之后, 调用一次 CFRelease __bridge: bridge 时候不要任何事情 __bridge_retained:(ObjC 转 CF 的时候使用) 在 bridge 的时候 retain 对象, 在 CF 一端负责释放对象 __bridge_transfer:(CF 转 ObjC 的时候使用) 转移 CF 对象的所有权, 不再需要在 CF 一端负责释放对象

5.1 野指针

iOS App Crash 防护框架之 MKCrashGuard

野指针访问已经释放的对象 crash 其实不是必现的, 因为 dealloc 执行后只是告诉系统, 这片内存我不用了, 而系统并没有就让这片内存不能访问。
所以野指针的崩溃是比较随机的, 你在测试的时候可能没发生 crash, 但是用户在使用的时候就可能发生 crash 了。

- 对象释放后内存没被改动过, 内存完好, 或者析构时删掉一些数据, 那么可能会出现【随机 crash】。
- 对象释放后内存被改动过, 内存被写上不可访问的新数据, 很可能 Crash 在 objc_msgSend 上面(必现 Crash, 常见)
- 对象释放后内存被改动过, 内存被写上可访问的新数据, 访问时访问到别的数据, 可能【随机 crash】

self.delegate = myVC; 用 weak 修饰的
myVC 执行  Pop, 之后就会被销毁
[self.delegate doSomething]; 而 self.delegate 仍然起作用, 成了野指针

避免这种异常可以在调用之前检查一下代理是否为空, 是否能够响应所给的 Selector
if(self.delegate != nil) {
    if([self.delegate respondsToSelector:@selector(doSomething)]) {
        [self.delegate doSomething];
    }
}

5.2 多线程 crash

一般是操作数据 / 库所致

5.3 EXC_BAD_ACCESS

 

 

github 传送门

  •