上一篇cache_t详解中,我们了解了调用方法时会往cache中插入方法缓存,下次再调用同样的方法时,就会读取缓存以便能更快地调用方法。那么在读取缓存时,苹果又做了哪些事情呢。
方法调用的本质
首先在objc_cache.m文件中的开头注释中,我们发现了如下的注释内容。
Cache readers (PC-checked by collecting_in_critical())
objc_msgSend*
cache_getImp
苹果在注释中已经告诉我们,读取方法缓存与objc_msgSend和cache_getImp有关。OC是一门动态的语言,在编译阶段并不知道具体的数据类型,也不知道真正调用的函数是哪个,只有在运行的时候才会去检查变量的数据类型和找到实际调用的函数,而runtime也提供了API来让我们动态地对程序进行修改。
runtime的核心一个是对类的各方面的动态配置,也就是在运行的过程中动态地修改类或者对象的信息,如添加类的方法或者属性、修改成员变量的值等,另一个就是消息传递,其中又包括了消息的发送和消息的转发。消息的发送就是runtime通过sel来找到imp的这么一个过程,而这个过程,就是在编译时通过msg_Send函数来实现的。
我们先定义一个类,在类中定义几个方法,然后调用它。
@interface MyObject : NSObject
- (void)actionOne;
- (void)actionTwo:(NSString *)two;
+ (void)actionThree;
@end
@implementation MyObject
- (void)actionOne {
NSLog(@"%s",__func__);
}
- (void)actionTwo:(NSString *)two {
NSLog(@"%s",__func__);
}
+ (void)actionThree {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject *mo = [MyObject alloc];
[mo actionOne];
[mo actionTwo:@"two"];
}
return NSApplicationMain(argc, argv);
}
使用clang -rewrite-objc main.m命令将main.m变成main.cpp,查看main.cpp。在main.cpp文件中找到main函数,发现它已经被转成如下的代码。
MyObject *mo = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)mo, sel_registerName("actionOne"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)mo, sel_registerName("actionTwo:"), (NSString *)&__NSConstantStringImpl__var_folders_th_1c31k73s1_qc60jtg3sx21740000gn_T_main_65f9ce_mi_3);
我们发现,所有的方法调用都被转换成了objc_msgSend函数,并且还有两个默认的参数,一个是消息的接收者,如(id)objc_getClass("MyObject")和(id)mo,另一个就是方法名,如sel_registerName("alloc")等。而actionTwo还有一个我们定义的参数作为objc_msgSend的第三个参数。objc_msgSend就可以通过消息的接收者和消息方法名来找到方法对应的实现。
自己调用objc_msgSend
既然方法的调用最终都被转换成了objc_msgSend,那么就可以直接调用objc_msgSend函数来实现方法的调用。
首先先导入头文件#import <objc/message.h>,然后调用((void (*)(id, SEL))(void *)objc_msgSend)((id)mo, sel_registerName("actionOne"));,前面((void (*)(id, SEL))就表示了这个方法返回值是void,参数类型是id和SEL。
通过打印我们确认了,直接调用objc_msgSend同样打印了方法名,说明直接调用objc_msgSend也是可以的。
在Xcode12前,我们还可以通过修改Build Setting中的Enable Strict Checking Of objc_msgSend Calls的值为NO,来简化调用objc_msgSend函数objc_msgSend((id)mo, sel_registerName("actionOne"));。
objc_msgSendSuper
探索
继续查看main.cpp文件,发现其中有objc_msgSend函数的踪迹。
__OBJC_RW_DLLIMPORT void objc_msgSend(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);
除了objc_msgSend,还有objc_msgSendSuper、objc_msgSend_stret、objc_msgSendSuper_stret等内容,接下来我们就先了解一下objc_msgSendSuper。
首先先定义一个类MySuperObject和它的子类MySubObject,并在MySubObject的init方法中调用[self class];和[super class];,并且调用init方法。
@interface MySuperObject : NSObject
-(void)study;
@end
@implementation MySuperObject
-(void)study {
NSLog(@"%s",__func__);
}
@end
@interface MySubObject : MySuperObject
@end
@implementation MySubObject
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
- (void)study {
[super study];
}
@end
// 在程序中调用init
MySubObject *sub = [[MySubObject alloc] init];
[sub study];
run起程序后查看打印结果。
结果令人诧异,不论是[self class];还是[super class];,最终的打印都是MySubObject,这又是为什么呢?
先重复刚才的流程,将MySubObject.m转换成MySubObject.cpp,然后查找init方法来查看一下。
static instancetype _I_MySubObject_init(MySubObject * self, SEL _cmd) {
if (self = ((MySubObject *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MySubObject"))}, sel_registerName("init"))) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_th_1c31k73s1_qc60jtg3sx21740000gn_T_MySubObject_566b8a_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_th_1c31k73s1_qc60jtg3sx21740000gn_T_MySubObject_566b8a_mi_1,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MySubObject"))}, sel_registerName("class")));
}
return self;
}
我们发现在调用[super class];的时候,转换成的函数是objc_msgSendSuper。在Xcode自带的官方文档中搜索objc_msgSendSuper,并将其官方的描述翻译出来。
当遇到方法调用时,编译器生成对objc_msgSend、objc_msgsend_stret,、objc_msgSendSuper或objc_msgsendsuper_stret函数之一的调用。发送到对象的超类(使用super关键字)的消息使用objc_msgSendSuper发送;其他消息使用objc_msgSend发送。将数据结构作为返回值的方法使用objc_msgSendSuper_stret和objc_msgSend_stret发送。
然后再看一下参数的描述。
super
指向objc_super数据结构的指针。传递标识消息发送到的上下文的值,包括要接收消息的类的实例和开始搜索方法实现的超类。op
SEL类型的指针。传递处理消息的方法的选择器。...
包含方法参数的变量参数列表。
再回过头来看转换后的代码(省略部分无用的代码)。
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MySubObject"))}, sel_registerName("class")));
}
根据参数描述可以得知,调用objc_msgSendSuper时,第一个参数是一个指向objc_super数据结构的指针,即__rw_objc_super,并且其中的参数有接收消息的类的实例self和超类(开始搜索方法实现的类),(id)class_getSuperclass(objc_getClass("MySubObject"))。这里我们就得出结论了,无论是[self class];还是[super class];,接受消息的类的实例都是self,即第一个隐式参数self均是MySubObject的对象,所以打印的结果都为MySubObject。并且,开始搜索方法实现的类是(id)class_getSuperclass(objc_getClass("MySubObject"))即MySubObject的超类MySuperObject,这也就解释了为什么使用super关键字能够调用到父类的方法。
自己调用objc_msgSendSuper
那么同样的,我们也可以自己调用objc_msgSendSuper来实现对父类方法的调用。
以study方法为例,首先查找objc_super结构体的源码。
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
objc_super结构体里面就是一个id receiver和Class super_class。那么就可以构造一个objc_super的结构体指针,它的receiver就是self即MySubObject,它的super_class就是父类MySuperObject的类对象。
struct objc_super my_objc_super;
my_objc_super.receiver = self;
my_objc_super.super_class = MySuperObject.class;
接着在源码中查看objc_msgSendSuper的签名为objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),所以可以将objc_msgSendSuper函数指针赋给我们自己根据objc_msgSendSuper函数签名定义的变量,然后通过这个变量来调用objc_msgSendSuper,第一个参数是objc_super的结构体指针,第二个参数就是方法名。
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&my_objc_super,@selector(study));
最终run起来来验证代码是否可行,发现其正确打印了结果。
objc_msgSend
源码
在objc源码中搜索objc_msgSend函数的实现,发现在不同的架构文件中,都有该函数的实现,那我们主要查看arm64架构下实现。
通过查看源码发现,objc_msgSend函数是使用汇编来实现的,这样子就会比使用C语言来写更快,同时可以免去大量拷贝局部变量等操作。
//进入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是小于判断,也就是小于的时候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
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空处理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
CacheLookup汇编源码
//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//从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表示如果不相同则跳转到2f
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
方法快速查找流程
MySubObject *sub = [[MySubObject alloc] init];
[sub study];
我们接着利用之前的代码,在调用study方法的地方打一个断点然后执行,断点到的时候进入汇编。
通过打印确认是study方法。继续往下调试。
在获取到buckets之后,如果在buckets中找到了我们所需要的方法,就会调用这一行汇编。
2: CacheHit \Mode // hit: call or return imp
当缓存命中的时候,就会调用或返回方法的imp。如果缓存未命中,则会调用objc_msgSend_uncached函数。
方法快速查找流程小结
- objc_msgSend函数有两个默认的参数,receiver接收者和sel方法名称
- 判断receiver是否存在,即执行方法的对象是否存在
- 获取对象的isa指针,通过isa指针获取类对象
- 通过类对象进行内存平移得到cache
- 通过cache获取到buckets
- 在buckets中查找sel,如果有则缓存命中,没有的话调用objc_msgSend_uncached
方法的慢速查找流程
刚才我们探索了方法的快速查找流程,即在缓存中命中了我们所调用的方法,那如果缓存没有命中时,调用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
在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,并且注释中已经告诉我们参数信息了,接着查看lookUpImpOrForward源码。
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())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
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)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
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)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
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
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;
}
首先lookUpImpOrForward这个方法也是返回一个imp,这个和快速查找中缓存命中时返回imp是一样的。第一行const IMP forward_imp = (IMP)_objc_msgForward_impcache;定义了一个消息转发的变量,后面也做了一些加锁、初始化等操作。跳过这些,接下来看关键的代码。
// 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().
// 上面注释也告诉我们,在加锁以后,会再次去查看缓存是否能命中,但在大多数情况下不会命中,这是为了避免多线程操作时,刚好调用函数,缓存就进来,从而没有取到缓存
3. 查找当前的method list,如果找到了就执行Done代码块
4. 如果没有找到
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 在当前的类的method list中查找我们所需要的方法method_t
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
// 如果找到了方法,就执行done代码块
if (meth) {
imp = meth->imp(false);
goto done;
}
// 如果在当前类中没有找到当sel时,如果superClass不为空,就先将curClass赋值为curClass->getSuperClass(),superClass为空那么就将我们之前定义的方法转发的变量forward_imp赋值给imp,并且结束循环。不为空就接着运行。
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 当在curClass没有找到sel,并且将curClass = curClass->getSuperclass()以后,再在父类中查找方法缓存
// Superclass cache.
imp = cache_getImp(curClass, sel);
// 如果imp是forward_imp,说明在根类中都没有找到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;
}
// 如果在superClass中找到了sel,执行done
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
通过查看源码我们了解到了慢速查找的一些流程,发现如果查找到了sel,就会执行Done代码块,那我们看一下这个代码块。
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
这个代码块调用了log_and_fill_cache函数,我们查看这个函数源码。
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);
}
在这个函数的最后,把我们所找到的方法的sel、imp和调用者receiver全都插入到了缓存中。那么,这一整个慢速查找的流程就比较清晰了。
慢速查找二分算法
从刚才的源码看到,在从缓存中查找方法时,调用的是getMethodNoSuper_nolock函数,通过函数调用链getMethodNoSuper_nolock->search_method_list_inline->findMethodInSortedMethodList->findMethodInSortedMethodList,最终在findMethodInSortedMethodList找到这么一段从列表中查找元素的方法,接下来让我们通过模拟解析一下这段代码。
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
假设list的长度为16,first和base都初始化为list的第一个元素,keyValue是一个unsigned long类型的值。
第一次循环
probe的值被赋值为base + (count >> 1),count >> 1就是将count右移1位,当前count是16,右移1位等于8,probe就是第一个元素平移8个位置的元素(后续为了方便直接用数字来表示probe、base等元素),即probe=8。这个时候比较二者的hash值,如果sel的哈希值大于当前probe的哈希值,那么就把base元素赋值为probe平移1个位置元素,即base=9,同时把count-1,此时count=15,判断循环结束条件,count还不等于0,所以将count再右移1位,count=7。如果hash值小于probe的哈希值,则说明也不做,则base=0,判断循环结束条件,count还不等于0,所以将count再右移1位,count=8。
第二次循环
第二次循环要分两种情况来考虑,一种是第一次循环keyValue > probeValue,另一种则反之。
首先分析keyValue > probeValue的情况:此时base为9,count = 7,count >> 1 = 3,所以probe的值为9+3=12。以此类推。
而当keyValue < probeValue时:此时base为0,probe = 8,count = 8,count >> 1 = 4,所以probe的值为0+4=4。以此类推。
命中查找
当缓存命中时,执行了一个while循环语句,这个语句的作用就是在命中的位置和第一个位置中间,继续查找,看看是否有其他相同的方法存在里面,最后返回list中的第一个(下标最小的)匹配的sel。
二分查找小结
通过数据模拟之后,我们发现查找的算法其实就是二分查找法,大致就是通过首游标base和尾游标count还有索引游标probe,每次都把probe设置为base+count>>1,即base和count中间的那个数,如果要查找的值大于probe,就把probe赋值给base,如果小于probe,就把count>>1来将count减半,依次类推,直到最后命中查找或者查找失败。
当命中查找时,还会向前遍历,找到在命中的位置之前是否有同名的sel,来找到数组中出现的第一个匹配的sel,通过注释我们也可以看出,分类的方法也会插入到method list中,并且在本类的同名方法之前,这也解释了为什么分类的方法会覆盖本来的方法。
慢速查找小结
- 调用了lookUpImpOrForward函数
- 再次去缓存中查找sel,避免多线程操作时,刚好调用函数,缓存就进来,从而没有取到缓存
- 查找curClass的method list,如果找到了方法就执行Done代码块
- 如果没有找到方法,把curClass变成curClass的superClass,通过继承链继续查找,如果superClass是nil就将imp设置为刚开始初始化的消息转发对象forward_imp并结束循环
- 通过继承链查找时,查找父类的cache,如果找到了,就执行Done代码块,没有找到就回到第三步
- 在Done中,将找到的方法插入到cache中
- 如果都没有找到,将imp赋值为消息转发对象forward_imp