0
点赞
收藏
分享

微信扫一扫

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

诗与泡面 2021-09-29 阅读 77
更新:关于Swift的动态性补充
一、关于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次补救的机会:

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

先讲解大家比较熟悉的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中如何实现消息转发,在面试中也可以谈谈自己的理解。

举报

相关推荐

0 条评论