iOS-Runtime3-objc_msgSend底层调用流程

经过上面的学习我们知道,当调用方法[person test],就是给person对象发送test消息,然后通过isa->superclass-> superclass......寻找类对象,找到类对象之后,还会先通过@selector(test)&_mask生成索引,通过索引直接在cache里的散列表里面取方法,如果cache里面没有方法,再遍历methods数组......

OC的方法调用:消息机制,就是给方法调用者发送消息,其实都是转换为objc_msgSend函数的调用。

比如:

[person personTest];

转成C++代码就是:

objc_msgSend(person, sel_registerName("personTest"));

以前我们讲过sel_registerName("personTest")和@selector(personTest)返回的都是SEL,而且打印发现他们的地址的确也相同,所以上面的代码也可以写成:

objc_msgSend(person, @selector(personTest));

消息接收者:person
消息名称:personTest
意思就是给person对象发送personTest消息。

同理,类方法调用:

[MJPerson initialize];

转成C++代码:

(objc_getClass("MJPerson"), sel_registerName("initialize"));

也可以写成:

objc_msgSend([MJPerson class], @selector(initialize));

意思就是给MJPerson类对象发送initialize消息。

一. objc_msgSend的执行流程

objc_msgSend的执行流程可以分为3大阶段:

  1. 消息发送:就是根据isa、superclass寻找方法
  2. 动态方法解析:允许开发者动态创建新的方法
  3. 消息转发:转发给另外一个对象调用这个方法

objc_msgSend内部的这三个阶段经历完还找不到方法就报错:unrecognized selector sent to instance/class。

由于源码解析比较麻烦,我放到objc_msgSend源码解析这篇文章里面了,强烈建议先看一下,这样就很容易理解下面的流程,下面直接说结论。

一. 消息发送

消息发送.png

二. 动态方法解析

动态方法解析.png

下面通过动态添加方法来验证阶段二,动态方法解析。

1. 实例对象的动态方法解析

先创建MJPerson对象,只声明方法,不实现方法:
调用代码:

MJPerson *person = [[MJPerson alloc] init];
[person test];
[MJPerson test];

发现会报错:

unrecognized selector sent to instance 0x10074a610
unrecognized selector sent to class 0x100001118

下面动态添加实例方法,动态添加实例方法需要我们实现resolveInstanceMethod方法。
在MJPerson.m添加如下代码:

- (void)other
{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他方法
        Method method = class_getInstanceMethod(self, @selector(other));

        // 动态添加test方法的实现
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));

        // 返回YES代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

打印:

-[MJPerson other]

可以发现方法添加成功。调用test,但是test没找到,会进入resolveInstanceMethod里面动态添加方法,方法添加完成会重新走消息发送流程,然后就找到了other,调用other,然后打印-[MJPerson other]。

补充:Method就是method_t

可能你不知道Method是什么,其实Method就是在iOS-底层-Runtime2里面讲过的method_t,method_t的结构体是这样的:

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

所以,上面的代码就能修改为:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他方法
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));

        //Method几乎等价于以前讲的method_t,可打印验证
        NSLog(@"%s, %s, %p",method->sel,method->types,method->imp);

        // 动态添加test方法的实现
        class_addMethod(self, sel, method->imp, method->types);

        // 返回YES代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

执行代码,打印结果如下:

other, v16@0:8, 0x100000dd0
-[MJPerson other]

可以发现,函数名、函数编码(参数、返回值)、函数地址都打印出来了,other方法也调用了,说明动态方法添加成功,Method和method_t没啥区别。

2. 类对象的动态方法解析

那么如果调用的是类方法呢?
需要在resolveClassMethod方法里面,动态添加类方法。

MJPerson *person = [[MJPerson alloc] init];
//[person test];
[MJPerson test];

动态添加类方法:

//动态添加c语言函数的实现
void c_other(id self, SEL _cmd)//这也验证了OC的方法都有两个隐式参数(id self, SEL _cmd)
{
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}

//动态添加类方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self)
        //c语言的函数地址就是函数名
        //object_getClass(self)获取元类对象
        class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

注意:动态添加实例方法传入到class_addMethod函数中的是当前类对象self,动态添加类方法传入到class_addMethod函数中的是元类对象object_getClass(self)。

上面代码运行后,打印:

c_other - MJPerson - test

可以发现添加成功,上面不但验证了类方法可以添加成功,而且验证了,还可以使用c语言函数作为他们的实现。

上面代码Demo地址:动态方法解析

三. 消息转发

消息转发.png

关于消息转发的逻辑如上图,下面进行验证。

1. 实例对象的消息转发

① forwardingTargetForSelector返回对象

