iOS底层原理 - 探寻Runtime本质 之 消息机制

面试题引发的思考:

Q: 简述消息转发机制?

  • 消息发送阶段:负责从 类及父类缓存列表及方法列表 查找方法;
  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责 动态的添加 方法实现;
  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息 转发 给可以处理消息的 接收者 来处理;
  • 报错:如果也没有实现消息转发方法,会报错unrecognzied selector sent to instance

1. 方法调用的本质

前两章分别对isa结构的本质、Class结构的本质做了探究,下面探究方法调用的本质。

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}

转成C++代码:

C++代码

由以上代码可知:
OC的方法调用都是转化为objc_msgSend()函数,即消息机制,通过Runtime给方法调用者发送消息。
objc_msgSend(person, @selector(test));的作用是给消息接收者person发送test消息。
那么接下来我们探究objc_msgSend()函数的调用过程:

objc_msgSend()的执行流程可以分为三个阶段:

  • 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法;
  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接收者来处理;
  • 报错:如果也没有实现消息转发方法,会报错unrecognzied selector sent to instance

接下来通过源码探寻以上三个阶段的实现。


(1) 消息发送阶段

OC源码中搜索_objc_msgSend,在objc-msg-arm64.s汇编文件找到_objc_msgSend函数的实现:

_objc_msgSend函数

判断消息接收者是否为nil
如果为nil,直接退出程序;
否则继续执行代码至CacheLookup NORMAL,去缓存查找:

CacheLookup宏

判断缓存中找到对应的方法:
如果可以找到,执行CacheHit $0,进而调用方法;
否则执行CheckMiss $0

CheckMiss宏

如果缓存中没有找到对应方法,执行__objc_msgSend_uncached函数:

__objc_msgSend_uncached函数

执行MethodTableLookup即方法列表查找:

MethodTableLookup宏

内部核心代码为__class_lookupMethodAndLoadCache3函数:
汇编的函数比C++的多一个下划线,所以查找_class_lookupMethodAndLoadCache3即可:

_class_lookupMethodAndLoadCache3函数

以上步骤进流程图如下:

objc_msgSend()执行流程

下面对lookUpImpOrForward函数进行分析:

lookUpImpOrForward函数

通过getMethodNoSuper_nolock函数在类的方法列表查找方法:

getMethodNoSuper_nolock函数

通过getMethodNoSuper_nolock函数遍历得到类对象的方法列表,然后通过search_method_list函数查找方法:

search_method_list函数

如果方法列表是有序的,通过二分查找方法;
否则遍历列表查找方法。

二分查找实现原理如下:

findMethodInSortedMethodList函数

以上步骤进流程图如下:

方法查找执行流程

如果消息发送阶段没有找到方法,就会进入动态解析阶段。


(2) 动态解析阶段

lookUpImpOrForward函数

由以上代码可知:
如果消息发送阶段没有找到方法,就会进入动态解析阶段;
动态解析方法之后,triedResolver = YES;,然后goto retry,那么会重新查找一遍方法,并跳过动态解析阶段。

动态解析阶段主要函数为_class_resolveMethod

_class_resolveMethod函数

_class_resolveMethod函数会根据类对象或者元类对象,分别调用resolveInstanceMethod方法或resolveClassMethod方法。

动态解析进流程图如下:

动态解析执行流程
动态解析过程实例
a> 实例方法动态解析
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
@end

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}

以上代码运行报错:unrecognized selector sent to instance 0x1006088b0,因为在Person类中声明了test方法,却并没有实现。

接下来通过动态解析解决问题,相关API为:

class_addMethod函数
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 获取其他方法
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        // 动态添加方法
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        // 返回YES表示有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
};
- (void)other {
    NSLog(@"%s", __func__);
}
@end

// 打印结果
Demo[1234:567890] -[Person other]

由打印结果可知:
person在调用test方法时经过动态解析成功调用了other方法。

b> 类方法动态解析
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
+ (void)classTest;
@end

@implementation Person
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classTest)) {
        // 获取其他方法
        Method otherMethod = class_getClassMethod(self, @selector(classOther));
        // 动态添加方法
        class_addMethod(object_getClass(self), sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        // 返回YES表示有动态添加方法
        return YES;
    }
    return [super resolveClassMethod:sel];
}
+ (void)classOther {
    NSLog(@"%s", __func__);
}
@end

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person classTest];
    }
    return 0;
}

(3) 消息转发阶段

如果消息发送阶段没有找到方法,动态解析阶段没有对方法进行动态解析,接下来就会进入消息转发阶段。

lookUpImpOrForward函数

消息转发阶段,会调用_objc_msgForward_impcache函数:

_objc_msgForward_impcache函数

汇编中找到__objc_msgForward_impcache函数实现,该函数调用__objc_msgForward函数;
__objc_msgForward函数实现调用__objc_forward_handler函数;
无法找到__objc_forward_handler函数实现,查找资料了解到消息转发机制是不开源的

消息转发过程实例
a> 实例方法消息转发
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
@end

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}

由以上代码可知:
Person类中只声明test方法,不实现方法,此时调用test方法会崩溃。

Student类中实现一个test方法,在Person类中通过forwardingTargetForSelector:方法将消息转发对象设置为Student对象:

// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
/**
 消息转发
 @param aSelector 方法
 @return 返回能够处理消息的对象
 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [[Student alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

// TODO: ----------------- Student类 -----------------
@interface Student : NSObject
@end

@implementation Student
- (void)test {
    NSLog(@"%s", __func__);
}
@end

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}

// 打印结果
// Runtime[939:123812] -[Student test]

如果forwardingTargetForSelector函数返回为nil或者没有实现的话,就会调用methodSignatureForSelector函数,用来返回一个方法签名;
然后调用forwardInvocation函数,其内部参数为NSInvocation类型,NSInvocation类型封装一个方法的调用,包括方法调用者、方法、方法的参数;
然后在forwardInvocation函数内修改方法调用对象。

// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
/**
 消息转发
 @param aSelector 方法
 @return 返回能够处理消息的对象
 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // 返回nil则会调用methodSignatureForSelector方法
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}
/**
 为另一个类实现的消息创建一个有效的方法签名
 @param aSelector 方法
 @return 方法签名:返回值类型、参数类型
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [[[Student alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
/**
 将选择器转发给一个真正实现了该消息的对象
 @param anInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
 @param anInvocation.target 方法调用者
 @param anInvocation.selector 方法
 @param [anInvocation getArgument: NULL atIndex: 0]; 方法的参数
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 此时anInvocation.target 还是person对象,需要修改target为可以执行方法的方法调用者。
    [anInvocation invokeWithTarget:[[Student alloc] init]];
}
@end

// 打印结果
// Runtime[1016:190175] -[Student test]

消息转发流程图如下:

消息转发执行流程
b> 类方法消息转发
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
+ (void)test;
@end

@implementation Person
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [Student class];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

// TODO: ----------------- Student类 -----------------
@interface Student : NSObject
@end

@implementation Student
+ (void)test {
    NSLog(@"%s", __func__);
}
@end

// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person test];
    }
    return 0;
}

// 打印结果
// Runtime[1245:167577] +[Student test]

如果forwardingTargetForSelector函数返回为nil或者没有实现的话:

// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)test;
@end

@implementation Person
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [[Student class] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[Student class]];
}
@end

// 打印结果
// Runtime[1335:187977] +[Student test]

(4) 总结

  • OC的方法调用都是转化为objc_msgSend()函数,即消息机制,通过Runtime给方法调用者发送消息。
  • 方法调用分为三个阶段:
    消息发送、动态方法解析、消息转发。

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

推荐阅读更多精彩内容