`
jewel0516
  • 浏览: 12220 次
  • 性别: Icon_minigender_2
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

iOS unrecognized selector crash 自修复技术实现与原理解析

阅读更多
前言

在开发中 unrecognized selector sent to instance XXXXX 是非常常见的 crash 类型。

例如调用以下一段代码就会产生crash

[[NSNull null] performSelector:@selector(fooDoesNotRecognizeSelector1)];
具体 crash 时的表现见下:

2018-01-11 16:28:04.433573+0800 CYLSwizzleMainDemo[13252:156773356] -[NSNull fooDoesNotRecognizeSelector1]: unrecognized selector sent to instance 0x102870ef0
2018-01-11 16:28:04.440436+0800 CYLSwizzleMainDemo[13252:156773356] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSNull fooDoesNotRecognizeSelector1]: unrecognized selector sent to instance 0x102870ef0'
*** First throw call stack:
(
   0   CoreFoundation                      0x00000001025a712b __exceptionPreprocess + 171
   1   libobjc.A.dylib                     0x0000000101c3bf41 objc_exception_throw + 48
   2   CoreFoundation                      0x0000000102628024 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
   3   CoreFoundation                      0x0000000102529f78 ___forwarding___ + 1432
   4   CoreFoundation                      0x0000000102529958 _CF_forwarding_prep_0 + 120
   5   CYLSwizzleMainDemo                  0x0000000101321cef -[AppDelegate application:didFinishLaunchingWithOptions:] + 527
   6   UIKit                               0x0000000103315ac6 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 299
   7   UIKit                               0x0000000103317544 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4113
   8   UIKit                               0x000000010331c9e7 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1720
   9   UIKit                               0x00000001036e5fb0 __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 924
   10  UIKit                               0x0000000103abb998 +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 153
   11  UIKit                               0x00000001036e5ba9 -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 249
   12  UIKit                               0x00000001036e6423 -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 696
   13  UIKit                               0x0000000104063fe9 __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 262
   14  UIKit                               0x0000000104063ea2 -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 444
   15  UIKit                               0x0000000103d410a0 __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 221
   16  UIKit                               0x0000000103f40126 _performActionsWithDelayForTransitionContext + 100
   17  UIKit                               0x0000000103d40f63 -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 231
   18  UIKit                               0x0000000103abaff5 -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 392
   19  UIKit                               0x000000010331b266 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 523
   20  UIKit                               0x00000001038f5b97 -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 369
   21  FrontBoardServices                  0x0000000106d74cc0 -[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 338
   22  FrontBoardServices                  0x0000000106d7d7b5 __56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 235
   23  libdispatch.dylib                   0x0000000105fd933d _dispatch_client_callout + 8
   24  libdispatch.dylib                   0x0000000105fde9f3 _dispatch_block_invoke_direct + 592
   25  FrontBoardServices                  0x0000000106da9498 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
   26  FrontBoardServices                  0x0000000106da914e -[FBSSerialQueue _performNext] + 464
   27  FrontBoardServices                  0x0000000106da96bd -[FBSSerialQueue _performNextFromRunLoopSource] + 45
   28  CoreFoundation                      0x000000010254a101 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
   29  CoreFoundation                      0x00000001025e9f71 __CFRunLoopDoSource0 + 81
   30  CoreFoundation                      0x000000010252ea19 __CFRunLoopDoSources0 + 185
   31  CoreFoundation                      0x000000010252dfff __CFRunLoopRun + 1279
   32  CoreFoundation                      0x000000010252d889 CFRunLoopRunSpecific + 409
   33  GraphicsServices                    0x000000010763b9c6 GSEventRunModal + 62
   34  UIKit                               0x000000010331e4d2 UIApplicationMain + 159
   35  CYLSwizzleMainDemo                  0x00000001013230bf main + 111
   36  libdyld.dylib                       0x0000000106055d81 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
这类 crash 尤其在混合开发,或者 JS 与 native 交互中经常遇到,非常影响用户体验,也降低了 app 的质量与稳定性。

常见的 crash 场景可以总结为:

JSON 解析后,空值解析为 NSNULL 对象,造成 crash
JS 调用 native 方法,结果由于native底版本,或者 JS 代码编写的问题,找不到方法,导致 app crash。
在研究如何实现 app 自修复该 bug 前,我们可以研究下什么时候会报 unrecognized selector 的异常?

什么时候会报unrecognized selector的异常?

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:

Method resolution
objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。

Fast forwarding
如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。
这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。

Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

拦截调用的整个流程即 Objective-C 的消息转发机制。其具体流程如下图:

enter image description here

unrecognized selector sent to instance XXXXX crash 自修复技术实现

原理简单来说:

当调用该对象上某个方法,而该对象上没有实现这个方法的时候,
可以通过“消息转发”进行解决。

可以利用消息转发机制的三个步骤,选择哪一步去改造比较合适呢?

这里我们选择了第二步forwardingTargetForSelector。引用 《大白健康系统--iOS APP运行时Crash自动修复系统》 的分析:

resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
forwardInvocation 可以通过 NSInvocation 的形式将消息转发给多个对象,但是其开销较大,需要创建新的 NSInvocation 对象,并且 forwardInvocation 的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
forwardingTargetForSelector 可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
选择了 forwardingTargetForSelector 之后,可以将 NSObject 的该方法重写,做以下几步的处理:

具体如下:

hook forwardingTargetForSelector 方法
添加白名单,限制hook的范围,排除内部类,并自定义需要hook的类
创建桩类 ForwardingTarget
为桩类动态添加对应的selector的imp,指向一个函数,返回 NSNull 对象
将消息转移到该桩类对象 ForwardingTarget 上
将 hook 掉的 crash 信息进行上报,方便发现问题,后期修复掉。
添加白名单,避免出现hook内部方法以及不必要的对象。
内部对象的特征是都以 _ 开头。
其他需要限制的部分,经常会出现在组件化开发、SDK开发中,避免影响到其他模块的正常工作,可以用类的前缀做区分。

其中动态创建的方法,返回值为什么返回一个 NSNull,而不是其他的值。

这样做的好处在于,在设置白名单的时候,只需要将 NSNull 设置进白名单,就可以解决方法返回值调用方法造成的crash。返回其他类型,就需要在白名单中多设置一种类型。

可以解决如下问题:

id foo = [[NSNull null] performSelector:@selector(fooDoesNotRecognizeSelector1)];
[foo performSelector:@selector(fooDoesNotRecognizeSelector2)];
hook时注意如果对象的类本事如果重写了forwardInvocation方法的话,就不应该对forwardingTargetForSelector进行重写了,否则会影响到该类型的对象原本的消息转发流程。

通过重写NSObject的forwardingTargetForSelector方法,我们就可以将无法识别的方法进行拦截并且将消息转发到安全的桩类对象中,从而可以使app继续正常运行。

具体的实现代码如下:

//
//  ForwardingTarge.h
// 
//
//  Created by ChenYilong on 18/01/10.
//  Copyright © 2018年  All rights reserved.
//

#import <Foundation/Foundation.h>

@interface ForwardingTarget : NSObject

@end
//
//  ForwardingTarge.m
// 
//
//  Created by ChenYilong on 18/01/10.
//  Copyright © 2018年  All rights reserved.
//

#import "ForwardingTarget.h"
#import <objc/runtime.h>

@implementation ForwardingTarget

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
   class_addMethod(self.class, sel, (IMP)ForwardingTarget_dynamicMethod, "@@:");
   [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];
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
   [super doesNotRecognizeSelector:aSelector]; // crash
}

@end
//
//  NSObject+DoesNotRecognizeSelectorExtension.h
// 
//
//  Created by ChenYilong on 18/01/10.
//  Copyright © 2018年  All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSObject (DoesNotRecognizeSelectorExtension)

@end
//
//  NSObject+DoesNotRecognizeSelectorExtension.m
// 
//
//  Created by ChenYilong on 18/01/10.
//  Copyright © 2018年 All rights reserved.
//

#import "NSObject+DoesNotRecognizeSelectorExtension.h"
#import <objc/runtime.h>
#import "ForwardingTarget.h"

static ForwardingTarget *_target = nil;

@implementation NSObject (DoesNotRecognizeSelectorExtension)

+ (void)load {
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       _target = [ForwardingTarget new];;
       not_recognize_selector_classMethodSwizzle([self class], @selector(forwardingTargetForSelector:), @selector(doesnot_recognize_selector_swizzleForwardingTargetForSelector:));
   });
}

+ (BOOL)isWhiteListClass:(Class)class {
   NSString *classString = NSStringFromClass(class);
   BOOL isInternal = [classString hasPrefix:@"_"];
   if (isInternal) {
       return NO;
   }
   BOOL isNull =  [classString isEqualToString:NSStringFromClass([NSNull class])];
  
   BOOL isMyClass  = [classString ...];
   return isNull || isMyClass;
}

- (id)doesnot_recognize_selector_swizzleForwardingTargetForSelector:(SEL)aSelector {
   id result = [self doesnot_recognize_selector_swizzleForwardingTargetForSelector:aSelector];
   if (result) {
       return result;
   }
   BOOL isWhiteListClass = [[self class] isWhiteListClass:[self class]];
   if (!isWhiteListClass) {
       return nil;
   }
  
   if (!result) {
       result = _target;
   }
   return result;
}

#pragma mark - private method

BOOL not_recognize_selector_classMethodSwizzle(Class aClass, SEL originalSelector, SEL swizzleSelector) {
   Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
   Method swizzleMethod = class_getInstanceMethod(aClass, swizzleSelector);
   BOOL didAddMethod =
   class_addMethod(aClass,
                   originalSelector,
                   method_getImplementation(swizzleMethod),
                   method_getTypeEncoding(swizzleMethod));
   if (didAddMethod) {
       class_replaceMethod(aClass,
                           swizzleSelector,
                           method_getImplementation(originalMethod),
                           method_getTypeEncoding(originalMethod));
   } else {
       method_exchangeImplementations(originalMethod, swizzleMethod);
   }
   return YES;
}

@end
参考文献:

分享到:
评论

相关推荐

    SceneDelegate:[AppDelegate setWindow:]: unrecognized selector sent to instance 0x60000002b440

    -[AppDelegate setWindow:]: unrecognized selector sent to instance 0x60000002b440 修改方法: 在AppDelegate.h里加声明window @property (nonatomic, strong) UIWindow * window; 运行问题解决 兼容13和13以前...

    iOS 拦截奔溃 使程序不在崩溃

    本文将详细介绍如何在iOS中实现拦截奔溃,以及利用Runloop来防止应用意外关闭。 首先,我们需要理解什么是奔溃。在iOS中,应用程序通常因为执行了非法操作,如访问空指针、内存溢出或者调用不存在的方法等,而导致...

    XXShield:这是一个可以避免由Objective-C编写的iOS项目崩溃的库

    前言 正在运行的 APP 突然 Crash,是一件令人不爽...Unrecognized Selector Crash KVO Crash Container Crash NSNotification Crash NSNull Crash NSTimer Crash 野指针 Crash 1 Unrecoginzed Selector Crash 出现原因

    iOS面试题合集

    - **知识点概述**:CocoaPods是一个iOS和macOS项目的依赖管理工具,它通过解析Podfile文件来管理项目的外部依赖库。 #### 三十一、内存管理中的问题解决 - **知识点概述**:针对内存泄露、内存溢出等问题,可以...

    iOS Crash防护

    iOS Crash防护内容涉及 NSArray/NSMutableArray、NSDictionary/NSMutableDictionary、NSString/NSMutableString、Unrecognized Selector、KVO、KVC 、野指针定位、内存泄漏/循环引用;主要是对常见易错的地方进行...

    ios runtime消息转发机制

    当对象收到一个消息但无法处理时,通常会引发一个未定义的方法( unrecognized selector sent to instance )异常。然而,通过消息转发,对象有三次机会来处理这个未定义的消息,从而避免程序崩溃。 1. **方法签名...

    iOS消息转发机制及避免崩溃的解决方案.pdf

    iOS的消息转发机制是Objective-C运行时系统中一个关键特性,它允许对象在接收到未实现的方法调用时,有机会去处理或转发这个消息,从而防止程序因无法识别选择子(selector)而崩溃。这一机制主要包含三个步骤: 1....

    iOS 最新面试总结题库,欢迎观看.docx

    在面试中,深入理解Runloop的原理和应用场景,以及Runtime如何实现消息传递和方法解析,都是展示技术深度和解决问题能力的关键。掌握这些知识点不仅能帮助开发者优化性能,还能在遇到问题时迅速定位和解决。

    APTargets:以正确的方式添加UIControl目标

    -[MyProject.ViewController buttonTapped]: unrecognized selector sent to instance 0x7f9b42d48ae0 ...然后恭喜,您是iOS开发人员。 我们都讨厌选择器,现在您可以彻底消除它们。 用法 UIControl + Target....

    成功解决stata软件中 command pwcorr-a is unrecognized问题

    成功解决stata软件中 command pwcorr_a is unrecognized问题。 步骤,把文件中.ado文件放在路径..\Stata\base\p中。 已经成功解决。

    iOS面试题集合(BAT及各大中小型公司)1

    iOS面试题集合主要涵盖了许多iOS开发中的核心概念和技术,这些题目可以帮助求职者准备面试,同时也能促进开发者自身的技能提升。以下是一些关键知识点的详细解释: 1. **Cell重用原理**:UITableView或...

    iOS对象指针和基础数据类型的强转详解

    本文主要介绍了iOS中对象指针和基础数据类型如何进行强转,下面话不多说,直接来看示例详解。 一、对象指针的强转: ...-[UIView setText:]: unrecognized selector sent to instance 0x7ff1e14e03

    解决JSON数据因为null导致数据加载失败的方法

    尽管URL是正确的,但依然可能会收到错误信息,如`reason: '-[NSNull length]: unrecognized selector sent to instance`。这是因为`NSJSONSerialization`在解析JSON时,将JSON中的`null`值转换为了`NSNull`对象。而`...

    Runtime面试题.pdf

    这种内存对齐保证了对象在内存中的地址总是按16字节的倍数对齐,这与iOS的内存分配机制有关。 3. class_rw_t和class_ro_t class_rw_t是可读可写的意思,它负责存储类的可变部分,即类的属性、方法和协议等信息。...

    unrecognized opcode(解决方案).md

    unrecognized opcode(解决方案).md

    ios xcode警告与错误的分析总结

    1.相同具有外部链接的静态变量 ...iOS7.0以下『containsString』报 __NSCFString containsString:]: unrecognized selector sent to instance在要使用的时候导入#import “NSString+Extension.h” 的扩展即可 - (B

    Objective-C:iOS原生开发语言Objective-C。从0到1开始的demos

    reason: '-[ViewController redBtn]: unrecognized selector sent to instance 0x7ff62ff05750' 多余的连线没有删除(创建了多余的IBAction) reason: '[&lt;ViewController&gt; setValue:forUndefinedKey:]: this class is ...

    iOS中json解析出现的null,nil,NSNumber的解决办法

    在iOS开发中,JSON是一种广泛使用的轻量级数据交换格式,尤其在与服务器进行通信时。然而,处理JSON数据时可能会遇到一些问题,比如解析出的`null`、`nil`以及`NSNumber`类型的困惑。这里我们将深入探讨如何解决这些...

    常见iOS错误解决方案

    #### 十二、`unrecognized selector sent to instance` **问题描述:** 对象接收到未识别的选择器消息。 **解决方案:** 1. 检查发送给对象的消息是否正确。 2. 确保对象的类型与预期一致。 3. 查看对象是否实现了...

    Runtime面试.pdf

    当消息发送给一个对象,而对象的isa指针所指的类及其所有父类的方法列表中都没有找到相应的selector时,程序会抛出unrecognized selector异常。这种情况通常发生在调用未实现的方法时。 10. Category中添加属性: ...

Global site tag (gtag.js) - Google Analytics