首页 > iOS消息转发

iOS消息转发

消息转发是一种功能强大的技术,可以大大增加Objective-C的表现力。什么是消息转发?简而言之,它允许未知的消息被困住并作出反应。换句话说,无论何时发送未知消息,它​​都会以一个很好的包发送到您的代码中,此时您可以随心所欲地执行任何操作。

为什么它被称为 “转发”? 当某个对象没有任何响应某个 消息 的操作就 “转发” 该 消息。原因是这种技术主要是为了让对象让其他对象为他们处理 消息,从而 “转发”。



1. 类,对象,方法

在我们开始使用消息机制之前,我们可以约定我们的术语。例如,很多人不清楚“方法”与“消息”是什么,但这对于理解消息传递系统如何在低级别工作至关重要。

  • 方法:与一个类相关的一段实际代码,并给出一个特定的名字。例:- (int)meaning { return 42; }
  • 消息:发送给对象的名称和一组参数。示例:向0x12345678对象发送meaning并且没有参数。
  • 选择器:表示消息或方法名称的一种特殊方式,表示为类型SEL。选择器本质上就是不透明的字符串,它们被管理,因此可以使用简单的指针相等来比较它们,从而提高速度。(实现可能会有所不同,但这基本上是他们在外部看起来的样子。)例如:@selector(meaning)
  • 消息发送:接收信息并查找和执行适当方法的过程。

1.1 OC的方法与C的函数

Objective-C方法最终被生成为C函数,并带有一些额外的参数。Objective-C中的方法默认被隐藏了两个参数:self_cmd。你可能知道self是作为一个隐式参数传递的,它最终成为一个明确的参数。鲜为人知的隐式参数_cmd(它保存了正在发送的消息的选择器)是第二个这样的隐式参数。总之,self指向对象本身,_cmd指向方法本身。举两个例子来说明:

  • 例1:- (NSString *)name 这个方法实际上有两个参数:self_cmd

  • 例2:- (void)setValue:(int)val 这个方法实际上有三个参数:self,_cmdval

在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 objc_msgSend() 。比如,下面两行代码就是等价的:

  • OC
[array insertObject:foo atIndex:5];
  • C
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

1.2 类,对象,方法的C表达

在 Objective-C 中,类、对象和方法都是一个 C 的结构体,从 objc/runtime.h 以及 objc/objc.h头文件中,我们可以找到他们的定义:

  • objc_class
struct objc_class {Class _Nonnull isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class _Nullable super_class                              OBJC2_UNAVAILABLE;const char * _Nonnull name                               OBJC2_UNAVAILABLE;long version                                             OBJC2_UNAVAILABLE;long info                                                OBJC2_UNAVAILABLE;long instance_size                                       OBJC2_UNAVAILABLE;struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
  • objc_object
/// Represents an instance of a class.
struct objc_object {Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
  • objc_method
struct objc_method {SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;char * _Nullable method_types                            OBJC2_UNAVAILABLE;IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • objc_method_list
struct objc_method_list {struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__int space                                                OBJC2_UNAVAILABLE;
#endif/* variable length structure */struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}    

1.3 消息发送

在C语言函数中发生了什么事情?编译器是如何找到这个方法的呢?消息发送的主要步骤如下:

  1. 首先检查这个selector是不是要忽略。比如Mac OS X开发,有了垃圾回收就不会理会retain,release这些函数。
  2. 检测这个selector的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。
  3. 如果上面两步都通过了,就开始查找这个类的实现IMP,先从cache里查找,如果找到了就运行对应的函数去执行相应的代码。
  4. 如果cache中没有找到就找类的方法列表中是否有对应的方法。
  5. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。
  6. 如果还是没找到就要开始进入动态方法解析,后面会说

2. 动态特性:方法解析和消息转发

没有方法的实现,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

