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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 在开发中 `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

参考文献:

相关文章
|
7天前
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
53 22
解析静态代理IP改善游戏体验的原理
|
4天前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
48 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
1天前
|
JavaScript 前端开发 Android开发
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
30 13
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
3天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
23 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
3天前
|
数据采集 JavaScript Android开发
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
26 7
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
12天前
|
机器学习/深度学习 人工智能 算法
DeepSeek技术报告解析:为什么DeepSeek-R1 可以用低成本训练出高效的模型
DeepSeek-R1 通过创新的训练策略实现了显著的成本降低,同时保持了卓越的模型性能。本文将详细分析其核心训练方法。
308 11
DeepSeek技术报告解析:为什么DeepSeek-R1 可以用低成本训练出高效的模型
|
1月前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
|
5天前
|
人工智能 自然语言处理 算法
DeepSeek模型的突破:性能超越R1满血版的关键技术解析
上海AI实验室周伯文团队的最新研究显示,7B版本的DeepSeek模型在性能上超越了R1满血版。该成果强调了计算最优Test-Time Scaling的重要性,并提出了一种创新的“弱到强”优化监督机制的研究思路,区别于传统的“从强到弱”策略。这一方法不仅提升了模型性能,还为未来AI研究提供了新方向。
234 5
|
1月前
|
缓存 算法 Oracle
深度干货 如何兼顾性能与可靠性?一文解析YashanDB主备高可用技术
数据库高可用(High Availability,HA)是指在系统遇到故障或异常情况时,能够自动快速地恢复并保持服务可用性的能力。如果数据库只有一个实例,该实例所在的服务器一旦发生故障,那就很难在短时间内恢复服务。长时间的服务中断会造成很大的损失,因此数据库高可用一般通过多实例副本冗余实现,如果一个实例发生故障,则可以将业务转移到另一个实例,快速恢复服务。
深度干货  如何兼顾性能与可靠性?一文解析YashanDB主备高可用技术
|
1月前
|
Kubernetes Linux 虚拟化
入门级容器技术解析:Docker和K8s的区别与关系
本文介绍了容器技术的发展历程及其重要组成部分Docker和Kubernetes。从传统物理机到虚拟机,再到容器化,每一步都旨在更高效地利用服务器资源并简化应用部署。容器技术通过隔离环境、减少依赖冲突和提高可移植性,解决了传统部署方式中的诸多问题。Docker作为容器化平台,专注于创建和管理容器;而Kubernetes则是一个强大的容器编排系统,用于自动化部署、扩展和管理容器化应用。两者相辅相成,共同推动了现代云原生应用的快速发展。
191 11

热门文章

最新文章

推荐镜像

更多