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

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


objc_msgSend铺垫

1. 源码查看

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

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

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


2. Clang

使用Clang编译以下代码:oop

@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(消息接收者,方法主体,方法参数..)性能

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


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。spa


objc_msgSend快速查找

objc_msgSend

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

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

解析:

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,分别保存在p1七、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流程分析之快速查找

相关文章
相关标签/搜索