  • Method resolution
  • Fast forwarding
  • Normal forwarding

2.1 动态方法解析: Method Resolution

首先,Objective-C 运行时会调用 + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:

void fooMethod(id obj, SEL _cmd)  
{NSLog(@"Doing foo");
}+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{if(aSEL == @selector(foo:)){class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");return YES;}return [super resolveInstanceMethod]; } 

这里第一字符v代表函数返回类型void,第二个字符@代表self的类型id,第三个字符:代表_cmd的类型SEL。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,更详细的官方文档传送门 在这里,此处不再列举了。



2.2 快速转发: Fast Rorwarding

消息转发机制执行前,runtime系统允许我们替换消息的接收者为其他对象。通过- (id)forwardingTargetForSelector:(SEL)aSelector方法。如果此方法返回的是nil 或者self,则会进入消息转发机制(- (void)forwardInvocation:(NSInvocation *)invocation),否则将会向返回的对象重新发送消息。

- (id)forwardingTargetForSelector:(SEL)aSelector {if(aSelector == @selector(foo:)){return [[BackupClass alloc] init];}return [super forwardingTargetForSelector:aSelector];
}

2.3 消息转发: Normal Forwarding

- (void)forwardInvocation:(NSInvocation *)invocation {SEL sel = invocation.selector;if([alternateObject respondsToSelector:sel]) {[invocation invokeWithTarget:alternateObject];} else {[self doesNotRecognizeSelector:sel];}
}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];if (!methodSignature) {methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];}return methodSignature; } 

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。

其中,参数invocation是从哪来的?在forwardInvocation:消息发送前,runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都继承了forwardInvocation:方法,我们可以将消息转发给其它的对象。

3. 应用实战:消息转发

3.1 特定奔溃预防处理

下面有一段因为没有实现方法而会导致奔溃的代码:

  • Test2ViewController
- (void)viewDidLoad {[super viewDidLoad];[self.view setBackgroundColor:[UIColor whiteColor]];self.title = @"Test2ViewController";//实例化一个button,未实现其方法UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];button.frame = CGRectMake(50, 100, 200, 100);button.backgroundColor = [UIColor blueColor];[button setTitle:@"消息转发" forState:UIControlStateNormal]; [button addTarget:self action:@selector(doSomething) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } 

为解决这个问题,可以专门创建一个处理这种问题的分类:

  • NSObject+CrashLogHandle
#import "NSObject+CrashLogHandle.h"@implementation NSObject (CrashLogHandle)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {//方法签名return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}- (void)forwardInvocation:(NSInvocation *)anInvocation {NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}@end

因为在category中复写了父类的方法,会出现下面的警告:



解决办法就是在Xcode的Build Phases中的资源文件里,在对应的文件后面 -w ,忽略所有警告。



3.2 苹果系统API迭代造成的奔溃处理

3.2.1 兼容系统API迭代的传统方案

随着每年iOS系统与硬件的更新迭代,部分性能更优异或者可读性更高的API将有可能对原有API进行废弃与更替。与此同时我们也需要对现有APP中的老旧API进行版本兼容,当然进行版本兼容的方法也有很多种,下面笔者会列举常用的几种:

  • 根据能否响应方法进行判断
if ([object respondsToSelector: @selector(selectorName)]) {//using new API
} else {//using deprecated API
}
  • 根据当前版本SDK是否存在所需类进行判断
if (NSClassFromString(@"ClassName")) {    //using new API
}else {//using deprecated API
}
  • 根据操作系统版本进行判断
#define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) {majorVersion,minorVersion,patchVersion
}]if (isOperatingSystemAtLeastVersion(11, 0, 0)) {//using new API
} else {//using deprecated API
}
3.2.2 兼容系统API迭代的新方案

**需求:**假设现在有一个过去写好的类,如下所示,其中有一行因为系统API过时导致奔溃的代码:

