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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 在开发中 `unrecognized selector sent to instance XXXXX` 是非常常见的 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的运行时会给出三次拯救程序崩溃的机会:

  1. Method resolution

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

  1. Fast forwarding

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

  1. 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进行重写了,否则会影响到该类型的对象原本的消息转发流程。

通过重写NSObjectforwardingTargetForSelector方法,我们就可以将无法识别的方法进行拦截并且将消息转发到安全的桩类对象中,从而可以使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

参考文献:

相关文章
|
6天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
3天前
|
机器学习/深度学习 算法
概率分布深度解析:PMF、PDF和CDF的技术指南
本文将深入探讨概率分布,详细阐述概率质量函数(PMF)、概率密度函数(PDF)和累积分布函数(CDF)这些核心概念,并通过实际示例进行说明。
29 15
概率分布深度解析:PMF、PDF和CDF的技术指南
|
10天前
|
机器学习/深度学习 人工智能 编解码
深入探索AI文生语音技术的奥秘:从文本输入到逼真语音输出的全链条语音合成过程解析
【9月更文挑战第2天】深入探索AI文生语音技术的奥秘:从文本输入到逼真语音输出的全链条语音合成过程解析
 深入探索AI文生语音技术的奥秘:从文本输入到逼真语音输出的全链条语音合成过程解析
|
3天前
|
存储 关系型数据库 MySQL
技术解析:MySQL中取最新一条重复数据的方法
以上提供的两种方法都可以有效地从MySQL数据库中提取每个类别最新的重复数据。选择哪种方法取决于具体的使用场景和MySQL版本。子查询加分组的方法兼容性更好,适用于所有版本的MySQL;而窗口函数方法代码更简洁,执行效率可能更高,但需要MySQL 8.0及以上版本。在实际应用中,应根据数据量大小、查询性能需求以及MySQL版本等因素综合考虑,选择最合适的实现方案。
22 6
|
1天前
|
负载均衡 5G 网络性能优化
深入解析LTE(长期演进技术)的基本架构及其关键组件
深入解析LTE(长期演进技术)的基本架构及其关键组件
17 2
|
12天前
|
API 云计算 开发者
使用宜搭平台带来的便利:技术解析与实践
【9月更文第8天】随着企业信息化建设的不断深入,业务流程自动化的需求日益增长。宜搭平台作为一种高效的应用构建工具,为企业提供了快速搭建各类业务系统的可能。本文将探讨使用宜搭平台给企业和开发者带来的便利,并通过具体的代码示例展示其优势。
47 11
|
7天前
|
存储 负载均衡 Java
Jetty技术深度解析及其在Java中的实战应用
【9月更文挑战第3天】Jetty,作为一款开源的、轻量级、高性能的Java Web服务器和Servlet容器,自1995年问世以来,凭借其卓越的性能、灵活的配置和丰富的扩展功能,在Java Web应用开发中占据了举足轻重的地位。本文将详细介绍Jetty的背景、核心功能点以及在Java中的实战应用,帮助开发者更好地理解和利用Jetty构建高效、可靠的Web服务。
21 2
|
1天前
|
搜索推荐 Linux Android开发
深入解析安卓与iOS系统架构设计差异
本文旨在探讨Android和iOS两大主流操作系统在架构设计上的根本差异。通过分析两种系统的设计理念、核心组件以及实际应用表现,揭示它们如何反映不同的开发哲学和用户体验策略。我们将从系统层级结构、内存管理机制、用户界面设计三个方面入手,逐一对比Android的开放性和灵活性如何与其对手iOS的封闭性和一致性相互辉映。
|
6天前
|
消息中间件 安全 Kafka
Kafka支持SSL/TLS协议技术深度解析
SSL(Secure Socket Layer,安全套接层)及其继任者TLS(Transport Layer Security,传输层安全)是为网络通信提供安全及数据完整性的一种安全协议。这些协议在传输层对网络连接进行加密,确保数据在传输过程中不被窃取或篡改。
15 0
|
6天前
|
分布式计算 Java Apache
Apache Spark Streaming技术深度解析
【9月更文挑战第4天】Apache Spark Streaming是Apache Spark生态系统中用于处理实时数据流的一个重要组件。它将输入数据分成小批次(micro-batch),然后利用Spark的批处理引擎进行处理,从而结合了批处理和流处理的优点。这种处理方式使得Spark Streaming既能够保持高吞吐量,又能够处理实时数据流。
23 0

推荐镜像

更多