前言

一、Runtime

二、objc_msgSend代码初探

三、objc_msgSend汇编实现

四、方法快速查找(cache的查找流程)


通过对cache_t的探索,我们清楚了整个cache_t的结构以及缓存方法的流程。但是缓存方法是在什么时候去进行插入的呢?然后缓存方法又是如何去查找的呢?那么我们就要对整个方法的查找流程进行深入的分析了。


一、Runtime

我们都知道oc方法的底层都是走的消息发送,通过clang编译下面的代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyTestObjc *objc = [[MyTestObjc alloc] init];
        [objc test1];
        
    }
    return 0;
}
编译之后:
  MyTestObjc *objc = ((MyTestObjc *(*)(id, SEL))(void *)objc_msgSend)((id)((MyTestObjc *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyTestObjc"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc, sel_registerName("test1"));

那么在编译期,每一个方法的调用都会变成objc_msgSend函数。
然后在运行时,通过objc_msgSend去进行消息发送。
先总结一下Runtime:

从编译时和链接时再到运行时,Objective-C语言会尽可能多地推迟决策。它就会动态地执行操作。这意味着该语言不仅需要编译器,而且还需要运行时系统来执行编译后的代码。运行系统充当一种用于Objective-C语言的操作系统;这就是oc动态性的原因。Runtime 就是一套由c/c++、汇编写的API,使程序在运行时动态的创建对象、检查对象以及修改类和对象的方法等。

我们在调用Runtime的时候,总共有3种途径:
1、通过OC代码直接调用(比如: [objc test1])
2、通过framework/service(比如: iskindOfClass)
3、直接通过Runtime Api (比如:objc_msgSend)。


二、objc_msgSend代码初探

 MyTestObjc *objc = [[MyTestObjc alloc] init];
 objc_msgSend(objc,@selector(test));

上面代码可以看到,我们可以直接用objc_msgSend函数来发送消息。此时需要在build setting关闭objc_msgSend的检测机制:

我再建一个MyTestObjc的父类(MyBaseTestObjc),在父类里面实现baseTest方法。

 MyTestObjc *objc = [[MyTestObjc alloc] init];
objc_msgSend(objc,@selector(baseTest));

同样的可以用objc_msgSend发送消息。
与此同时,也可以直接通过objc_msgSendSuper的方法直接给父类发消息。

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

通过objc_msgSendSuper的方法定义,我们可以确定其2个参数,第一个是objc_super,第二个sel。

/// 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结构体可以确定,objc_super有2个成员,一个是receiver(消息接受者),一个是super_class(父类)。
所以可以直接由下面代码来进行父类消息发送:

 struct objc_super mySupter;
mySupter.receiver = objc;
mySupter.super_class = MyBaseTestObjc.class;
objc_msgSendSuper(&mySupter, @selector(baseTest));

三、objc_msgSend汇编实现

首先看了源码之后,objc_msgSend是用汇编写的。

1、使用汇编的原因首先是快!可以免去大量局部变量拷贝的操作,会更有针对性的去运算,参数会直接被存放在寄存器中,当找到IMP时,参数已经保存在了寄存器中,可以直接使用

2、由于方法的参数有不确定性,我们在用c/c++的时候,无法更好的满足不确定的参数需求。当然c/c++可以使用va_list来实现动态参数,但是从效率上来说还是汇编更高。


四、方法快速查找(cache的查找流程)

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check cmp对比命令 po代表对象的isa地址
#if SUPPORT_TAGGED_POINTERS //是否是taggedpointers对象
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
    //
	ldr	p13, [x0]		// p13 = isa //ldr加载指令,取x0地址的内容保存在p13,p13就是对象的isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class  通过isa&mask = class
LGetIsaDone: //获取isa完毕
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

通汇编可以看到,首先拿到消息接受者对象,判断是否为空,如果为空直接返回0,这也是为什么给nil发消息不会崩溃的原因。接着如果不为空,拿到对象的首地址,加载首地址对应的值,也就是isa,放到p13中。然后通过与掩码的&操作,得到class。接着会走CacheLookup流程,也就是缓存查找流程。
以下简化CacheLookup的汇编(不区分宏定义的判断):

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	mov	x15, x16			// stash the original isa //mov数据传送指令
LLookupStart\Function:
	// p1 = SEL, x16 = isa
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets //#define CACHE  (2 * __SIZEOF_POINTER__) 通过isa的地址,位移16的字节,找到对应的mask|buckets,也就是cache_t中的第一个成员
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets     通过p10加上掩码,得到buckets
	and	p12, p1, p11, LSR #48		// p12 = _cmd & mask //首先p11 逻辑右移48位 (存的时候:_bucketsAndMaybeMask.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets)所以可以直接拿到mask,然后p12 = _cmd & mask 拿到哈希索引。

#error Unsupported cache mask storage for ARM64.
#endif
add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) //PTRSHIFT = 3 ,这句代码意思就是:buckets为首地址,因为一个bucket是16个字节,所以 通过 (_cmd & mask) << (1+PTRSHIFT),就平移到了指定的bucket的了。
                        

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets) //
	b.hs	1b
    add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
                        	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

1、首先通过isa位移16个字节拿到cache_t,cache_t的首地址也就是指向_buketsAndMaybeMask。
2、接着通过_buketsAndMaybeMask & 0x0000ffffffffffff 得到buckets。
3、再通过_buketsAndMaybeMask右移48位拿到mask,然后通过sel & mask 拿到哈希索引。
4、再通过首地址平移(sel & mask) << 4(因为一个bucket是16个字节,所以通过 (_cmd & mask) << (1+PTRSHIFT),就平移到了指定的bucket的了), 确定了bucket。
5、判断bucket里面的sel 是否等于要查找的sel,如果等于直接返回imp。
6、如果不等于,开始向前遍历查找。查找时,如果sel ==0 直接goto miss
7、如果循环下来还没有找到,将从最后一个桶子继续循环向前查找,直到遍历到第一次确定的桶子时还没有找到,查找结束,goto miss。

流程图总结:

补充:为什么CacheLookup拿到第一次探测的bucket后要向前查找,这里我的分析是:在insert插入缓存的时候,为了解决哈希冲突,使用了再次哈希取下标

#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

而再次哈希的算法是: 对原来的下标进行-1的操作,也是一个向前插入的过程。所以cache的查找和插入流程就完美的契合了。