  • Test3ViewController.m
- (void)viewDidLoad {[super viewDidLoad];[self.view setBackgroundColor:[UIColor whiteColor]];self.title = @"Test3ViewController";UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain];tableView.delegate = self;tableView.dataSource = self;tableView.backgroundColor = [UIColor orangeColor];// May Crash LinetableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];[self.view addSubview:tableView];
}

其中有一行会发出警告,Xcode也给出了推荐解决方案,如果你点击Fix它会自动添加检查系统版本的代码,如下图所示:



**方案1:**手动加入版本判断逻辑

以前的适配处理,可根据操作系统版本进行判断

if (isOperatingSystemAtLeastVersion(11, 0, 0)) {scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {viewController.automaticallyAdjustsScrollViewInsets = NO;
}

**方案2:**消息转发

在iOS11 Base SDK直接采取最新的API并且配合Runtime的消息转发机制就能实现一行代码在不同版本操作系统下采取不同的消息调用方式

  • UIScrollView+Forwarding.m
#import "UIScrollView+Forwarding.h"
#import "NSObject+AdapterViewController.h"@implementation UIScrollView (Forwarding)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1NSMethodSignature *signature = nil;if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]; }else { signature = [super methodSignatureForSelector:aSelector]; } return signature; } - (void)forwardInvocation:(NSInvocation *)anInvocation { // 2 BOOL automaticallyAdjustsScrollViewInsets = NO; UIViewController *topmostViewController = [self cm_topmostViewController]; NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3 [viewControllerInvocation setTarget:topmostViewController]; [viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]; [viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4 [viewControllerInvocation invokeWithTarget:topmostViewController]; // 5 } @end 
  • NSObject+AdapterViewController.m
#import "NSObject+AdapterViewController.h"@implementation NSObject (AdapterViewController)- (UIViewController *)cm_topmostViewController {UIViewController *resultVC;resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];while (resultVC.presentedViewController) {resultVC = [self cm_topViewController:resultVC.presentedViewController];}return resultVC;
}- (UIViewController *)cm_topViewController:(UIViewController *)vc {if ([vc isKindOfClass:[UINavigationController class]]) {return [self cm_topViewController:[(UINavigationController *)vc topViewController]]; } else if ([vc isKindOfClass:[UITabBarController class]]) { return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]]; } else { return vc; } } @end 

当我们在iOS10调用新API时,由于没有具体对应API实现,我们将其原有的消息转发至当前栈顶UIViewController去调用低版本API。

关于[self cm_topmostViewController];,执行之后得到的结果可以查看如下:



方案2的整体流程:

  1. 为即将转发的消息返回一个对应的方法签名(该签名后面用于对转发消息对象(NSInvocation *)anInvocation进行编码用)

  2. 开始消息转发((NSInvocation *)anInvocation封装了原有消息的调用,包括了方法名,方法参数等)

  3. 由于转发调用的API与原始调用的API不同,这里我们新建一个用于消息调用的NSInvocation对象viewControllerInvocation并配置好对应的target与selector

  4. 配置所需参数:由于每个方法实际是默认自带两个参数的:self和_cmd,所以我们要配置其他参数时是从第三个参数开始配置

  5. 消息转发

3.2.3 验证对比新方案

注意测试的时候,选择iOS10系统的模拟器进行验证(没有的话可以先Download Simulators),安装完后如下如选择:



  • 不注释并导入UIScrollView+Forwarding类


  • 注释掉UIScrollView+Forwarding的功能代码

会如下图所示奔溃:



4. 总结

4.1 模拟多继承

面试挖坑:OC是否支持多继承?好,你说不支持多继承,那你有没有模拟多继承特性的办法?

转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

虽然转发可以实现继承功能,但是NSObject还是必须表面上很严谨,像respondsToSelector:isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。

4.2 消息机制总结

Objective-C 中给一个对象发送消息会经过以下几个步骤:

  1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;

  2. 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;

  3. 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;

  4. 如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector:-forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

转载于:https://www.cnblogs.com/weiboyuan/p/8983797.html

