Objective-C 里每一个对象都会指向一个类,每一个类都会有一个方法列表,方法列表里的每一个方法都是由 selector、函数指针imp 和 metadata 组成的。objc_msgSend的工做就是传入对象和selector,查找相应方法的函数指针,而后跳到函数指针所指向的位置。html
objc_msgSend是用汇编写的缘由有两个:缓存
消息发送的代码能够被分为两部分:objc_msgSend中有一个快速路径,是用汇编写的,还有一个慢速的路径,是用C实现的。汇编部分主要实现的是在缓存中查找方法,而且若是找到的话就跳转过去的一个过程。若是在缓存中没有找到方法的实现,就会调用C的代码来处理后续的事情。bash
分析objc_msgSend的流程:多线程
根据上述流程分析objc_msgSen的汇编。架构
如今,苹果公司已经开源了 Objective-C 的运行时代码。你能够在苹果公司的开源网站,找到 objc_msgSend 的源码。app
ARM64架构下有31个通用寄存器,每一个都是64位宽的。他们被标记为x0~x30。一样也有可能使用w0到w30来访问寄存器的低32位。寄存器x0~x7被用于函数入参的前8个参数。这就表示objc_msgSend收到的self参数是保存在x0中,selector _cmd参数在x1里。函数
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
复制代码
判断存储在p0中的self是否为空。若是小于0,跳转到 LNilOrTagged, 执行tagged_pointers状况,若是等于0,跳转到 LReturnZero ,执行发送消息给nil的状况。(在ARM64上 经过设置指针的高位来指明是tagged pointer。(x86-64上是设置低位)。若是高位被设置了1,且被做为一个带符号的整型解析的时候,那么值就是负数。通常状况下self是正常的,不会进入这些分支。)oop
ldr p13, [x0] // p13 = isa
复制代码
加载x0所指向的内存,实则是加载self的isa指针。一个对象的第一个指针就是isa指针。p13寄存器存储了isa。性能
GetClassFromIsa_p16 p13 // p16 = class
复制代码
执行宏 GetClassFromIsa_p16网站
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
复制代码
判断 SUPPORT_INDEXED_ISA ,表示isa_t中存放的信息是Class的地址,仍是一个索引(根据索引能够在类信息表中查找该类的结构地址)。iOS设备SUPPORT_INDEXED_ISA 为0,而后判断是否为64位,执行
and p16, $0, #ISA_MASK
复制代码
ARM64可使用非指针的isa。一般isa指针指向的是对象的类,可是非指针的isa利用了备用的bit位,填充了一些其余的信息。这条汇编指令执行了一个逻辑与运算,掩盖掉了全部额外的位,把实际的指向类的指针保存在p16寄存器中。
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
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
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// 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
3: // double wrap
JumpMiss $0
.endmacro
复制代码
分析上面的宏
#define CACHE (2 * __SIZEOF_POINTER__)
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
复制代码
从x16寄存器编译16个字节,取到的值保存到p10和p11中。方法缓存的结构以下:
typedef uint32_t mask_t;
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
复制代码
p10存储了buckets的值,p11的高32位保存了_occupied,低32位保存了_mask。
_occupied指定了哈希表中包含了多少条目,在objc_msgSend中不起什么做用。_mask很重要:它描述了哈希表的尺寸,方便用于与运算的掩码。它的值老是一个2的幂减一,用二进制的方法描述看起来就像是000000001111111,末尾是可变数量的1。经过这个值能够知道selector的查找索引,并在查找表的时候包裹着结尾。
and w12, w1, w11 // x12 = _cmd & mask
复制代码
x1中包含_cmd,因此w1包含了_cmd的低32位。w11包含了上面提到的_mask。这条指令将这两个值作与运算并将结果放到w12中。结果至关因而计算_cmd & mask,可是避免了开销很大的模运算。这一步计算出了传入的selector的起始哈希表的索引。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
复制代码
这条指令经过索引表的指针向左位移,再加上buckets,获得第一个查找的bucket的地址,也就是imp。
ldp p17, p9, [x12] // {imp, sel} = *bucket
复制代码
把x12保存的bucket的地址,每一个bucket包含一个selector和imp。p17包含了当前的imp,p9包含了当前的selector。
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
复制代码
对p1中的cmd和p9中的selector比较。若是相等,CacheHit将imp返回。接下去就是执行目标方法的代码了,objc_msgSend的快速路径到此已经结束了。全部参数寄存器不会受到干扰,原封不动的传给目标方法,就好像直接调用了目标方法同样。若是不相等,跳转到2f位置,处理不相等的逻辑,执行CheckMiss。
来看CheckMiss的宏定义
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
复制代码
因为传入的是nomal模式,会进入__objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
复制代码
p9包含了从bucket加载的selector,这条指令是p9和0做比较,若是等于0跳转到__objc_msgSend_uncached,这就说明这是个空的bucket,意味着目标方法不在缓存中,这时候会进入C语言方法__objc_msgSend_uncached,执行详细的查找流程。若是不为0就说明bucket不是空,只是没有找到,则继续查找。
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
复制代码
p12中存储的是当前的bucket地址,p10中存储的是buckets哈希表的首地址,比较若是匹配,跳转到3f处。 若是不匹配会继续执行
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
复制代码
再一次从缓存的bucket中加载。此次他从偏移量为BUCKET_SIZE的地方加载当前缓存bucket的地址。地址引用末尾的感叹号是一个有趣的特性。这指定一个寄存器进行回写,意思就是寄存器会更新为计算后的值。这条指令有效的执行了x12 -= 16来加载新的bucket,并使x12指向这个新的bucket。
如今已经加载了一个新的bucket,因此接下去的执行就要回到以前的检查当前bucket是否匹配的代码。这条指令表明回到1b,使用新的值再执行一次全部代码。若是仍然没有找到匹配的bucket,这些代码会持续执行,直到找到匹配的,或者空的bucket,或者命中表的开头。
3f处匹配的逻辑指令:
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// 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
复制代码
x12中包含了当前的bucket指针,w11表示的是mask,表的大小。将mask左移1+PTRSHIFT位,加上buckets的首地址。获得的结果指向表的末尾。
把获得的新的bucket存储到p17,p9。
cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
复制代码
这段代码仍是去检测bucket是否匹配,而且跳转返回imp。咱们能够看到有两次123的流程,第二次123就是为了防止两种状况:
第一种,多线程调用的时候给的一次容错机会。
第二种是 为了在遇到内存被破坏或者无效对象时,防止陷入无限循环而榨干性能。举个例子,堆损坏可以在缓存中塞满非0的数据,或者设置缓存的掩码为0,缓存不命中就会一直循环执行缓存扫描。额外的检查能够中止循环,将问题转变为崩溃日志。
参考资料: