消息传递与转发
- 一、消息传递(方法调用)
- 二、消息转发
- 1.动态方法解析
- 举例测试:
- 2.备援接收者
- 举例测试:
- 3.完整消息转发
- 举例测试:
- 三、相关源码解析
- 1.消息发送的快速查找`imp`过程(汇编环节)
- 1.1 当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行`objc_msgSend`。
- 1.2 检测指针如果为空,就立马返回。`结论:给nil发送消息不会做处理`。
- 1.3 通过 类对象/元类 (`objc_class`) 通过内存平移得到`cache`,获取`buckets`,通过内存平移的方式获取对应的方法(对比`sel`)。
- 1.4 如果对比`sel`找到了`imp`,就会`return or call imp`,如果没有`sel`,则去调用`_lookUpImpOrForward`。
- 总结消息发送快速查找`imp`(汇编):
- 2.消息发送的慢速查找`imp`过程(`c/c++`环节)
- 2.1 检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。
- 2.2 在类和父类中查找`imp`。
- 总结消息发送慢速查找`imp`(`c/c++`):
- 3.动态方法解析流程分析
- 3.1 对象方法动态解析
- 3.2 类方法动态解析
- 3.3 特殊的NSObject对象方法动态解析
Objective-C 本质上是一种基于 C 语言的领域特定语言。C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间层实现的。
一、消息传递(方法调用)
在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式转化为一个消息函数的调用。
OC中的消息表达式如下(方法调用):
id returnValue = [someObject messageName:parameter];
这里,someObject
叫做接收者(receiver)
,messageName:
叫做选择子(selector)
,选择子和参数合起来称为“消息
”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend
,编译器看到上述这条消息会转换成一条标准的 C 语言函数调用:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend
函数,这个函数将消息接收者和方法名作为主要参数,其原型如下所示:
objc_msgSend(receiver, selector) // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...) // 带参数
objc_msgSend
通过以下几个步骤实现了动态绑定机制:
- 首先,获取
selector
指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据receiver
所属的类进行判断。 - 其次,传递
receiver
对象、方法指定的参数来调用方法实现。 - 最后,返回方法实现的返回值。
消息传递的关键在于上一篇博客记录过的objc_class
结构体,其有三个关键的字段:
-
isa
:指向类的指针。 -
superclass
:指向父类的指针。 -
methodLists
:类的方法分发表(dispatch table)。
当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa
指针也会被初始化,让对象可以访问类及类的继承链。
下图所示为消息传递过程的示意图:
- 当消息传递给一个对象时,首先从运行时系统缓存
objc_cache
中进行查找。如果找到,则执行。否则,继续执行下面步骤。 -
objc_msgSend
通过对象的isa
指针获取到类的结构体,然后在方法分发表methodLists
中查找方法的selector
。如果未找到,将沿着类的superclass
找到其父类,并在父类的分发表methodLists
中继续查找。 - 以此类推,一直沿着类的继承链追溯至
NSObject
类。一旦找到selector
,传入相应的参数来执行方法的具体实现,并将该方法加入缓存objc_cache
。如果最后仍然没有找到selector
,则会进入消息转发流程。
二、消息转发
当一个对象能接收一个消息时,会走正常的消息传递流程。当一个对象无法接收某一消息时,会发生什么呢?
- 默认情况下,如果以
[object message]
的形式调用方法,如果object
无法响应message
消息时,编译器会报错。 - 如果是以
performSeletor:
的形式调用方法,则需要等到运行时才能确定object
是否能接收message
消息。如果不能,则程序崩溃。
对于后者,当不确定一个对象是否能接收某个消息时,可以调用respondsToSelector:
来进行判断:
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)
”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。
消息转发机制大致可分为三个步骤:
- 动态方法解析
- 备援接收者
- 完整消息转发
下图为消息转发过程的示意图:
1.动态方法解析
Objective-C 运行时会调用+ (BOOL)resolveInstanceMethod:(SEL)sel
或者+ (BOOL)resolveClassMethod:(SEL)sel
,让你有机会提供一个函数实现。前者在对象方法未找到时调用,后者在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES
, 那运行时系统就会重新启动一次消息发送的过程。
主要用的的方法如下:
// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法
返回值@return
表示能否新增一个方法来处理,一般使用@dynamic
属性来实现,若方法返回YES
,则表示可以处理该消息,在这个过程中,可以动态的给消息增加方法。若方法返回NO
,则进行消息转发的第二步,查找是否有其他的接收者。
这里简单说一下class_addMethod
方法:
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char * _Nullable types);
/**
* class_addMethod 向具有给定名称和实现的类中添加新方法
* @param cls 被添加方法的类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types imp 指向函数的返回值与参数类型
* @return 如果添加方法成功返回 YES,否则返回 NO
*/
其中types
的编码类型为:
另外一定要记得每一个方法会默认隐藏两个参数,self
、_cmd
。
举例测试:
先新建一个类Person
:
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
id getterName(id self, SEL cmd);
//- (NSString *)getName; OC的方法实现get函数
void setterName(id self, SEL cmd, NSString *value);
+ (BOOL)resolveInstanceMethod:(SEL)sel; //动态方法解析
- (void)eat;
- (void)sleep;
@end
// Person.m
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
@dynamic name;
//动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(name)) {
//@@: 此为签名符号
class_addMethod(self, sel, (IMP)getterName, "@@:");
//获取OC的函数指针
//IMP getIMP = class_getMethodImplementation(self, @selector(getName));
//获取函数方法数据
//Method nameMethod = class_getInstanceMethod(self, @selector(getName));
//自动获取函数参数类型
//const char *nameType = method_getTypeEncoding(nameMethod);
//添加方法
//class_addMethod(self, sel, getIMP, nameType);
return YES;
}
if (sel == @selector(setName:)) {
class_addMethod(self, sel, (IMP)setterName, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
id getterName(id self, SEL cmd) {
NSLog(@"%@, %s", [self class], sel_getName(cmd));
return @"Getter called";
}
//OC的方法实现get
//- (NSString *)getName {
// NSLog(@"%@", self);
// return @"Getter called";
//}
void setterName(id self, SEL cmd, NSString *value) {
NSLog(@"%@, %s, %@", [self class], sel_getName(cmd), value);
NSLog(@"SetterName called");
}
@end
然后创建Person
类的对象,并调用其name
属性的set
、get
方法:
// main.m
Person *person = [[Person alloc] init];
person.name = @"Jake";
NSLog(@"%@", person.name);
输出的信息与我们刚才定义的函数相同,说明我们在动态方法解析过程中拯救成功了。
2.备援接收者
如果上一步中+ (BOOL)resolveClassMethod:(SEL)sel
或者+ (BOOL)resolveInstanceMethod:(SEL)sel
没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。
如果当前对象实现了- (id)forwardingTargetForSelector:(SEL)aSelector
方法,Runtime
就会调用这个方法,允许我们将消息的接受者转发给其他对象。
其调用的方法如下:
//传入参数aSelector同样为无法处理的方法
//返回值为当前找到的备援接受者,如果没有找到则返回nil,进入下一阶段
- (id)forwardingTargetForSelector:(SEL)aSelector;
很明显:forwardingTargetForSelector
不能返回self
,否则会陷入死循环,因为返回self
又回去当前实例对象身上走一遍消息查找流程,显然又会来到forwardingTargetForSelector
。
举例测试:
我们再创建一个毫不相关的Child
类,其中实现Person
的eat
函数:
// Child.h
#import <Foundation/Foundation.h>
@interface Child : NSObject
@end
// Child.m
#import "Child.h"
@implementation Child
- (void)eat {
NSLog(@"Child method eat called");
}
@end
然后在Person.m
中实现- (id)forwardingTargetForSelector:(SEL)aSelector
并使用Person
类对象调用eat
函数:
// Person.m
//备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selStr = NSStringFromSelector(aSelector);
if ([selStr isEqualTo:@"eat"]) {
//这里创建一个child的类对象,所以就算你没有在.h中实现该方法,也是会调用的,它就代表着用这个child对象来调用其eat私有方法一样
return [[Child alloc] init];
//通过返回一个对象,让这个对象调用其相关的函数,来实现响应
}
return [super forwardingTargetForSelector:aSelector];
}
// main.m
Person *person = [[Person alloc] init];
[person eat];
输出的信息与我们刚才在Child
类中定义的函数相同,说明我们在备援接收者过程中拯救成功了。
这样的方式被叫做伪多继承。消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。
3.完整消息转发
如果上述的步骤返回的不是一个对象,而是nil
或者self
,系统将会执行消息转发的最后一步:完整消息转发。
首先会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法创建NSInvocation
对象,把尚未处理的那条消息的全部信息细节装在里边,在触发NSInvocation
对象时,信息派发系统(message-dispatch system)将会把消息指派给目标对象。这时会调用该方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
举例测试:
在Child
类中实现Person
类的sleep
函数:
// Child.m
- (void)sleep {
NSLog(@"Child method sleep called");
}
然后在Person类中实现- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
和- (void)forwardInvocation:(NSInvocation *)anInvocation
函数,并使用Person
对象调用sleep
函数:
// Person.m
//生成方法签名
//我们必须重写该方法 消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因为我们要给selector提供一个合适的方法签名,所以我们必须重写这个方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *sel = NSStringFromSelector(aSelector);
// 判断要转发的SEL(方法)
if ([sel isEqualToString:@"sleep"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
//"v@:"解释一下:每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。
}
return [super methodSignatureForSelector:aSelector];
}
//NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//拿到消息(methodSignatureForSelector中生成的方法签名)
SEL selector = [anInvocation selector];
//新建需要转发消息的对象,即新建一个可以实现该消息的对象
Child *child = [[Child alloc] init];
if ([child respondsToSelector:selector]) {
//转发 唤醒这个方法,交给这个可以实现该消息的对象来处理该消息
[anInvocation invokeWithTarget:child];
//这里将该方法给一个child的类对象来调用,就算你没有在.h中实现该方法,也是会调用的,它就意味着用这个child对象来调用其sleep私有方法一样
} else {
[super forwardInvocation:anInvocation];
}
}
//从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。
// main.m
Person *person = [[Person alloc] init];
[person sleep];
这样就实现了消息的第三次拯救。
在消息转发机制中,虽有三层顺序关系(先执行第一种方法,前一套方案实现后一套就不会执行),但是在消息转发的过程中这三种办法都可以用来解决接受消息找不到对应的方法的情况。如果这三套方案都没有得以处理问题,那么程序就会crash
。
三、相关源码解析
- 快速 -> 缓存里找汇编
cache_t
方法实现imp
哈希表 - 慢速 ->
c/c++
找缓存
1.消息发送的快速查找imp
过程(汇编环节)
先进入objc_msgSend
源码的入口处:
//进入objc_msgSend流程
ENTRY _objc_msgSend
//流程开始,无需frame
UNWIND _objc_msgSend, NoFrame
//判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
//b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//等于,如果不支持小对象,就跳转至LReturnZero退出
b.eq LReturnZero
#endif
//通过p13取isa
ldr p13, [x0] // p13 = isa
//通过isa取class并保存到p16寄存器中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
1.1 当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend
。
再到LNilOrTagged
都做了些什么:
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//nil check判空处理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
我们先看LReturnZero
:
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
等于说是结束了消息转发_objc_msgSend
。
1.2 检测指针如果为空,就立马返回。结论:给nil发送消息不会做处理
。
接着回来看看LGetIsaDone
做了什么(LGetIsaDone
入口在获取isa
和class
的后面):
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
进入CacheLookup
,是一个宏定义,在cache
中查找imp
:
//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function label we may have
// loaded an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd\Function,
// then our PC will be reset to LLookupRecover\Function which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
//从x16中取出class移到x15中
mov x15, x16 // stash the original isa
//开始查找
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//ldr表示将一个值存入到p10寄存器中
//x16表示p16寄存器存储的值,当前是Class
//#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
//#define CACHE (2 * __SIZEOF_POINTER__)
//经计算,p10就是cache
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//and表示与运算,将与上mask后的buckets值保存到p10寄存器
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
#endif
//按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//LSR表示逻辑向右偏移
//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
//这个是哈希算法,p12存储的就是搜索下标(哈希地址)
//整句表示_cmd & mask并保存到p12
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//去除掩码后bucket的内存平移
//PTRSHIFT经全局搜索发现是3
//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
cmp p9, p1 // if (sel != _cmd) {
//b.ne表示如果不相同则跳转到3f
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//通过p13和p10来判断是否是第一个bucket
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
// keep the remaining 38 bits for the IMP offset, which may need to reach
// across the shared cache. This offset needs to be shifted << 2. We did this
// to give it even more reach, given the alignment of source (the class data)
// and destination (the IMP)
ldr x17, [x10, x9, LSL #3] // x17 == (sel_offs << 38) | imp_offs
cmp x12, x17, LSR #38
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x0, x16, x17 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x17, x16, x17 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
1.3 通过 类对象/元类 (objc_class
) 通过内存平移得到cache
,获取buckets
,通过内存平移的方式获取对应的方法(对比sel
)。
如果找到sel
就会进入CacheHit
,去return or call imp
:
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
//编码查找imp,并且返回x17,也就是imp
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
如果没有找到sel
就会进入__objc_msgSend_uncached
,下面是上述判断跳转代码:
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
__objc_msgSend_uncached
源码汇编:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
其中调用了MethodTableLookup
宏:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
从MethodTableLookup
又跳转到了_lookUpImpOrForward
处。
1.4 如果对比sel
找到了imp
,就会return or call imp
,如果没有sel
,则去调用_lookUpImpOrForward
。
接下来_lookUpImpOrForward
在汇编代码里就找不到了。
至此快速查找imp
汇编部分就结束了,接下来到了漫长查找过程:c/c++
环节。
总结消息发送快速查找imp
(汇编):
objc_msgSend(receiver, sel, ...)
:
- 1.检查消息接收者
receiver
是否存在,为nil
则不做任何处理 - 2.如果不为
nil
,通过receiver
的isa
指针找到对应的class类对象 - 3.找到
class
类对象进行内存平移,找到cache
- 4.从
cache
中获取buckets
- 5.从
buckets
中对比参数sel
,看在缓存里有没有同名方法 - 6.如果
buckets
中有对应的sel
-->cacheHit
--> 调用imp
- 7.如果
buckets
中没有对应的sel
-->_objc_msgSend_uncached
->_lookUpImpOrForward
(c/c++
慢速查找)
2.消息发送的慢速查找imp
过程(c/c++
环节)
来看看lookUpImpOrForward
函数的实现:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
// 检查当前类是个已知类
checkIsKnownClass(cls);
// 确定当前类的继承关系
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// 如果是常量优化缓存
// 再一次从cache查找imp
// 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
imp = cache_getImp(curClass, sel); //cache中找IMP
if (imp) goto done_unlock; //找到就直接返回了
curClass = curClass->cache.preoptFallbackClass();
#endif
} else { //如果不是常量优化缓存
// 当前类的方法列表。
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
// 每次判断都会把curClass的父类赋值给curClass
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// 如果超类链中存在循环,则停止。
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// 在超类中找到方法。在这个类中缓存它。
goto done;
}
}
// 没有实现,尝试一次方法解析器。
// 这里就是消息转发机制第一层的入口
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
下面来慢慢讲解:
2.1 检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// 如果是常量优化缓存
// 再一次从cache查找imp
// 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass方法列表。
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
// 每次判断都会把curClass的父类赋值给curClass
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// 没有找到实现,方法解析器没有帮助。
// 使用转发。
imp = forward_imp;
break;
}
}
// 如果超类链中存在循环,则停止。
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 超类缓存。
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// 在超类中找到forward::条目。
// 停止搜索,但不要缓存;调用方法
// 首先为这个类解析器。
break;
}
if (fastpath(imp)) {
// 在超类中找到方法。在这个类中缓存它。
goto done;
}
}
进入了一个循环逻辑:
- a.从本类的
method list
查找imp
(查找的方式是getMethodNoSuper_nolock
,一会分析); - b.从本类的父类的
cache
查找imp
(cache_getImp
汇编写的) - c.从本类的父类的
method list
查找imp
…继承链遍历…(父类->…->根父类) - d.若上面环节有任何一个环节查找到了
imp
,跳出循环,缓存方法到本类的cache
(log_and_fill_cache
); - e.直到查找到
nil
,指定imp
为消息转发,跳出循环。
跳出循环后的逻辑:
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
如果找到了imp
,就会把imp
缓存到本类cache
里(log_and_fill_cache
):(注意这里不管是本类还是本类的父类找到了imp
,都会缓存到本类中去)
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver); // 插入缓存
}
2.2 在类和父类中查找imp
。
看看在类和父类继承链中查找imp
是怎样查找的(getMethodNoSuper_nolock
):
/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
// 找到方法列表
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
在search_method_list_inline
里找到了method_t
就会返回出去了(search_method_list_inline
):
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->isExpectedSize();
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name() == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
这里就是使用findMethodInSortedMethodList
和findMethodInUnsortedMethodList
通过sel
找到method_t
的。这两个函数的区别就是:
findMethodInSortedMethodList
查找有序的方法列表,通过二分查找对比sel
取出method_t
:findMethodInUnsortedMethodList
查找无序的方法列表,通过for
循环遍历一个个对比sel
从而取出method_t
:
总结消息发送慢速查找imp
(c/c++
):
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
:
- 1.从本类的
method list
(二分查找/遍历查找)查找imp
- 2.从本类的父类的
cache
查找imp
(汇编) - 3.从本类的父类的
method list
(二分查找/遍历查找)查找imp
…继承链遍历…(父类->…->根父类)里找cache
和method list
的imp
- 4.若上面环节有任何一个环节查找到了
imp
,跳出循环,缓存方法到本类的cache
,并返回imp
- 5.直到查找到
nil
,指定imp
为消息转发,跳出循环,执行动态方法解析resolveMethod_locked
3.动态方法解析流程分析
到此就说明之前的查找方法都没有找到sel
的imp
,所以我们在运行期再进行动态方法解析。
我们先来到_class_resolveMethod
方法,该方法就是两种动态方法解析(实例和类)的入口,该方法源码如下:
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
//判断进行解析的是不是元类
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
//不是元类,调用实例解析方法进行动态解析
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//是元类,调用类解析方法进行动态解析
_class_resolveClassMethod(cls, sel, inst);
//检查cls中sel的IMP是否存在
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
//没找到,则在进行一次实例方法解析
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
大概的流程如下:
- 判断进行解析的是否是元类
- 如果不是元类,则调用
_class_resolveInstanceMethod
进行对象方法动态解析 - 如果是元类,则调用
_class_resolveClassMethod
进行类方法动态解析 - 完成类方法动态解析后,再次查询
cls
中的imp
,如果没有找到,则进行一次对象方法动态解析
3.1 对象方法动态解析
我们先分析对象方法的动态解析,我们直接来到_class_resolveInstanceMethod
方法处:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
//在cls->ISA()中检查是否实现了SEL_resolveInstanceMethod方法
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
//没有实现直接返回
return;
}
//到这就说明实现了SEL_resolveInstanceMethod方法
//那就通过objc_msgSend手动调用该类方法发送消息,这两步操作过后,这个动态方法就应该已经被加到方法列表里了
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 缓存结果(好或坏),这样解析器下次就不会触发。
// +resolveInstanceMethod添加到self,也就是cls
//调用objc_msgSend完成后,再次查询cls中sel的IMP指针
//因为上一步的成功,这一步应该就能找到,找不到肯定是有问题了
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
//解析完成并且需要输出日志
if (resolved && PrintResolving) {
//如果IMP找到了
if (imp) {
//输出动态解析对象方法成功的日志
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else { //如果IMP没找到
// 方法解析器没有添加任何东西?
//输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel方法,并且返回了 YES,但并没有查找到IMP指针的日志
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
大致的流程如下:
- 检查是否实现了
+(BOOL)resolveInstanceMethod:(SEL)sel
类方法,如果没有实现则直接返回(通过cls->ISA()
是拿到元类,因为类方法是存储在元类上的对象方法) - 如果当前实现了
+(BOOL)resolveInstanceMethod:(SEL)sel
类方法,则通过objc_msgSend
手动调用该类方法 - 完成调用后,再次查询
cls
中的imp
,并存到方法列表 - 如果
imp
找到了,则输出动态解析对象方法成功的日志 - 如果
imp
没有找到,则输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel
,并且返回了YES
,但并没有查找到imp
的日志
3.2 类方法动态解析
接着我们分析类方法动态解析,我们直接来到_class_resolveClassMethod
方法处:
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
//断言是不是元类,不是元类就直接退出
assert(cls->isMetaClass());
//能到这里就表示该类就是元类
//检查cls中是否实现了SEL_resolveClassMethod方法
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
//没有实现,就直接返回
return;
}
//到这里还没有返回,就说明实现了SEL_resolveClassMethod方法
//那就通过objc_msgSend手动调用该类方法发送消息,这两步操作过后,这个动态方法就应该已经被加到方法列表里了
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//这里需要通过元类和对象来找到类
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// 缓存结果(好或坏),这样解析器下次就不会触发。
// +resolveInstanceMethod添加到self,也就是cls
//调用完成之后,再次查找cls中的sel的IMP指针
//因为上一步的成功,这一步应该就能找到,找不到肯定是有问题了
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
//解析完成并且需要输出日志
if (resolved && PrintResolving) {
//如果找到了IMP指针
if (imp) {
//输出动态解析对象方法成功的日志
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else { //没有找到IMP指针
// 方法解析器没有添加任何东西?
//输出虽然实现了+(BOOL)resolveClassMethod:(SEL)sel,并且返回了YES,但并没有查找到imp指针的日志
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
大致的流程如下:
- 断言是否是元类,如果不是,直接退出,因为我们是要找类方法
- 检查是否实现了
+ (BOOL)resolveClassMethod:(SEL)sel
类方法,如果没有实现则直接返回(通过cls
是因为当前cls
就是元类,因为类方法是存储在元类上的对象方法) - 如果当前实现了
+ (BOOL)resolveClassMethod:(SEL)sel
类方法,则通过objc_msgSend
手动调用该类方法,注意这里和动态解析对象方法不同,这里需要通过元类和对象来找到类,也就是_class_getNonMetaClass
- 完成调用后,再次查询
cls
中的imp
,并存到方法列表 - 如果
imp
找到了,则输出动态解析对象方法成功的日志 - 如果
imp
没有找到,则输出虽然实现了+(BOOL)resolveClassMethod:(SEL)sel
,并且返回了YES
,但并没有查找到imp
的日志
3.3 特殊的NSObject对象方法动态解析
我们再聚焦到_class_resolveMethod
方法上,如果cls
是元类,也就是说进行的是类方法动态解析的话,有以下源码:
_class_resolveClassMethod(cls, sel, inst); // 已经处理
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 对象方法 决议
_class_resolveInstanceMethod(cls, sel, inst);
}
对于_class_resolveClassMethod
的执行,肯定是没有问题的,只是为什么在判断如果动态解析失败之后,还要再进行一次对象方法解析呢,这个时候就需要上一张经典的isa
走位图了:
由这个流程图我们可以知道,元类最终继承于根元类,而根元类又继承于NSObject
,那么也就是说在根元类中存储的类方法等价于在NSObject
中存储的对象方法。而系统在执行lookUpImpOrNil
时,会递归查找元类的父类的方法列表。但是由于元类和根元类都是系统自动生成的,我们是无法直接编写它们,而对于NSObject
,我们可以借助分类(Category)来实现统一的类方法动态解析,不过前提是类本身是没有实现resolveClassMethod
方法。
这也就解释了为什么_class_resolveClassMethod
为什么会多一步对象方法解析的流程了。