更多相关:

  • 首先对微擎的工作原理做简单描述, 微擎使用规则和模块的机制来处理公众平台的请求数据并返回响应的结果.执行流程描述为: 粉丝用户与公众号码进行对话或交互, 而后公众平台将粉丝用户的请求消息(当前包括: 文本, 图片, 位置, 链接, 事件. 请参阅消息类型)传递给微擎系统, 微擎系统按照消息类型和对应的公众号所设定的规则列表匹配到合适的...

  • 消息队列的使用场景以下介绍消息队列在实际应用常用的使用场景。异步处理、应用解耦、流量削锋和消息通讯四个场景。1】异步处理:场景说明:用户注册后,需要发注册邮件和注册短信。引入消息队列后架构如下:用户的响应时间=注册信息写入数据库的时间,例如50毫秒。发注册邮箱、发注册短信写入消息队列后,直接返回客户端,因写入消息队列的速度很快,基...

  • 下面是我凭记忆想到的几个题目,有需要的同学就拿去吧,我也算做了点善事. 中体骏彩C++笔试题 2013-11-18 1.指针的含义是:B A.名字 B.地址 C.名称 D.符号 2.给出下面的程序输出: #include #include #include ...

  • 双端通信描述 利用消息队列针对发送接受消息的类型唯一性 进行多个客户端之间消息传递,而不需要server端进行消息转发。 同时消息队列的读阻塞和写阻塞特性(消息队列中已经写入数据,如果再不读出来,则无法再次写入)让消息队列的实现过程只能如下: 客户端1的父进程用来处理类型1的消息写,子进程处理类型2的消息读客户端2的父进程处理类型...

  • 文章目录基本介绍编程接口代码实例消息队列的发送和接收消息队列中的消息对象的属性控制 基本介绍 支持不同进程之间以消息(messages)的形式进行数据交换,消息能够拥有自己的标识,且内核使用链表方式进行消息管理。进程之间的通信角色为:发送者和接受者 发送者: a. 获取消息队列的ID(key或者msgid) b. 将数据放入...

  • 来源:公众号|计算机视觉工坊(系投稿)作者:仲夏夜之星「3D视觉工坊」技术交流群已经成立,目前大约有12000人,方向主要涉及3D视觉、CV&深度学习、SLAM、三维重建、点云后处理、自动驾驶、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、学术交流、...

  • 点云PCL免费知识星球,点云论文速读。文章:Real-Time LIDAR-Based Urban Road and Sidewalk Detection for Autonomous Vehicles作者:Ern˝o Horváth  , Claudiu Pozna ,and Miklós Unger编译:点云PCL代码:http...

  • 文章:Semantic Histogram Based Graph Matching for Real-Time Multi-Robot Global Localization in Large Scale Environment作者:Xiyue Guo, Junjie Hu, Junfeng Chen, Fuqin Deng, T...

  • 点云PCL免费知识星球,点云论文速读。文章:Robust Place Recognition using an Imaging Lidar作者:Tixiao Shan, Brendan Englot, Fabio Duarte, Carlo Ratti, and Daniela Rus编译:点云PCL(ICRA 2021)开源代码:...

  • 文章:A Survey of Calibration Methods for Optical See-Through Head-Mounted Displays作者:Jens Grubert , Yuta Itoh, Kenneth Moser编译:点云PCL本文仅做学术分享,如有侵权,请联系删除。欢迎各位加入免费知识星球,获取PD...

  • 点云PCL免费知识星球,点云论文速读。文章:DSP-SLAM: Object Oriented SLAM with Deep Shape Priors作者:Jingwen Wang Martin Runz Lourdes Agapito编译:点云PCL代码:https://github.com/JingwenWang95/DSP-S...

  • RAM缓存 新RAM缓存算法(CLFUS) 新的RAM缓存使用的创意来自许多缓存替换策略和算法,包括LRU,LFU,CLOCK,GDFS及2Q,它被命名为时钟周期内最小频繁使用大小算法CLFUS(Clocked Least Frequently Used by Size)。它避开了任何专利算法,具有如下特性: 均衡最近性(Rec...

  • MP4 |视频:AVC,1280×720 30 fps |音频:AAC,48 KHz,2 Ch |时长:2h 12m 语言:英语+中英文字幕(根据原英文字幕机译更准确)|大小解压后:560M C4D是一个有抱负的运动图形艺术家和设计师的重要工具。借助C4D,您可以使用3D对象、动态效果和动画来增强运动图形、模型和可视化效果。本课...

  • 文章目录先说问题:再说解决尝试1:尝试2(该尝试建议先在自己环境搭配对应业务测试通过后再现场尝试): 感谢 学无止境996同学的陪伴和vigourtyy美丽女友的支持,直到这个解决问题的深夜 先说问题: ceph 12.2.1生产环境:3副本 tier + 3副本data 机房在拥有业务的情况下重启集群交换机,产生如下场景...

  • 这周主要学习了java中的类和对象的知识点,发现和C++中的类和对象极为相似,对于类和对象的概念理解起来也简单。同时在自学的过程中也把类的知识重新复习巩固了一下(如类的三大特征:继承,封装和多态,构造,成员对象的访问权限,构造,无参有参函数的调用等),同时也了解到一些新的概念,比如类对象创建和引用占据堆内存和栈内存,输出对象时默认调...