Runtime 消息传递、转发机制(OC&Swift )

更新:关于Swift的动态性补充

参考:Objective-C 的运行时以及 Swift 的动态性
现在让我们来谈谈 Swift 吧。Swift 是一种强类型语言。类型静态,也就是说 Swift 的默认类型是非常安全的。如果需要的话,不安全类型也是存在的,但是 Swift 仍然是尽力推动我们使用安全的静态类型。Swift 中的动态性可以通过 Objective-C 运行时来获得。

本来这是很好的,但是 Swift 开源并迁移到 Linux 之后,由于 Linux 上的 Swift 并不提供 Objective-C 运行时,事情就大条了。社区的关键点在于,让 Swift 未来能够自己配备动态性,而不是依赖于 Apple。

也就是说,Swift 当中存在有这两个修饰符 @objc 和 dynamic,此外我们同样还可以访问 NSObject。@objc 将您的 Swift API 暴露给 Objective-C 运行时,但是它仍然不能保证编译器会尝试对其进行优化。如果您真的想使用动态功能的话(例如用KVO监听值的变化时),就需要使用 dynamic。

一、关于Runtime

本文意在介绍Runtime消息转发的OC&Swift写法,并不是一个Runtime详解的文章。

但是这里依然要简单例举一下Runtime学习的关键字,如果感兴趣的话可以通过下面关键字进行检索和学习:

  • Runtime开源,在这里下到苹果维护的开源代码
  • Runtime的两个版本和区别
  • Swift是否具备运行时特性?

其次,例举Runtime在项目中主要的应用:

  • 消息转发机制
  • 分类扩展属性
  • 序列化和反序列化
  • Method Swizzling(AOP) 和 isa Swizzling(KVO)
  • 关于Swift反射
二、消息传递

person 实例调用方法 eat 为例:

[person eat] ---> objc_msgSend(person, eat)

Runtime时执行的流程是这样的:
1 首先,通过 person 的 isa 指针找到它的 class
2 在 class 的 method list 找 eat
3 如果 class 中没到 eat,继续往它的 superclass 中找
4 一旦找到 eat 这个函数,就去执行它的实现 IMP

如果 superclass 中没找到 eat,会继续向父类去寻找,直到 root class(NSObject) 中依然没找到的话,程序会发生崩溃:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x6080000135d0'

当然,在程序崩溃之前,系统会给你3个机会进行补救,这个过程就成为 消息转发

三、消息转发

下图是完整的消息转发的流程图,上面也提到了,系统一共给你提供了3次补救的机会:

  • 动态方法解析
  • 快速消息转发
  • 正常消息转发

1.进入 resolveInstanceMethod: 方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕。

2.resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步,返回某个对象,则会调用该对象的方法。

3.若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步。

4.当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等。

如果到最后,消息还是没有得到响应,程序就会crash

先讲解大家比较熟悉的OC写法,随后讲讲Swift中的区别。

3.1 动态方法解析

动态方法解析确切的说还不属于消息转发的过程,是在消息转发之前对实例方法或类方法进行补救。
实例方法解析对应: resolveInstanceMethod: ,类方法解析对应:resolveClassMethod:

#import <objc/runtime.h>
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *methodName = NSStringFromSelector(sel);
    if ([methodName isEqualToString:@"eat:"]) {
        return class_addMethod(self, sel, (IMP)addMethod, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}

void addMethod(id self, SEL _cmd, NSString * something) {
    NSLog(@"eat: %@", something);
}

这里利用runtime动态添加了一个c函数进行补救,对于方法class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)不熟悉的朋友可以通过官方文档查阅一下。

“v@:@”是什么?
它是函数的签名类型,用来描述函数的返回值和参数
每一个函数会默认两个隐藏参数:self_cmd
self代表方法的调用者,_cmd代表方法的SEL
上面代码中的“v@:@”分别表示:v代表返回值为void,第一个@代表self,:代表_cmd,最后一个@代表 eat:方法的参数

3.2 快速消息转发

快速消息转发就是在继承树中寻找不到目标方法时,你可以快速指定一个其他类去实现这个方法。
例如本文中Person类没有对eat:方法进行实现,但是你声明了一个Man类,在Man的.m文件中对eat:进行了实现,你就可以通过快速消息转发,把这个消息丢给其他类去处理。

@interface Man : NSObject
@end

