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

参考文献:

相关文章
|
12天前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术深度解析:从基础到应用的全面介绍
人工智能(AI)技术的迅猛发展,正在深刻改变着我们的生活和工作方式。从自然语言处理(NLP)到机器学习,从神经网络到大型语言模型(LLM),AI技术的每一次进步都带来了前所未有的机遇和挑战。本文将从背景、历史、业务场景、Python代码示例、流程图以及如何上手等多个方面,对AI技术中的关键组件进行深度解析,为读者呈现一个全面而深入的AI技术世界。
77 10
|
19天前
|
机器学习/深度学习 人工智能 自然语言处理
秒级响应 + 99.9%准确率:法律行业文本比对技术解析
本工具基于先进AI技术,采用自然语言处理和语义匹配算法,支持PDF、Word等格式,实现法律文本的智能化比对。具备高精度语义匹配、多格式兼容、高性能架构及智能化标注与可视化等特点,有效解决文本复杂性和法规更新难题,提升法律行业工作效率。
|
16天前
|
数据采集 存储 JavaScript
网页爬虫技术全解析:从基础到实战
在信息爆炸的时代,网页爬虫作为数据采集的重要工具,已成为数据科学家、研究人员和开发者不可或缺的技术。本文全面解析网页爬虫的基础概念、工作原理、技术栈与工具,以及实战案例,探讨其合法性与道德问题,分享爬虫设计与实现的详细步骤,介绍优化与维护的方法,应对反爬虫机制、动态内容加载等挑战,旨在帮助读者深入理解并合理运用网页爬虫技术。
|
22天前
|
机器学习/深度学习 自然语言处理 监控
智能客服系统集成技术解析和价值点梳理
在 2024 年的智能客服系统领域,合力亿捷等服务商凭借其卓越的技术实力引领潮流,它们均积极应用最新的大模型技术,推动智能客服的进步。
59 7
|
25天前
|
存储 安全 数据安全/隐私保护
深入解析iOS 14隐私保护功能:用户数据安全的新里程碑
随着数字时代的到来,个人隐私保护成为全球关注的焦点。苹果公司在最新的iOS 14系统中引入了一系列创新的隐私保护功能,旨在为用户提供更透明的数据使用信息和更强的控制权。本文将深入探讨iOS 14中的几项关键隐私功能,包括App跟踪透明性、简化的隐私设置以及增强的系统安全性,分析它们如何共同作用以提升用户的隐私保护水平。
79 3
|
26天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
27天前
|
负载均衡 网络协议 算法
Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式
本文探讨了Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式,以及软件负载均衡器、云服务负载均衡、容器编排工具等实现手段,强调两者结合的重要性及面临挑战的应对措施。
64 3
|
1月前
|
网络协议 网络性能优化 数据处理
深入解析:TCP与UDP的核心技术差异
在网络通信的世界里,TCP(传输控制协议)和UDP(用户数据报协议)是两种核心的传输层协议,它们在确保数据传输的可靠性、效率和实时性方面扮演着不同的角色。本文将深入探讨这两种协议的技术差异,并探讨它们在不同应用场景下的适用性。
71 4
|
1月前
|
安全 持续交付 Docker
深入理解并实践容器化技术——Docker 深度解析
深入理解并实践容器化技术——Docker 深度解析
57 2
|
1月前
|
数据安全/隐私保护 iOS开发 开发者
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####

热门文章

最新文章

推荐镜像

更多