iOS - 面试宝典

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

2018-07-19  本文已影响64人  YYYYYY25
更新:关于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在项目中主要的应用:

二、消息传递

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中如何实现消息转发,在面试中也可以谈谈自己的理解。

如果有错误,恳请指正。

上一篇 下一篇

猜你喜欢

热点阅读