更新:关于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中如何实现消息转发,在面试中也可以谈谈自己的理解。
如果有错误,恳请指正。