OC底层原理06:消息流程分析之快速查找过程

1,436 阅读5分钟

上一篇文章,我们探索了,当实例对象调用实例方法时,方法insertsel和imp插入到cache的过程。现在我们再来看看,当调用方法时,是如何从cache中将其取出来的,即sel-imp的快速查找


objc_msgSend铺垫

1. 源码查看

在调用方法时,是怎么跑到insert方法的呢?

在源码objc_cache.mm中搜索insert,除了可以看到上篇文章中的insert方法之外,还找到了cache_fill方法: 是在cache_fill中,调用了insert的,再搜cache_fill

也就是说在Cache writers缓存写入中,会进行cache_fill操作,而且在缓存写入前,会先进行Cache readers缓存读取的过程,其中有objc_msgSend 和 cache_getImp


2. Clang

使用Clang编译如下代码:

@interface Person : NSObject

- (void)sayHello;
- (int)addNumber:(int)number;

@end

@implementation Person
- (void)sayHello{
    NSLog(@"Hello world");
}
- (int)addNumber:(int)number{
    return number+1;
}
@end

Person *p = [Person alloc];
[p sayHello];
int result = [p addNumber:2];

在cpp文件中,我们可以看到编译后的结果: 无论是调用类方法alloc,还是实例方法sayHello,都会编译成objc_msgSend(消息接收者,方法主体,方法参数..)

消息接收者的意义,在于通过消息接收者才能找到方法的寻根路径


3. 拓展

#import <objc/message.h>
@interface Tercher : Person
@end

@implementation Tercher
@end


Person *p = [Person alloc];
[p sayHello];
objc_msgSend(p, sel_registerName("sayHello"));

Tercher *t = [Tercher alloc];
[t sayHello];
struct objc_super xsuper;
xsuper.receiver = t;
xsuper.super_class = [Person class];
objc_msgSendSuper(&xsuper, sel_registerName("sayHello"));

需要设置Build Settings->Enable Strict Checking of objc_msgSend Calls为NO。


objc_msgSend快速查找

objc_msgSend

源码中全局搜objc_msgSend,查找到objc-msg-arm64.s的汇编代码:

objc_msgSend函数是所有OC方法调用的核心引擎,负责查找方法的实现,并执行。因调用频率非常高,其内部实现对性能的影响大,所以使用汇编语言来编写内部实现代码。汇编的特点有速度快、参数不确定性。

解析:

1.
cmp p0, #0 	// nil check and tagged pointer check

第一段代码cmp(compared对比),我们从注释就可以看到nil check判断是否为nil。其中p0是objc_msgSend的第一个参数消息接收者receiver,那么这句代码的含义就是:判断接收者是否存在


2.
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif

SUPPORT_TAGGED_POINTERS判断是否支持小对象类型,支持会b.le跳转到LNilOrTagged,否则b.eq LReturnZero返回空。

当支持小对象类型时,仍会由cmp p0, #0的结果来决定是否继续,消息接收者为空则同样调用LReturnZero

le = less equal 小于等于; eq = equal 等于


3.
ldr p13, [x0]       // p13 = isa 

根据对象拿到isa存入p13寄存器中。


4.
GetClassFromIsa_p16 p13     // p16 = class 

在64位真机中,将$0(传入的p13->isa)ISA_MASK掩码进行与运算,可以得到class类信息,查找到类信息后,就可以偏移到cache进行方法的查找,即CacheLookup NORMAL 快速查找

5.
LGetIsaDone: // 获取isa完毕
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend

CacheLookup NORMAL

源码:

.macro CacheLookup
 //
 // Restart protocol:
 //
 //   As soon as we're past the LLookupStart$1 label we may have loaded
 //   an invalid cache pointer or mask.
 //
 //   When task_restartable_ranges_synchronize() is called,
 //   (or when a signal hits us) before we're past LLookupEnd$1,
 //   then our PC will be reset to LLookupRecover$1 which forcefully
 //   jumps to the cache-miss codepath which have the following
 //   requirements:
 //
 //   GETIMP:
 //     The cache-miss is just returning NULL (setting x0 to 0)
 //
 //   NORMAL and LOOKUP:
 //   - x0 contains the receiver
 //   - x1 contains the selector
 //   - x16 contains the isa
 //   - other registers are set as per calling conventions
 //