@implementation Man
- (void)eat:(NSString *)something {
    NSLog(@"man eat: %@", something);
}
@end
----------------
// fast forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"eat:"]) {
        return [Man new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
3.3 正常消息转发

正常消息转发分为两个步骤:1 方法签名 2 消息转发(指定消息接收者)

// normal forwarding
// 1 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString *methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"eat:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

这里签名的types与上面动态添加方法的签名一致,不明白的可以返回去再看看。签名完成之后,就剩最后一步消息转发了,当然最后这个消息转发也有不同的实现方式:例如转发给其他类进行实现,或者是在本类中改变方法选择器

// 2 消息转发 - 指定消息的接收者为Man类,并在Man类中实现eat:方法
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Man *man = [Man new];
    if ([man respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:man];
        return;
    }
    [super forwardInvocation:anInvocation];
}

// 2 消息转发 - 指定消息接收者为self,并指定方法选择器
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation setSelector:@selector(unKnown:)];
    [anInvocation invokeWithTarget:self];
}
- (void)unKnown:(NSString *)something {
    NSLog(@"%@", something);
}

最后,假如你只进行了方法签名,但是并没有实现forwardInvocation:方法,系统会在最后执行doesNotRecognizeSelector:方法,保证程序不会直接崩溃。

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"doesNotRecognizeSelector:");
}

到此,补救程序崩溃的3个机会就全部介绍完毕了。这就是消息转发的完整过程的OC写法。

四、Swift写法

我们都知道,Objective-C有运行时机制,具备动态性,但是Swift没有。它是继承自Objective-C的runtime机制,才获取了动态性。当然两者在runtime的使用上也有很多区别之处,我们也许很熟悉OC的消息传递和转发机制,但是你用Swift写过消息转发吗?

只是在Swift4.0中,去除了methodSignatureForSelector:forwardInvocation:这两个方法,
观察NSObject类也能发现,在Swift中只有动态方法解析和快速消息转发可以去实现了。
ps: 这里@available(iOS 2.0, *) 与Runtime版本有关,感兴趣的可以自己去百度一下。

@available(iOS 2.0, *)
open func forwardingTarget(for aSelector: Selector!) -> Any?

@available(iOS 2.0, *)
open class func resolveClassMethod(_ sel: Selector!) -> Bool
@available(iOS 2.0, *)
open class func resolveInstanceMethod(_ sel: Selector!) -> Bool

所以要Swift实现消息转发,首先要继承NSObject
假如在Swift中调用一个不存在的方法,可以用如下代码:

Person().perform(Selector("run"))

消息转发的代码如下,基本就是这样写,因为Swift中没办法写C语言函数,所以只能通过Method获取IMP和types签名。注意调用OC方法时,要添加@objc关键字。

import Foundation

class Animal : NSObject {
    @objc func run() {
        print("run")
    }
}

class Person : NSObject {
    // 动态方法解析
    override class func resolveInstanceMethod(_ sel: Selector!) -> Bool {
        guard let method = class_getInstanceMethod(self, #selector(runIMP))  else {
            return super.resolveInstanceMethod(sel)
        }
        return class_addMethod(self, Selector("run"), method_getImplementation(method), method_getTypeEncoding(method))
    }
    @objc func runIMP() {
        print("runIMP")
    }
    
    // 快速消息转发
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return Animal()
    }
}
五、总结

消息转发机制的应用场景:
1 如何拯救不存在的方法调用?避免程序崩溃
2 解决Timer对self的强引用问题?避免控制器无法释放

本文比较浅显,多在介绍代码的写法,多少了解一下OC和Swift中如何实现消息转发,在面试中也可以谈谈自己的理解。

如果有错误,恳请指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 一、OC语言的特性 首先,想要了解iOS的消息发送机制,我们需要先理解OC这门语言。相较于静态语言而言,动态语言是...
    学知无涯阅读 773评论 0 9
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 感恩父母赐予我生命辛苦将我养育! 感恩我家婆的付出! 感恩我先生的包容与支持 感恩顾客的信任! 感恩钱宝宝的陪伴!...
    彭焱娟阅读 188评论 0 0
  • 女孩:性格有些内敛,没谈过恋爱,学霸… 男孩:谈过一次,幽默风趣,有上进心,喜欢打篮球…… 她与他相识...
    小小叮阅读 181评论 0 0