runtime学习(三):消息发送和消息转发

注:本文不是原创,只是在学习中做的整理和笔记,以便自己以后更好的复习。原文来自runtime从入门到精通系列以及深入理解Objective-C的Runtime机制。

前面从objc_msgSend作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入消息发送和消息转发这两个正题。

消息发送

当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
例如:

[person read: book];
// 会被编译成:
objc_msgSend(person, @selector(read:), book);

现在让我们看一下objc_msgSend它具体是如何发送消息

  1. 首先根据receiver对象的isa指针获取它对应的class;
  2. 优先在class的cache查找message方法,如果找不到,再到methodLists查找;
  3. 如果没有在class找到,就沿着继承体系继续向上到super_class查找;
  4. 一旦找到message这个方法,就执行它实现的IMP;
  5. 找不到, 执行”消息转发”。
runtime学习(三):消息发送和消息转发_第1张图片
Objc消息发送.jpg

方法解析与消息转发

[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序,即消息转发,消息转发的流程如下:

  1. 动态方法解析(Method Resolution) : 先问接收者所属的类, 你看能不能动态添加个方法来处理这个”未知的消息”? 如果能, 则消息转发结束.
  2. 后备接收者(Fast Forwarding): 请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.
  3. 完整的消息转发(Normal Forwarding) : 首先调用 methodSignatureForSelector:进行方法签名 ,获取函数的参数和返回值,如果返回为nil,程序会Crash掉。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。 再问接收者一次, 快想办法把这个搞定了. 到了这个地步如果还无法处理, 消息转发机制也无能为力了。
runtime学习(三):消息发送和消息转发_第2张图片
消息转发.png

1. 动态方法解析(Method Resolution):

对象在收到无法解读的消息后, 首先在运行时调用+ resolveInstanceMethod:+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

// selector : 那个未知的选择子
// 返回YES则结束消息转发
// 返回NO则进入备胎
+ (BOOL)resolveInstanceMethod:(SEL)selector

/// 尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法
+ (BOOL)resolveClassMethod:(SEL)selector

举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:

// .h文件
@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end

// .m文件
@implementation Message
- (void)sendMessage:(NSString *)word
{
    NSLog(@"normal way : send message = %@", word);
}
@end

如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    Message *message = [Message new];
    [message sendMessage:@"Sam Lau"];
}

控制台会打印以下信息:

normal way : send message = Sam Lau

但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:

#pragma mark - Method Resolution
/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(sendMessage:)) {
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
            NSLog(@"method resolution way : send message = %@", word);
        }), "v@*");
    }
    return YES;
}

控制台就会打印以下信息:

method resolution way : send message = Sam Lau

注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings。

如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)。

2. 后备接收者(Fast Forwarding):

动态方法解析失败, 目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。

// selector : 那个未知的消息
// 返回一个能响应该未知选择子的后备对象
- (id)forwardingTargetForSelector:(SEL)selector

继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:

#pragma mark - Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(sendMessage:)) {
        return [MessageForwarding new];
    }
    return nil;
}

此时还缺一个转发消息的后备接收类MessageForwarding,这个类的设计与实现如下:

@interface MessageForwarding : NSObject
- (void)sendMessage:(NSString *)word;
@end
@implementation MessageForwarding
- (void)sendMessage:(NSString *)word
{
    NSLog(@"fast forwarding way : send message = %@", word);
}
@end

此时,控制台会打印以下信息:

fast forwarding way : send message = Sam Lau

这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

3. 完整的消息转发(Normal Forwarding)

如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用- methodSignatureForSelector:返回一个方法签名,来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用- forwardInvocation:方法。

// NSMethodSignature : 该selector对应的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

// invocation : 封装了与那条尚未处理的消息相关的所有细节的对象
- (void)forwardInvocation:(NSInvocation *)invocation

继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:

#pragma mark - Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    MessageForwarding *messageForwarding = [MessageForwarding new];
    if ([messageForwarding respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding];
    }
}

上面这4个方法均是模板方法,开发者可以override,由runtime来调用.

三种方法的选择

Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?

  • 动态方法解析(Method Resolution):由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  • 后备接收者(Fast Forwarding):它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
  • 完整的消息转发(Normal Forwarding):它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

runtime其他补充

Associated Objects(关联)

当使用Category对某个类进行扩展时,有时需要存储属性,Category是不支持的,这时需要使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个API来向对象添加、获取和删除关联值:

void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
id objc_getAssociatedObject (id object, const void *key )
void objc_removeAssociatedObjects (id object )

其中objc_AssociationPolicy是个枚举类型,它可以指定Objc内存管理的引用计数机制。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

下面有个关于NSObject+AssociatedObject Category添加属性associatedObject的示例代码:

// NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject)
@property (strong, nonatomic) id associatedObject;
@end
// NSObject+AssociatedObject.m

@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)associatedObject
{
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject
{
    return objc_getAssociatedObject(self, _cmd);
}
@end

Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。_cmd表示当前调用方法,其实它就是一个方法选择器SEL。

Method Swizzling

Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。如果能够利用好这个技巧,可以写出简洁、有效且维护性更好的代码。可以参考两篇关于Method Swizzling技巧的文章:

  • nshipster Method Swizzling
  • Method Swizzling和AOP实践
危险性

Method Swizzling就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。在stackoverflow上有人已经提出这样一个问题:What are the Dangers of Method Swizzling in Objective C?,它的危险性主要体现以下几个方面:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

总结

虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。

扩展阅读

  • 玉令天下博客的Objective-C Runtime
  • 顾鹏博客的Objective-C Runtime
  • Associated Objects
  • Method Swizzling
  • Method Swizzling和AOP实践
  • Objective-C Runtime Reference
  • What are the Dangers of Method Swizzling in Objective C?
  • iOS程序员6级考试(答案和解释)

你可能感兴趣的:(runtime学习(三):消息发送和消息转发)