我们平时写得最多的代码,就是让一个对象去调用一个方法,一般都是这样的:
XXPerson *p = [XXPerson alloc];
[p method1];
复制代码
那么,为什么[p method1]
这一句就可以找到实例方法method1呢?中间的过程是怎样的呢?今天我们来探讨一下。
初见objc_msgSend
先看下这段代码:
通过clang -rewrite-objc main.m
命令编译main.m文件,得到main.cpp文件,打开并找到main方法被编译后的代码:
可见,两次调用实例方法都被编译成了objc_msgSend函数,这个函数名的字面意思就是“发送消息给对象”
。既然是给某个对象发消息,那么消息的内容应该包含这几项:
1、给哪个对象发消息?
2、让这个对象做什么?
3、有什么数据要传给这个对象?
objc_msgSend函数后面带着的参数就回答了上面这几个问题:
第一个参数p是消息的接收者
,
第二个参数是表示要让这个对象执行的方法名
,比如sel_registerName("method1")就表示让对象p执行method1方法,
第三个参数就表示传给对象的数据
,比如method2中传给了p对象一个字符串“person”。
在main.cpp文件中,我们还看到了objc_msgSend
和objc_msgSendSuper
:
前者我们已经知道objc_msgSend是做什么的,那么objc_msgSendSuper又是做什么的呢?这两个有什么区别呢?
我写了两个类,XXTeacher和XXPerson,XXTeacher继承自XXPerson,我们看下XXTeacher类中的这段代码及打印结果:
这道题面试的时候很常见,很多人会认为第二行会打印XXPerson。我们来编译一下,看看底层的代码到底是什么样的。
static instancetype _I_XXTeacher_init(XXTeacher * self, SEL _cmd) {
if (self = ((XXTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("XXTeacher"))}, sel_registerName("init"))) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders__1_jx4jbsns2dz66q920kzxfjp80000gn_T_XXTeacher_a84b50_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders__1_jx4jbsns2dz66q920kzxfjp80000gn_T_XXTeacher_a84b50_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("XXTeacher"))}, sel_registerName("class")));
}
return self;
}
复制代码
我们看到,self调方法,编译后是objc_msgSend
方法,super调方法,编译后是objc_msgSendSuper
方法,不过super多了一个__rw_objc_super结构体,这个结构体中有两个参数,第一个表示接受消息的对象
,第二个表示开始搜索方法的超类
,这里是class_getSuperclass(objc_getClass("XXTeacher"))
,即XXPerson
类。也就是说,当我们调用[super class]的时候,接受消息的对象仍然是当前的self对象,不过要从XXTeacher的父类开始找方法。所以我们明白了,上面的两行代码打印结果一样,是因为让同一个对象执行了同一个方法。
了解objc_msgSend
我们来看下objc_msgSend方法里到底做了些什么。
XXTeacher *t = [[XXTeacher alloc] init];
[t study];
复制代码
加断点,跟下流程,走到了objc_msgSend方法里:
看下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是小于判断,也就是小于的时候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
复制代码
大致的流程就是:
1、判断消息接受者是否为空
2、拿到接收者的isa指针
3、通过isa和掩码,获取到对应的类
4、去类中的方法缓存里去查找要调用的方法
上面的第4步是在CacheLookup
方法中完成的,我们先来看看这个方法里面做了些什么。
//从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
复制代码
上面这一段的意思,就是:找到对应类以后,通过内存平移,找到cache的地址,从而获取缓存中的buckets
。
//去除掩码后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
复制代码
在缓存方法里面查找方法,如果找到了就调用CacheHit
方法,如果没找到,就会走_objc_msgSend_uncached
方法。
那我们就来看看_objc_msgSend_uncached
方法又做了些什么。_objc_msgSend_uncached调用了MethodTableLookUp
方法,MethodTableLookUp调用了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;
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;
}
复制代码
上面这段代码中,核心就是for (unsigned attempts = unreasonableClassCount();;)
这个for循环,这里先是会再次到缓存中查找一遍, 是为了防止恰巧在走到这个方法的时候,有其它的线程把我们要找的方法缓存到cache里了
,如果还是没有找到的话,就走getMethodNoSuper_nolock(curClass, sel)
方法,这个方法是准备到类的方法列表
里去找我们所要调用的方法,跟着这个方法会走到search_method_list_inline
方法, 这个方法是通过二分查找
算法来查找要调用的方法。
如果在当前类的方法列表里:
- 找到了对应的方法, 就
goto done
,会去调用log_and_fill_cache
方法,把找到的这个方法插入到当前类的cache中 - 如果没有在当前类的方法列表中找到方法,就会拿到当前类的父类:
- 先到父类的cache中去查找,如果找到了,插入到当前类的cache中
- 如果没找到,就到父类的方法列表中去查找:
- 找到了,就放到cache中
- 如果还没找到,就再去父类的父类中去找
如此一直沿着继承链向上查询,如果到了NSObject类,都没找到的话,那么就会进入消息转发
阶段。
这篇文章主要是讲在消息发送过程中,类是如何找方法的,消息转发将在下篇文章介绍。