LLookupStart$1:

 // p1 = SEL, p16 = isa
 ldr p11, [x16, #CACHE]    // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
 and p10, p11, #0x0000ffffffffffff // p10 = buckets
 and p12, p1, p11, LSR #48  // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
 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


 add p12, p10, p12, LSL #(1+PTRSHIFT)
               // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

 ldp p17, p9, [x12]  // {imp, sel} = *bucket
1: cmp p9, p1   // if (bucket->sel != _cmd)
 b.ne 2f   //     scan more
 CacheHit $0   // call or return imp
 
2: // not hit: p12 = not-hit bucket
 CheckMiss $0   // miss if bucket->sel == 0
 cmp p12, p10  // wrap if bucket == buckets
 b.eq 3f
 ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
 b 1b   // loop

3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
 add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
     // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
 add p12, p12, p11, LSL #(1+PTRSHIFT)
     // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

 // Clone scanning loop to miss instead of hang when cache is corrupt.
 // The slow path may detect any corruption and halt later.

 ldp p17, p9, [x12]  // {imp, sel} = *bucket
1: cmp p9, p1   // if (bucket->sel != _cmd)
 b.ne 2f   //     scan more
 CacheHit $0   // call or return imp
 
2: // not hit: p12 = not-hit bucket
 CheckMiss $0   // miss if bucket->sel == 0
 cmp p12, p10  // wrap if bucket == buckets
 b.eq 3f
 ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
 b 1b   // loop

LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
 JumpMiss $0

.endmacro


1.
 // p1 = SEL, p16 = isa
 ldr p11, [x16, #CACHE]    // p11 = mask|buckets

其中#CACHE == 2*8 = 16为: 由类结构可知,将isa位移16个字节,可以得到cache,即最终结果p11=cache。但是注释为什么是p11 = mask|buckets? 原因在于:在64位系统下为了节省内存读取方便,mask和buckets存在了一起,cache的结构:


2.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
and	p12, p1, p11, LSR #48		// x12 = _cmd & mask

p11(mask|buckets)0x0000ffffffffffff进行与运算,会将其高16位进行抹零,得到的结果就是buckets,存入p10

and p12, p1, p11, LSR #48分为两段,首先计算p11, LSR #48,将p11进行逻辑右移48位,即可得到cache中的mask。然后将p1与运算mask的结果存在p12中。其中p1为sel(_cmd)。看CacheLookup源码中最开头的注释里。 最终与运算的结果p12,就是方法存在buckets下标

因为在上篇文章,insert方法中要插入的位置,就是利用sel & mask计算得到的下标.


3.
add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

这里也分为两段看待:

  • p12, LSL #(1+PTRSHIFT)。全局搜索PTRSHIFT 64位真机下,PTRSHIFT = 3,那么第一段代码的含义就是将方法的下标进行逻辑左移4位。左移4位也等同于2^4

    0000 0001 << 4  = 0001 0000 = 16 = 2^4
    

    所以上段汇编代码的含义就是,方法的下标 * 2^4。得到的结果存入p10

  • add p12, p10

p12保存的buckets的首地址,这段汇编就是从首地址,偏移方法下标 * 2^4个字节,得到要查找的方法bucket_t

为什么下标乘上16个字节?原因在于bucket_t中保存的是sel和imp,都为8个字节,一个bucket_t就是16字节,所以下标乘每个bucket_t的大小,就可以找到,下标所指的bucket_t。bucket_t等同于汇编中的bucket,bucket_t是C语言中的结构体,bucket是汇编的。


4.
ldp	p17, p9, [x12]		// {imp, sel} = *bucket

以上通过得到的方法所在的bucket,也就找到了其中的imp和sel,分别保存在p17、p9


5.
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp

这段汇编代码的含义,在注释中也可以很清楚的了解,通过对比查找到的bucket中的sel是否等于CacheLookup传入的参数p1(_cmd),不相等b.ne 2f跳转到第2步,相等则CacheHit $0 缓存命中,进行imp返回


6.
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

查找到的sel不等于参数p1(_cmd)时,首先会判断查找到的bucket是否等于buckets,即是否是buckets的开头,相等会跳到3,否则ldp p17, p9, [x12, #-BUCKET_SIZE]!。即不相等时,会向前找bucket,再次跳转到1进行loop循环。


7.
3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

当查找到的bucket等于buckets,即等于开头第一个时:add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))

其中p11在最开始就知道p11 = mask|buckets,p11逻辑右移44位,也可以认为是p11中的mask左移了4位,即注释中 (mask << 1+PTRSHIFT) == mask * 2^4

在上篇文章中,mask的值等于capacity-1,即buckets中所有的结构体个数减一。

所以这句汇编的含义就是:当查找的bucket等于buckets中第一个时,会偏移到最后一个bucket,再次进行比较


快速查找的过程总结:



推荐参考

深入解构objc_msgSend函数的实现

objc_msgSend流程分析之快速查找