在MJPerson.m里面实现如下代码:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        // objc_msgSend([[MJCat alloc] init], aSelector)
        return [[MJCat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

执行:

MJPerson *person = [[MJPerson alloc] init];
[person test];

打印:

-[MJCat test]

可以发现,person对象没实现test方法,会调用MJCat的test方法。

那如果forwardingTargetForSelector返回值为空呢?

② methodSignatureForSelector返回方法签名

//方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

 /*
  NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
    anInvocation.target 方法调用者
    anInvocation.selector 方法名
    [anInvocation getArgument:NULL atIndex:0] 获取参数
  */
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    
//    anInvocation.target = [[MJCat alloc] init];
//    [anInvocation invoke];
    
     //上面下面都可以了
    [anInvocation invokeWithTarget:[[MJCat alloc] init]];
}

上面的代码,我们在返回方法签名之后,把anInvocation.target给修改为MJCat,所以最后还是会调用MJCat的test方法。

上面的代码,如果仅仅是想要调用MJCat的test方法,在forwardingTargetForSelector里面修改其实更简单。

在forwardInvocation做更多操作:

[person test]执行的代码其实就是forwardInvocation方法里面执行的代码,所以我们可以在forwardInvocation方法里面做任何操作,比如只打印,获取参数,获取返回值。

首先,MJPerson只声明不实现test方法,MJCat实现test方法,如下

@implementation MJCat
- (int)test:(int)age
{
    return age * 2;
}
@end

在MJPerson.m里面实现如下代码:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test:)) {
        //直接返回方法签名
        //return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
        //方法签名省略数字
        return [NSMethodSignature signatureWithObjCTypes:"i@:I"];
        //拿到MJCat的test方法签名来用
        //return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //参数顺序:receiver(也就是self)、selector、other arguments
    int age;
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age + 10);
    //打印:15 + 10 = 25;
    
    /*
     anInvocation.target 是 [[MJCat alloc] init]
     anInvocation.selector 是 test:
     anInvocation的参数:15
     */
    [anInvocation invokeWithTarget:[[MJCat alloc] init]];
    int ret;
    [anInvocation getReturnValue:&ret];
    NSLog(@"%d", ret);
    //打印:15 * 2 = 30;
}

关于返回方法签名、获取参数、获取返回值可看注释。

2. 类对象的消息转发

刚才讲的是实例对象的消息转发,如果是类对象需要实现+号开头的方法,如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [MJCat class];
    //+[MJCat test]
    return [super forwardingTargetForSelector:aSelector];
}

返回MJCat类对象,当调用[MJPerson test],会打印:+[MJCat test],调用了MJCat的test方法。

上面的代码如果返回的是实例对象呢?如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    // objc_msgSend([[MJCat alloc] init], @selector(test))
    // [[[MJCat alloc] init] test]
    if (aSelector == @selector(test)) return [[MJCat alloc] init];
    //-[MJCat test]

    return [super forwardingTargetForSelector:aSelector];
}

可以发现,返回MJCat实例对象,当调用[MJPerson test],会打印:-[MJCat test],调用了MJCat实例对象的对象方法,为什么会这样呢?

通过源码分析可知,forwardingTargetForSelector方法内部就是给返回的对象发送test消息,所以返回的是实例对象就是给实例对象发送消息,自然就是如下:

objc_msgSend([[MJCat alloc] init], @selector(test))

同样,如果上面方法返回为空,也会走以下代码:

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];

    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"1123");
}

打印:

1123

补充:@synthesize、@dynamic

首先在MJPerson.h写如下代码:

@property (assign, nonatomic) int age;

我们都知道,编译器会自动生成_age成员变量、setter和getter的声明、setter和getter的实现。

但是在很久以前,Xcode还没这么智能的时候,如果只写上面那句还不行,因为@property只负责生成settetr和getter方法的声明。还要在.m文件中使用@synthesize。

@synthesize age = _age, height = _height;

这时候编译器才会生成_age成员变量、setter和getter的实现。

如果使用@dynamic就是提醒编译器不要自动生成成员变量,不要自动生成setter和getter的实现,等到运行时再添加方法实现。

MJPerson.m代码如下:

@dynamic age;

void setAge(id self, SEL _cmd, int age)
{
    NSLog(@"age is %d", age);
}

int age(id self, SEL _cmd)
{
    return 120;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(setAge:)) {
        class_addMethod(self, sel, (IMP)setAge, "v@:i");
        return YES;
    } else if (sel == @selector(age)) {
        class_addMethod(self, sel, (IMP)age, "i@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

执行代码:

MJPerson *person = [[MJPerson alloc] init];
person.age = 20;
NSLog(@"%d", person.age);

打印:

age is 20
120

可以发现,使用@dynamic就没有生成_age成员变量、setter和getter的实现,这时候我们自己通过Runtime动态添加了setter和getter的实现才实现了如上打印。

总结:@synthesize自动生成_age成员变量、setter和getter的实现,@dynamic不自动生成_age成员变量、setter和getter的实现,正好是反过来的

Demo地址:消息转发

面试题:

  1. 讲一下OC的消息机制

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。
objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发。

  1. 说一下消息转发流程

当消息发送和动态方法解析都没找到方法就会进入消息转发阶段
① 首先会调用+或-开头的forwardingTargetForSelector方法,如果这个方法返回值不为空,就给返回值发送SEL消息:objc_msgSend(返回值, SEL)。
② 如果这个方法的返回值为空,就会调用+或-开头的methodSignatureForSelector方法,如果这个方法返回值不为空,就会再调用+或-开头的forwardInvocation方法,我们可以在forwardInvocation里面方法做任何我们想做的事。
③ 如果这个方法的返回值为空,就会调用doesNotRecognizeSelector,报错unrecognized selector sent to instance/class。

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