我们都知道类里面有个cache_t的结构体来缓存方法,缓存方法的目的就是为了当这个方法再次调用的时候能够更快的响应。今天我们就来分析一下方法的响应查找原理
objc_msgSend和objc_msgSendSuper
在objc源码的objc-cache文件下有这样一句注释
意思就是从cache缓存中读取数据的时候是通过objc_msgSend和cache_getImp这两个函数来查找方法的。
那我们今天就研究一下objc_msgSend这个函数。先通过一个实例来感受一下
@interface MyClass: MySuperClass
- (void)study;
- (void)happy;
+ (void)eat;
@end
@implementation MyClass
- (void)study {
NSLog(@"%s", __func__ );
}
- (void)happy {
NSLog(@"%s", __func__ );
}
+ (void)eat {
NSLog(@"%s", __func__ );
}
@end
在main.m文件中去调用一下一下代码
MyClass *p = [MyClass alloc];
[p study];
[p happy];
在终端中输入命令:clang -rewrite-objc main.m编译一下main.m文件,编译完成之后在原文件夹下会出现一个main.cpp的文件,打开main.cpp文件找到main方法
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
MyClass *p = ((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("study"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));
}
return NSApplicationMain(argc, argv);
}
此时可以看到我们调用的方法都被转换成objc_msgSend的函数,同时带有两个默认隐式参数,第一个参数是消息的接收者,第二个参数是消息的方法名(SEL)。objc_msgSend就可以通过这两个参数来找到方法的实现。
如果调用的是类方法,'消息的接收者'就是类对象,就会通过类对象的isa指针(指向元类),从元类对象里面去找对应的方法。
同理如果调用的是实例方法,'消息的接收者'就是实例对象,就会通过实例对象的isa指针从类里面找方法的实现。
以上实例中方法都是无参的,而对于有参数的方法在转换为objc_msgSend函数时,参数就会追加在两个隐式参数后,如下所示给study方法添加一个NSString类型的参数:
- (void)study:(NSString *)str {
NSLog(@"%s", **__func__** );
}
转换为objc_msgSend函数如下
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_l6_95lwxwzj28bf2w622vp49pxr0000gn_T_main_6bb293_mi_3);
既然方法调用会转换为objc_msgSend函数,那我们也可以直接调用objc_msgSend函数来实现方法的调用。例如[p happy];这个函数调用可以写成((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));或((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"happy"));
'objc_msgSend'有多种,当遇到方法调用时,编译器生成对以下函数之一的调用:
1. `objc_msgSendSuper`:发送到对象的超类(使用super关键字)的消息使用objc_msgSendSuper发送
2. `objc_msgSend_stret`:将结构体作为返回值的方法使用objc_msgsend_street发送。
3. `objc_msgsendsuper_street`:使用super关键字且将结构体作为返回值的方法
4. `objc_msgSend_fpret`:在i386平台上,用于返回浮点值的函数的ABI与用于返回整型类型的函数的ABI
不兼容。因此,在i386平台上,对于返回非整型的函数,必须使用objc_msgSend_fpret。
4. `objc_msgSend`:其他消息使用objc_msgSend发送
对于objc_msgSendSuper和objc_msgSend的区别也可以通过以下实例去了解:
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
[self class]和[super class]打印的结果一样都是我们的MyClass类,同样我们通过编译成cpp文件可以看到通过super关键字调用的class方法会转换成objc_msgSendSuper函数
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_LGTeacher_cbd05b_mi_1,((Class (*)(__rw_objc_super *, SEL))(**void** *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("class")));
同样也能看到其第一个隐式参数消息的接收者是self,和[self class]转换的objc_msgSend函数第一个隐式参数一样。所以两者打印的结果一样。super调⽤⽅法和self调⽤⽅法的区别就在于去找⽅法的时候出发点不⼀样⽽已。self会从当前类开始去找,⽽super会从当前类的⽗类开始去找。
了解了super关键字的原理之后,我们可以自己实现super关键字,例如以下方法
- (void)study {
[super study];
}
可以写成
- (void)study {
struct objc_super objc_super;
objc_super.receiver = self;
objc_super.super_class = MySuperClass.class;
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&objc_super, @selector(study));
}
消息的快速查找流程
了解了方法调用会转换成objc_msgSend函数之后,我们去探究一下objc_msgSend这个函数的具体查找流程。在objc源码里面搜索objc_msgSend发现针对每个架构都有对应的实现
我们就以真机的环境arm64架构为例来分析,找到arm64架构下的objc_msgSend实现发现使用汇编来写的
汇编会在函数和全局变量前面自动加入一个`_`,因为在一个程序当中往往会包含汇编和C文件,对于编译器来说,
汇编和C文件是一样的,加入`_`目的是用来防止符号云冲突。
使用汇编来写是一个很重要的原因就是快。首先汇编比C更快,其次使用汇编的时候可以免去大量的局部变量的拷贝
的操作,它的参数会直接存放在寄存器当中,当找到imp的时候,函数的参数就已经存放在寄存器当中了,可以对参
数进行一个直接的使用。
为了更直观的了解objc_msgSend汇编查找流程,我们可以通过一下代码进行断点调试
勾选Xcode -> Debug -> Debug Workflow -> Always Show Disassembly打开汇编
打开汇编之后,进入objc_msgSend内
此时我们可以验证一下是否是MyClass实例对象在调用study方法,读取一下寄存器内的值
发现的确是MyClass实例对象在调用study方法后,继续向下执行
执行到这一步,我们可以读取寄存器中的值验证一下x11中是否存储了cache中的第一个元素值
通过打印寄存器中的值可以看出x11存储的就是cache中的第一个元素值_bucketsAndMaybeMask。那么x10寄存器中存储的就是缓存方法的桶子buckets。接下来就是去buckets中去查找方法
如果x9和x1相同则说明已经找到对应的方法,返回对应的imp走CacheHit。如果没找到就调用_objc_msgSend_uncached。通过以上分析对objc_msgSend的快速查找流程总结如下:
消息的慢速查找流程
接下来我们再分析_objc_msgSend_uncached中消息的查找,在objc源码中搜索查找_objc_msgSend_uncached
从源码中发现_objc_msgSend_uncached逻辑比较简单,只调用了两个关键的方法MethodTableLookup和TailCallFunctionPointer。分别查看一下这两个方法的实现
MethodTableLookup
在MethodTableLookup方法内又调用了lookUpImpOrForward的方法来获取IMP
TailCallFunctionPointer
TailCallFunctionPointer方法直接返回函数指针的值(IMP)
从断点调试中也可以看出_objc_msgSend_uncached内会调用lookUpImpOrForward方法查找IMP
那么我们就看一下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();
//判断cls是否进行初始化
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
//是否是已知的类
checkIsKnownClass(cls);
//确定当前类的父类的继承链的关系
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
//局部变量curClass赋值为当前类
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//再一次从cache里面去找IMP
//目的:防止多线程操作时,刚好调用函数,此时缓存进来了
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 跳转至 done 进行方法缓存
imp = meth->imp(false);
goto done;
}
//curClass类里面没有找到,curClass = curClass的父类
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
//找到父类为nil时还没有找到IMP,使用消息的转发,结束循环查找
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)) {
break;
}
if (fastpath(imp)) {
// 在父类链中找到了IMP,跳转至done 在当前类中进行缓存
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方法内进行方法查找主要是调用了getMethodNoSuper_nolock方法去方法列表中查找。
二分查找法
我们再去看一下getMethodNoSuper_nolock方法的实现
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
在getMethodNoSuper_nolock方法内又调用了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;
}
search_method_list_inline方法内的返回值是通过findMethodInSortedMethodList方法返回的,在findMethodInSortedMethodList方法内使用的就是二分查找,并且其代码逻辑非常精妙
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
//base = 0
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//count = list->count 初始化为列表的长度
//count >> 1 即 count除以2
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {//值相同,找到了
//倒序查找方法列表中第一次出现的值
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {//没找到
base = probe + 1;
count--;
}
}
return nil;
}
举例分析:
假如`count = 16`
1、小了的情况:
循环第一次:
base = 0 (初始值)
probe = 8 (base + (count >> 1))
base = 9 (probe + 1)
count = 15 (count--)
循环第二次:
count = 7 (count >>= 1)
probe = 12 (base + (count >> 1))
base = 13 (probe + 1)
count = 6 (count--)
循环第三次:
count = 3 (count >>= 1)
probe = 14 (base + (count >> 1))
probe 的值依次是 8 -> 12 -> 14,每次都取一半
2、大了的情况:
循环第一次:
base = 0 (初始值)
probe = 8 (base + (count >> 1))
循环第二次:
count = 8 (count >>= 1)
base = 0 (初始值)
probe = 4 (base + (count >> 1))
循环第三次:
count = 4 (count >>= 1)
base = 0 (初始值)
probe = 2 (base + (count >> 1))
probe 的值依次是 8 -> 4 -> 2,也是每次都取一半
findMethodInSortedMethodList方法内通过这个for循环实现二分查找的逻辑,,在for循环中如果找到了目标方法。则倒序从方法列表中获取这个值第一次出现的地方。因为如果存在分类,分类的方法会加载到方法列表的前面,这样处理的目的是如果分类里面有同名方法时,优先调用分类的方法。找到了方法之后,获取方法的IMP,跳转至done调用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里插入找到的sel和imp
cls->cache.insert(sel, imp, receiver);
}
通过以上分析对objc_msgSend的慢速查找流程总结如下:
以上就是objc_msgSend方法的快速查找和慢速查找流程。