IOS 消息传递与消息转发

1、方法method和selector(选择子)有什么关系

在 Objective-C 中,selector,Method 和 implementation(IMP) 都是 Runtime 的组成部分。在实际开发中它们常常是可以相互转换来处理消息的发送的。选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。
得出结论:选择子其实是方法的名称,不同类中方法名相同参数不同的俩个方法,他们的选择子是相同的。

Method的结构体

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};
  • 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。(即类型编码)
  • method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。

消息传递

在OC中,给对象发送消息的语法是:

id returnValue = [someObject messageName:parameter];

这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)

第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的·,所以这个函数就是参数个数可变的函数。

因此,上述以OC形式展现出来的函数就会转化成如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

可以看出,在调用方法时,编译器将它转成了objc_msgSend消息发送了,在Runtime的执行过程如下

  • 1、Runtime先通过对象someobject找到isa指针,判断isa指针是否为nil,为nil直接return。
  • 2、若不为空则通过isa指针找到当前实例的类对象,在类对象下查找缓存是否有messageName方法。
  • 3、若在类对象缓存中找到messageName方法,则直接调用IMP方法(本质上是函数的指针)。
  • 4、若在类对象缓存中没找到messageName方法,则查找当前类对象的方法列表methodlist,若找到方法则将其添加到类对象的缓存中。
  • 5、若在类对象方法列表中没找到messageName方法,则继续到当前类的父类中以相同的方式查找(即类的缓存->类的方法列表)。
  • 6、若在父类中找到messageName方法,则将IMP添加到类对象缓存中。
  • 7、若在父类中没找到messageName方法,则继续查询父类的父类,直到追溯到最上层NSObject
  • 8、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
  • 9、若还是没找到,则Runtime会抛出异常doesNotRecognizeSelector

综上,方法的查询流程基本就是查询类对象中的缓存和方法列表->父类中的缓存和方法列表->父类的父类中的缓存和方法列表->...->NSObject中的缓存和方法列表->动态方法解析->备用接收者->消息转发

消息动态解析

Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。前者在 对象方法未找到时 调用,后者在 类方法未找到时 调用。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。

主要用的的方法如下:

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有给定名称和实现的类中添加新方法
 * @param cls         被添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);

测试代码

main.m 文件中
  int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    Person *xiaoming = [[Person alloc]init];
    [xiaoming performSelector:@selector(way)];
    return 0;
    
}

person.m 文件中
  // 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(way)) {
        class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod动态添加方法method
    }
    return YES;
}
- (void)method{
    NSLog(@"进入了消息动态解析");
}
运行了上述的测试代码后,我们会发现即便我们并没有实现way方法,而且使用了performSelector去强行调用way方法,但是我们的程序并没有崩溃,是因为在查找了类方法和所有的父类后还是没有找到way方法,程序进入了消息动态解析,然后我们使用了class_addMethod去动态添加方法method,最后程序从调用

performSelector和直接调用方法的区别

performSelector: withObject:是在iOS中的一种方法调用方式。他可以向一个对象传递任何消息,而不需要在编译的时候声明这些方法。所以这也是runtime的一种应用方式。

所以performSelector和直接调用方法的区别就在与runtime。直接调用编译是会自动校验。如果方法不存在,那么直接调用 在编译时候就能够发现,编译器会直接报错。
但是使用performSelector的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。所以一般使用performSelector的时候,一般都会使用- (BOOL)respondsToSelector:(SEL)aSelector;来在运行时判断对象是否响应此方法。

消息接受者重定向(备用接受者)

如果上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

如果当前对象实现了 -forwardingTargetForSelector: 或者 +forwardingTargetForSelector: 方法,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。

其中用到的方法。

// 重定向类方法的消息接收者,返回一个类或实例对象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;

注意:

  1. 类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector: 方法,后者是 -forwardingTargetForSelector: 方法。
  2. 这里+resolveInstanceMethod: 或者 +resolveClassMethod:无论是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,运行时都会进行下一步。

测试代码

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        Friends *friends = [[Friends alloc]init];
        return friends;//返回friends对象,让friends对象接受这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

可以看到,虽然当前 person 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。但是我们通过 forwardingTargetForSelector 把当前 person 的方法转发给了 friends 对象去执行了。打印结果也证明我们成功实现了转发。

我们通过 forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程。

消息重定向

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法获取函数的参数和返回值类型。

  • 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
  • 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。

所以我们可以在 forwardInvocation: 方法中对消息进行转发。

注意:类方法和对象方法消息转发第三步调用的方法同样不一样。
类方法调用的是:

  1. + methodSignatureForSelector:
  2. + forwardInvocation:
  3. + doesNotRecognizeSelector:

对象方法调用的是:

  1. - methodSignatureForSelector:
  2. - forwardInvocation:
  3. - doesNotRecognizeSelector:

用到的方法:

// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        return [NSMethodSignature methodSignatureForSelector:@selector(way)];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    Friends *f = [[Friends alloc] init];
    if([f respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:f];  // 若可以响应,则将消息转发给其他对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
    }
}

既然 -forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

以上就是 Runtime 消息转发的整个流程。

消息发送以及转发机制总结

调用 [receiver selector]; 后,进行的流程:

  1. 编译阶段:

    [receiver selector]; 方法被编译器转换为:

    1. objc_msgSend(receiver,selector) (不带参数)
    2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)
  2. 运行时阶段:消息接受者

    recevier寻找对应的 selector

    1. 通过 recevierisa 指针 找到 recevierclass(类)
    2. Class(类)cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现)
    3. 如果在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类)method list(方法列表) 中找对应的 selector,如果找到,填充到 cache(方法缓存) 中,并返回 selector
    4. 如果在 class(类) 中没有找到这个 selector,就继续在它的 superclass(父类)中寻找;
    5. 一旦找到对应的 selector,直接执行 recevier 对应 selector 方法实现的 IMP(方法实现)
    6. 若找不到对应的 selector,Runtime 系统进入消息转发机制。
  3. 运行时消息转发阶段:

    1. 动态解析:通过重写 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其他函数实现;
    2. 消息接受者重定向:如果上一步添加其他函数实现,可在当前对象中利用 forwardingTargetForSelector: 方法将消息的接受者转发给其他对象;
    3. 消息重定向:如果上一步没有返回值为 nil,则利用 methodSignatureForSelector:方法获取函数的参数和返回值类型。
      1. 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
      2. 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容