Runtime 运行时之二:方法调用流程与消息转发

方法调用流程

在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示:


objc_msgSend(receiver, selector)

如果消息中还有其它参数,则该方法的形式如下所示:


objc_msgSend(receiver, selector, arg1, arg2, ...)

这个函数完成了动态绑定的所有事情:

  1. 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
  2. 它调用方法实现,并将接收者对象及方法的所有参数传给它。
  3. 最后,它将实现返回的值作为它自己的返回值。

消息的关键在于我们前面章节讨论过的结构体objc_class,这个结构体有两个字段是我们在分发消息的关注的:

  1. 指向父类的指针
  2. 一个类的方法分发表,即methodLists

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。

下图演示了这样一个消息的基本框架:

messaging1-1.gif

当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在后面讨论。

为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。这点我们在前面讨论过,不再重复。

获取方法地址

Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。

我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。

NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。

我们通过以下代码来看看methodForSelector:的使用:

@implementation ViewController
- (void)viewDidLoad{
  ///通过runtime获取IMP
   Method nameLogMethod = class_getInstanceMethod([self class], sel1);
    IMP nameLogImp = method_getImplementation(nameLogMethod);
    nameLogImp();
    
    ///通过NSObject获取IMP
    ///methodForSelector 如果接受者是一个类对象,则返回类对象的方法;如果接受者是一个实例对象,则返回实例对象的方法;
    IMP nameLogImp2 = [self methodForSelector:sel1];
    nameLogImp2();
    ///instanceMethodForSelector 是通过遍历自身中的函数列表换句话说,如果调用的一个元类,那么返回的是一个类实例的函数,如果调用的是一个类,那么返回的就是实例对象的函数。
    IMP nameLogImp3 = [ViewController instanceMethodForSelector:sel1];
    nameLogImp3();
   ///https://www.jianshu.com/p/2007e03b6296
}
-(void)nameLog{
    
    NSLog(@"我的名字3--");
   
}

这里需要注意的就是函数指针的前两个参数必须是idSEL

当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

消息转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以performSelector的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:


if ([self respondsToSelector:@selector(runtimeAction)]) {

    [self performSelector:@selector(runtimeAction)];

}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:


libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[runtimeViewController runtimeAction]: unrecognized selector sent to instance 0x7fd89460dc30'
terminating with uncaught exception of type NSException

这段异常信息实际上是由NSObject的”doesNotRecognizeSelector“方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者\替换消息接收者(快速转发)
  3. 完整转发
2145446-c73a54d9b48e0417.png

下面我们详细讨论一下这三个步骤。

第一步、动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法

///实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
///类方法
+ (BOOL)resolveClassMethod:(SEL)sel

在这方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

//第一步、动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
 //  SEL获取IMP
//    [self instanceMethodForSelector:@selector(runtimeFuncAction)]
//  SEL转字符串
//    NSStringFromSelector(sel)


   if (sel == @selector(runtimeAction)) {
        ///第一种方式
        ///class_addMethod(self, sel, [self instanceMethodForSelector:@selector(runtimeFuncAction)], "v@:");

        ///第二种方式
        ///v@:代表形参
        class_addMethod(self, sel, (IMP)runtimeFuncActionC, "v@:");

        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

///第一种方式
- (void)runtimeFuncAction {
    
    NSLog(@"runtimeActionq未实现-第一步、动态方法解析-runtimeFuncAction-执行");
    
}
///第二种方式
void runtimeFuncActionC(id self, SEL _cmd)

{
    
    NSLog(@"runtimeActionq未实现-第一步、动态方法解析-runtimeFuncActionC-执行-%@-%@",self,NSStringFromSelector(_cmd));
    
}

不过这种方案更多的是为了实现@dynamic属性,dynamic修饰的属性,无set,get方法。

扩展-@dynamic
@synthesize 和 @dynamic分别有什么作用

  • @property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;
  • @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法
  • @dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成(当然对于readonly的属性只需提供getter即可)
  • 假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = instance.var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定

第二步、备用接收者\替换消息接收者(快速转发)

如果在上一步无法处理消息,则Runtime会继续调以下方法:

///实例方法
- (id)forwardingTargetForSelector:(SEL)aSelector
///类方法
+ (id)forwardingTargetForSelector:(SEL)sel

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:

runtimeViewController.m文件
#import "runtimeViewController.h"
///运行时头文件
#import <objc/runtime.h>

#import "notificationViewController.h"
@interface runtimeViewController ()

@end


@implementation runtimeViewController

- (void)viewDidLoad {
 [self performSelector:@selector(runtimeAction)];
}

- (id)forwardingTargetForSelector:(SEL)sel{
    
    if(sel == @selector(runtimeAction)) {
///将消息转发给notificationViewController来处理
//        return [[notificationViewController alloc]init];
        return NSClassFromString(@"notificationViewController");
    }
    return [super forwardingTargetForSelector:sel];
    
}

notificationViewController.m文件
- (void)runtimeAction {
    
    NSLog(@"runtimeActionq未实现-第二步、备用接收者或第三步、完整消息转发-(对象)runtimeAction-执行");
    
}

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:


- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

完整的示例如下所示:

runtimeViewController.m文件
#import "runtimeViewController.h"
///运行时头文件
#import <objc/runtime.h>

#import "notificationViewController.h"
@interface runtimeViewController ()

@end


@implementation runtimeViewController

- (void)viewDidLoad {
 [self performSelector:@selector(runtimeAction)];
}
///第三步、完整消息转发
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(runtimeAction))
    {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation{

    SEL aSelector = [invocation selector];
    ///判断notificationViewController是否能响应aSelector方法
    if ([notificationViewController respondsToSelector:aSelector]){
        ///将 invocation 消息转发给其它对象 notificationViewController 执行
        [invocation invokeWithTarget:[[notificationViewController alloc]init]];
    }else{
        [super forwardInvocation:invocation];
    }

}
notificationViewController.m文件
- (void)runtimeAction {
    
    NSLog(@"runtimeActionq未实现-第二步、备用接收者或第三步、完整消息转发-(对象)runtimeAction-执行");
    
}

NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

消息转发与多重继承

回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:


- (BOOL)respondsToSelector:(SEL)aSelector

{

    if ( [super respondsToSelector:aSelector])

        return YES;

    else {

        /* Here, test whether the aSelector message can     *

         * be forwarded to another object and whether that  *

         * object can respond to it. Return YES if it can.  */

    }

    return NO;  

}

小结

在此,我们已经了解了Runtime中消息发送和转发的基本机制。这也是Runtime的强大之处,通过它,我们可以为程序增加很多动态的行为,虽然我们在实际开发中很少直接使用这些机制(如直接调用objc_msgSend),但了解它们有助于我们更多地去了解底层的实现。其实在实际的编码过程中,我们也可以灵活地使用这些机制,去实现一些特殊的功能,如hook操作等。

为什么Objective-C的消息转发要设计三个阶段?

第一阶段意义在于动态添加方法实现,第二阶段直接把消息转发给其他对象,第三阶段是对第二阶段的扩充,可以实现多次转发,转发给多个对象等。这也许就是设计这三个阶段的意义。

补充:下面这张图(来自:一缕殇流化隐半边冰霜(侵删))我觉得更符合消息转发的流程,更容易理解。


20200407161830450.png

参考:
https://www.jianshu.com/p/8d4f2f1d8482
https://www.jb51.net/article/157079.htm
https://blog.csdn.net/fishmai/article/details/73468952
https://www.jianshu.com/p/19c5736c5d9a
http://southpeak.github.io/categories/objectivec/

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

推荐阅读更多精彩内容