源码

首页 » 归档 » 源码 » iOS 开发:『Crash 防护系统』(一)Unrecognized Selector

iOS 开发:『Crash 防护系统』(一)Unrecognized Selector

> **本文首发于我的个人博客/uff1a[『不羁阁』 ](https://bujige.net)** > **文章链接/uff1a[传送门](https://bujige.net/blog/iOS-YSCDefender-01.html)** > **本文更新时间/uff1a2019年08月23日12:15:21** --- > 本文是 **『Crash 防护系统』系列** 第一篇。 > 这个系列将会介绍如何设计一套 APP Crash 防护系统。这套系统采用 AOP/uff08面向切面编程/uff09的设计思想/uff0c利用 Objective-C语言的运行时机制/uff0c在不侵入原有项目代码的基础之上/uff0c通过在 APP 运行时阶段对崩溃因素的的拦截和处理/uff0c使得 APP 能够持续稳定正常的运行。 --- > 通过本文/uff0c您将了解到/uff1a > ###### 1. Crash 防护系统开篇 > ###### 2. 防护原理简介和常见 Crash > ###### 3. Method Swizzling 方法的封装 > ###### 4. Unrecognized Selector 防护 > ###### 4.1 unrecognized selector sent to instance/uff08找不到对象方法的实现/uff09 > ###### 4.2 unrecognized selector sent to class/uff08找不到类方法实现/uff09 > > 文中示例代码在/uff1a [bujige](https://github.com/bujige) / **[YSC-Avoid-Crash](https://github.com/bujige/YSC-Avoid-Crash)** --- # 1. Crash 防护系统开篇 APP 的崩溃问题/uff0c一直以来都是开发过程中重中之重的问题。日常开发阶段的崩溃/uff0c发现后还能够立即处理。但是一旦发布上架的版本出现问题/uff0c就需要紧急加班修复 BUG/uff0c再更新上架新版本了。在这个过程中/uff0c 说不定会因为崩溃而导致关键业务中断、用户存留率下降、品牌口碑变差、生命周期价值下降等/uff0c最终导致流失用户/uff0c影响到公司的发展。 当然/uff0c避免崩溃问题的最好办法就是不产生崩溃。在开发的过程中就要尽可能地保证程序的健壮性。但是/uff0c人又不是机器/uff0c不可能不犯错。不可能存在没有 BUG 的程序。但是如果能够利用一些语言机制和系统方法/uff0c设计一套防护系统/uff0c使之能够有效的降低 APP 的崩溃率/uff0c那么不仅 APP 的稳定性得到了保障/uff0c而且最重要的是可以减少不必要的加班。 这套 Crash 防护系统被命名为/uff1a**『YSCDefender/uff08防卫者/uff09』**。Defender 也是路虎旗下最硬派的越野车系。在电影《Tomb Raider》里面/uff0c由 Angelina Jolie 饰演的英国女探险家 Lara Croft/uff0c所驾驶的就是一台 Defender。Defender 也是我比较喜欢的车之一。 不过呢/uff0c这不重要。。。我就是为这个项目起了个花里胡哨的名字/uff0c并给这个名字赋予了一些无聊的意义。。。 --- # 2. 防护原理简介和常见 Crash `Objective-C` 语言是一门动态语言/uff0c我们可以利用 `Objective-C` 语言的 `Runtime` 运行时机制/uff0c对需要 `Hook` 的类添加 `Category/uff08分类/uff09`/uff0c在各个分类的 `+(void)load;` 中通过 `Method Swizzling` 拦截容易造成崩溃的系统方法/uff0c将系统原有方法与添加的防护方法的 `selector/uff08方法选择器/uff09` 与 IMP/uff08函数实现指针/uff09进行对调。然后在替换方法中添加防护操作/uff0c从而达到避免以及修复崩溃的目的。 > 通过 Runtime 机制可以避免的常见 Crash /uff1a > ###### 1. unrecognized selector sent to instance/uff08找不到对象方法的实现/uff09 > ###### 2. unrecognized selector sent to class/uff08找不到类方法实现/uff09 > ###### 3. KVO Crash > ###### 4. KVC Crash > ###### 5. NSNotification Crash > ###### 6. NSTimer Crash > ###### 7. Container Crash/uff08集合类操作造成的崩溃/uff0c例如数组越界/uff0c插入 nil 等/uff09 > ###### 8. NSString Crash /uff08字符串类操作造成的崩溃/uff09 > ###### 9. Bad Access Crash /uff08野指针/uff09 > ###### 10. Threading Crash /uff08非主线程刷 UI/uff09 > ###### 11. NSNull Crash 这一篇我们先来讲解下 `unrecognized selector sent to instance/uff08找不到对象方法的实现/uff09` 和 `unrecognized selector sent to class/uff08找不到类方法实现/uff09` 造成的崩溃问题。 --- # 3. Method Swizzling 方法的封装 由于这几种常见 Crash 的防护都需要用到 Method Swizzling 技术。所以我们可以为 NSObject 新建一个分类/uff0c将 Method Swizzling 相关的方法封装起来。 ```Objc /********************* NSObject+MethodSwizzling.h 文件 *********************/ #import NS_ASSUME_NONNULL_BEGIN @interface NSObject (MethodSwizzling) /** 交换两个类方法的实现 * @param originalSelector 原始方法的 SEL * @param swizzledSelector 交换方法的 SEL * @param targetClass 类 */ + (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass; /** 交换两个对象方法的实现 * @param originalSelector 原始方法的 SEL * @param swizzledSelector 交换方法的 SEL * @param targetClass 类 */ + (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass; @end /********************* NSObject+MethodSwizzling.m 文件 *********************/ #import "NSObject+MethodSwizzling.h" #import @implementation NSObject (MethodSwizzling) // 交换两个类方法的实现 + (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass { swizzlingClassMethod(targetClass, originalSelector, swizzledSelector); } // 交换两个对象方法的实现 + (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass { swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector); } // 交换两个类方法的实现 C 函数 void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getClassMethod(class, originalSelector); Method swizzledMethod = class_getClassMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } // 交换两个对象方法的实现 C 函数 void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } @end ``` --- # 4. Unrecognized Selector 防护 ## 4.1 unrecognized selector sent to instance/uff08找不到对象方法的实现/uff09 如果被调用的对象方法没有实现/uff0c那么程序在运行中调用该方法时/uff0c就会因为找不到对应的方法实现/uff0c从而导致 APP 崩溃。比如下面这样的代码/uff1a ``` UIButton *testButton = [[UIButton alloc] init]; [testButton performSelector:@selector(someMethod:)]; ``` `testButton` 是一个 `UIButton` 对象/uff0c而 `UIButton` 类中并没有实现 `someMethod:` 方法。所以向 `testButoon` 对象发送 `someMethod:` 方法/uff0c就会导致 `testButoon` 对象无法找到对应的方法实现/uff0c最终导致 APP 的崩溃。 **那么有办法解决这类因为找不到方法的实现而导致程序崩溃的方法吗/uff1f** 我们知道消息转发机制有三大步骤/uff1a**消息动态解析**、**消息接受者重定向**、**消息重定向**。通过这三大步骤/uff0c可以让我们在程序找不到调用方法崩溃之前/uff0c拦截方法调用。 大致流程如下/uff1a ###### 1. 消息动态解析/uff1aObjective-C 运行时会调用 `+resolveInstanceMethod:` 或者 `+resolveClassMethod:`/uff0c让你有机会提供一个函数实现。我们可以通过重写这两个方法/uff0c添加其他函数实现/uff0c并返回 YES/uff0c 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现/uff0c则进入下一步。 ###### 2. 消息接受者重定向/uff1a如果当前对象实现了 `forwardingTargetForSelector:`/uff0cRuntime 就会调用这个方法/uff0c允许我们将消息的接受者转发给其他对象。如果这一步方法返回 `nil`/uff0c则进入下一步。 ###### 3. 消息重定向/uff1aRuntime 系统利用 `methodSignatureForSelector:` 方法获取函数的参数和返回值类型。 - 如果 `methodSignatureForSelector:` 返回了一个 `NSMethodSignature` 对象/uff08函数签名/uff09/uff0cRuntime 系统就会创建一个 NSInvocation 对象/uff0c并通过 `forwardInvocation:` 消息通知当前对象/uff0c给予此次消息发送最后一次寻找 IMP 的机会。 - 如果 `methodSignatureForSelector:` 返回 `nil`。则 Runtime 系统会发出 `doesNotRecognizeSelector:` 消息/uff0c程序也就崩溃了。 ![Runtime 消息转发步骤图.png](https://upload-images.jianshu.io/upload_images/1877784-b035061ccb2efd26.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这里我们选择第二步/uff08消息接受者重定向/uff09来进行拦截。因为 `-forwardingTargetForSelector` 方法可以将消息转发给一个对象/uff0c开销较小/uff0c并且被重写的概率较低/uff0c适合重写。 具体步骤如下/uff1a ###### 1. 给 NSObject 添加一个分类/uff0c在分类中实现一个自定义的 `-ysc_forwardingTargetForSelector:` 方法/uff1b ###### 2. 利用 Method Swizzling 将 `-forwardingTargetForSelector:` 和 `-ysc_forwardingTargetForSelector:` 进行方法交换。 ###### 3. 在自定义的方法中/uff0c先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现/uff0c就动态创建一个目标类/uff0c给目标类动态添加一个方法。 ###### 4. 把消息转发给动态生成类的实例对象/uff0c由目标类动态创建的方法实现/uff0c这样 APP 就不会崩溃了。 实现代码如下/uff1a ```Objc #import "NSObject+SelectorDefender.h" #import "NSObject+MethodSwizzling.h" #import @implementation NSObject (SelectorDefender) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 拦截 `-forwardingTargetForSelector:` 方法/uff0c替换自定义实现 [NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:) withMethod:@selector(ysc_forwardingTargetForSelector:) withClass:[NSObject class]]; }); } // 自定义实现 `-ysc_forwardingTargetForSelector:` 方法 - (id)ysc_forwardingTargetForSelector:(SEL)aSelector { SEL forwarding_sel = @selector(forwardingTargetForSelector:); // 获取 NSObject 的消息转发方法 Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel); // 获取 当前类 的消息转发方法 Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel); // 判断当前类本身是否实现第二步:消息接受者重定向 BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method); // 如果没有实现第二步:消息接受者重定向 if (!realize) { // 判断有没有实现第三步:消息重定向 SEL methodSignature_sel = @selector(methodSignatureForSelector:); Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel); Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel); realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method); // 如果没有实现第三步:消息重定向 if (!realize) { // 创建一个新类 NSString *errClassName = NSStringFromClass([self class]); NSString *errSel = NSStringFromSelector(aSelector); NSLog(@"出问题的类/uff0c出问题的对象方法 == %@ %@", errClassName, errSel); NSString *className = @"CrachClass"; Class cls = NSClassFromString(className); // 如果类不存在 动态创建一个类 if (!cls) { Class superClsss = [NSObject class]; cls = objc_allocateClassPair(superClsss, className.UTF8String, 0); // 注册类 objc_registerClassPair(cls); } // 如果类没有对应的方法/uff0c则动态添加一个 if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) { class_addMethod(cls, aSelector, (IMP)Crash, "@@:@"); } // 把消息转发到当前动态生成类的实例对象上 return [[cls alloc] init]; } } return [self ysc_forwardingTargetForSelector:aSelector]; } // 动态添加的方法实现 static int Crash(id slf, SEL selector) { return 0; } @end ``` --- ## 4.2 unrecognized selector sent to class/uff08找不到类方法实现/uff09 同对象方法一样/uff0c如果被调用的类方法没有实现/uff0c那么同样也会导致 APP 崩溃。 例如/uff0c有这样一个类/uff0c声明了一个 `+ (id)aClassFunc;` 的类方法/uff0c 但是并没有实现/uff0c就像下边的 `YSCObject` 这样。 ```Objc /********************* YSCObject.h 文件 *********************/ #import @interface YSCObject : NSObject + (id)aClassFunc; @end /********************* YSCObject.m 文件 *********************/ #import "YSCObject.h" @implementation YSCObject @end ``` 如果我们直接调用 `[YSCObject aClassFunc];` 就会导致崩溃。 找不到类方法实现的解决方法和之前类似/uff0c我们可以利用 Method Swizzling 将 `+forwardingTargetForSelector:` 和 `+ysc_forwardingTargetForSelector:` 进行方法交换。 ```Ojbc #import "NSObject+SelectorDefender.h" #import "NSObject+MethodSwizzling.h" #import @implementation NSObject (SelectorDefender) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 拦截 `+forwardingTargetForSelector:` 方法/uff0c替换自定义实现 [NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:) withMethod:@selector(ysc_forwardingTargetForSelector:) withClass:[NSObject class]]; }); } // 自定义实现 `+ysc_forwardingTargetForSelector:` 方法 + (id)ysc_forwardingTargetForSelector:(SEL)aSelector { SEL forwarding_sel = @selector(forwardingTargetForSelector:); // 获取 NSObject 的消息转发方法 Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel); // 获取 当前类 的消息转发方法 Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel); // 判断当前类本身是否实现第二步:消息接受者重定向 BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method); // 如果没有实现第二步:消息接受者重定向 if (!realize) { // 判断有没有实现第三步:消息重定向 SEL methodSignature_sel = @selector(methodSignatureForSelector:); Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel); Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel); realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method); // 如果没有实现第三步:消息重定向 if (!realize) { // 创建一个新类 NSString *errClassName = NSStringFromClass([self class]); NSString *errSel = NSStringFromSelector(aSelector); NSLog(@"出问题的类/uff0c出问题的类方法 == %@ %@", errClassName, errSel); NSString *className = @"CrachClass"; Class cls = NSClassFromString(className); // 如果类不存在 动态创建一个类 if (!cls) { Class superClsss = [NSObject class]; cls = objc_allocateClassPair(superClsss, className.UTF8String, 0); // 注册类 objc_registerClassPair(cls); } // 如果类没有对应的方法/uff0c则动态添加一个 if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) { class_addMethod(cls, aSelector, (IMP)Crash, "@@:@"); } // 把消息转发到当前动态生成类的实例对象上 return [[cls alloc] init]; } } return [self ysc_forwardingTargetForSelector:aSelector]; } // 动态添加的方法实现 static int Crash(id slf, SEL selector) { return 0; } @end ``` 将 4.1 和 4.2 结合起来就可以拦截所有未实现的类方法和对象方法了。具体实现可参考代码/uff1a [bujige](https://github.com/bujige) / **[YSC-Avoid-Crash](https://github.com/bujige/YSC-Avoid-Crash)** --- # 参考资料 - [How Not to Crash](https://inessential.com/hownottocrash) - [iOS 防止应用崩溃](https://www.jianshu.com/p/8f1fb138ff8d) - [大白健康系统--iOS APP运行时Crash自动修复系统](https://neyoufan.github.io/2017/01/13/ios/BayMax_HTSafetyGuard)

(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/ios-kaifacrash-fanghuxitongyiunrecognized-selector.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:8 月 29, 2019 at 04:12 上午

热评文章