OC的消息发送objc_msgSend

我们平时写得最多的代码,就是让一个对象去调用一个方法,一般都是这样的:

XXPerson *p = [XXPerson alloc];
[p method1];
复制代码

那么,为什么[p method1]这一句就可以找到实例方法method1呢?中间的过程是怎样的呢?今天我们来探讨一下。

初见objc_msgSend

先看下这段代码:

图片.png

通过clang -rewrite-objc main.m命令编译main.m文件,得到main.cpp文件,打开并找到main方法被编译后的代码:

图片.png

可见,两次调用实例方法都被编译成了objc_msgSend函数,这个函数名的字面意思就是“发送消息给对象”。既然是给某个对象发消息,那么消息的内容应该包含这几项:
1、给哪个对象发消息?
2、让这个对象做什么?
3、有什么数据要传给这个对象?

objc_msgSend函数后面带着的参数就回答了上面这几个问题:
第一个参数p是消息的接收者
第二个参数是表示要让这个对象执行的方法名,比如sel_registerName("method1")就表示让对象p执行method1方法,
第三个参数就表示传给对象的数据,比如method2中传给了p对象一个字符串“person”。

在main.cpp文件中,我们还看到了objc_msgSendobjc_msgSendSuper

图片.png

前者我们已经知道objc_msgSend是做什么的,那么objc_msgSendSuper又是做什么的呢?这两个有什么区别呢?

我写了两个类,XXTeacher和XXPerson,XXTeacher继承自XXPerson,我们看下XXTeacher类中的这段代码及打印结果:

图片.png

这道题面试的时候很常见,很多人会认为第二行会打印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方法里:

图片.png

图片.png

看下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类,都没找到的话,那么就会进入消息转发阶段。

这篇文章主要是讲在消息发送过程中,类是如何找方法的,消息转发将在下篇文章介绍。

猜你喜欢

转载自juejin.im/post/7109169608518533156