Runtime的消息发送和转发

前面从objc_msgSend作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入消息发送这个正题。体会苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。

objc_msgSend函数

在前面已经提过,当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:

id objc_msgSend ( id self, SEL op, ... );  

这里要清楚一点,objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):现在让我们看一下objc_msgSend它具体是如何发送消息:

  1. 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
  1. 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
  2. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
  3. 如果 cache 找不到就找类的方法列表中是否有对应的方法。
  4. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
  5. 如果还找不到,就要开始进入动态方法解析了,后面会提到。
B2D699D0-7B1C-4140-90F7-56F4E6CA1848.png

在消息的传递中,编译器会根据情况在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper , objc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。

方法中的隐藏参数

疑问:
我们经常用到关键字 self ,但是 self 是如何获取当前方法的对象呢?

其实,这也是 Runtime 系统的作用,self 是在方法运行时被动态传入的。

当 objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:

  • 接受消息的对象(self 所指向的内容,当前方法的对象指针)
  • 方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)

因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们时在代码被编译时被插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。

这两个参数中, self更实用。它是在方法实现中访问消息接收者对象的实例变量的途径。

这时我们可能会想到另一个关键字 super ,实际上super 关键字接收到消息时会转化为objc_msgSendSuper,这个函数定义如下:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)  

objc_msgSendSuper函数第一个参数的数据类型是一个指向objc_super的结构体,这个结构体指明了消息应该被传递给特定的父类,从message.h文件中查看它的定义:

struct objc_super { id receiver; Class class; };

/// Specifies the superclass of an instance.   
struct objc_super {  
    /// Specifies an instance of a class.  
    __unsafe_unretained id receiver;  
  
    /// Specifies the particular superclass of the instance to message.   
#if !defined(__cplusplus)  &&  !__OBJC2__  
    /* For compatibility with old objc-runtime.h header */  
    __unsafe_unretained Class class;  
#else  
    __unsafe_unretained Class super_class;  
#endif  
    /* super_class is the first class to search */  
};  
#endif

结构体包含两个成员,第一个是receiver,receiver 仍然是 self 本身,表示某个类的实例。第二个是super_class表示当前类的父类。

比如,当我们想通过 [super class] ---获取某个类对象的方法,获取父类时,编译器其实是将指向 self 的 id 指针和 class 的 SEL 传递给了 objc_msgSendSuper 函数。只有在 NSObject 类中才能找到 class 方法,然后 class 方法底层被转换为 object_getClass(), 接着底层编译器将代码转换为 objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向 self 的 id 指针,与调用 [self class] 相同,所以我们得到的永远都是 self 的类型。因此你会发现:

// 这句话并不能获取父类的类型,只能获取当前类的类型名
NSLog(@"%@", NSStringFromClass([super class]));
获取方法地址

NSObject 类中有一个实例方法:methodForSelector,你可以用它来获取某个方法选择器对应的 IMP

方法解析与消息转发

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

  1. Method Resolution
  1. Fast Forwarding
  2. Normal Forwarding
7F35C58D-9CC0-4864-A414-A6D379AF4B91.png
Method Resolution

首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

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

@interface Message : NSObject  
- (void)sendMessage:(NSString *)word;  
@end  

@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)。

Fast Forwarding

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

继续上面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会创建它,所以相对于更快点。

Normal Forwarding

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

继续前面的例子,将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];  
    }  
} 
三种方法的选择

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

  1. Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  1. Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
  2. Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
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的示例代码:

@interface NSObject (AssociatedObject)  
@property (strong, nonatomic) id associatedObject;  
@end

@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

Method Swizzling

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

危险性

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

推荐阅读更多精彩内容