objc_msgSend
是OC中调用最为频繁的方法,所有OC方法的调用都离不开这个它。苹果已经将其开源(https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/),这是使用汇编语言编写的,其好处就是能提升函数的执行速度。本文选用它的arm64为汇编代码(objc-msg-arm64.s)进行分析。
函数入口
首先,找到ENTRY _objc_msgSend
这一行,它是objc_msgSend的函数入口,下面逐行进行分析:
cmp p0, #0 将传入的第一个参数与0判断
这里的p0实际上就是x0,其定义在arm64-asm.h里面。
b.le LNilOrTagged // (MSB tagged pointer looks negative)
如果p0<0(即最高位为1),该对象是tagged pointer,实际上是一个为了节省空间而使用的特殊指针,关于它的详细描述可以看这篇文章,而当p0为0的时候,即代表传入对象为nil,函数应该立即返回,总之,都先要跳到LNilOrTagged
进行特殊处理。。
如果p0>0,则代码会继续执行下去:
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
然后将x0指向内存中的值(isa)赋值给p13,然后通过GetClassFromIsa_p16
的宏后,p16得到了class的地址。GetClassFromIsa_p16
的实现如下(剔除了SUPPORT_INDEXED_ISA的部分,因为它是针对watch的):
.macro GetClassFromIsa_p16 /* src */
and p16, $0, #ISA_MASK
#endif
ISA_MASK是定义在isa.h的宏,其值为0x0000000ffffffff8ULL。
可以看出class的地址是isa指针跟ISA_MASK与运算得来的,其中的关系可以参考这篇文章,这里就不展开讲了。
LGetIsaDone:
CacheLookup NORMAL
接下来,就是查缓存的流程,在讲这个之前,先把其它分支条件过一遍。
LNilOrTagged
LNilOrTagged的实现723版本和750版本的不太一样,不过原理是一样的,先看下723版本的:
LNilOrTagged:
1 b.eq LReturnZero // nil check
// tagged
2 mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
3 adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
4 ubfx x11, x0, #60, #4
5 ldr x16, [x10, x11, LSL #3]
6 b LGetIsaDone
- 首先,如果p0 = 0,则跳到
LReturnZero
返回。接下来就是处理tagged pointer的逻辑。 - tagged pointer有两种,一种是系统的,其isa的前4位为标志位,最高位位1。另一种是开发者扩展的,其isa的前8位是标志位,前4位都是1。因此,如果p0比0xf00....要大(这里是无符号比较),就跳到
LExtTag
进行扩展的处理,否则执行系统tagged pointer的逻辑。 - 取出
_objc_debug_taggedpointer_classes
的地址加载到x10中 - 获取x0的高4位保存到x11中(高4位也是isa指针在_objc_debug_taggedpointer_classes中的索引)
- 以x11作为索引,算出对应isa指针的内存地址存到x16中
- 取出class地址之后执行
LGetIsaDone
对于750的版本,逻辑是这样的:
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
750的版本并没有区分tagged pointer是系统的还是扩展的,直接就将其当成系统的处理取出class地址,在这之后,又将___NSUnrecognizedTaggedPointer
的地址赋给x10,如果取出的这个class地址跟NSUnrecognizedTaggedPointer相等,就代表这是一个扩展指针(因为如果是扩展指针的话,最高4位必须是1,通过前面的运算之后x16存的地址只能是一个确定的值。也可以由此推断出___NSUnrecognizedTaggedPointer
在_objc_debug_taggedpointer_classes
中的索引是0x1111。
我看不出750的实现方式优越在哪个地方,看起来都是9条汇编代码,希望有大神来解释一下。
LExtTag
求扩展的tagged pointer的class地址和系统的tagged pointer是类似的,其代码如下:
1 adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
2 ubfx x11, x0, #52, #8
3 ldr x16, [x10, x11, LSL #3]
4 b LGetIsaDone
- 取出
_objc_debug_taggedpointer_ext_classes
的地址加载到x10中。 - 去x0(isa指针)的高8位放到x11
- 通过索引求出class的地址并将其放到x16
- 执行
LGetIsaDone
LReturnZero
如果p0=0,则说明传入的类为nil,这个时候应该执行返回nil的逻辑
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
在arm64位中,函数整型的返回值会存在x0,x1中,而浮点数的返回值存在v1-v3中,由于不知道函数的调用者需要什么类型,因此会将上述寄存器都清空,x0已经是0了,因此不需要清空。
CacheLookup
不管是哪个分支条件,来到CacheLookup这个宏之后,p16都已经得到类的地址了,接下来就是查找缓存的过程
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
将class地址的CACHE偏移量的内存赋值给p10和p11,对于CACHE的定义可以在本文件中找到:
/* Selected field offsets in class structure */
#define SUPERCLASS __SIZEOF_POINTER__
#define CACHE (2 * __SIZEOF_POINTER__)
其中SIZEOF_POINTER是8个字节,因此这里偏移了16个字节,在objc-runtime-new.h中,我们可以找到objc_class的实现如下(注意不是runtime.h里面的objc_class,后者已经废弃掉了):
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
对于其父结构体objc_object,其定义在objc.h
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
这样,我们可以推断出isa指针的偏移量是0,superClass的偏移量是8,cache的偏移量是16。
对于cache_t的结构体,定义如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
其中bucket_t占了8字节,而mark_t占了4个字节,因此ldr p10, p11, [x16, #CACHE]
的结果是p10存了_buckets,p11高32位存_occupied,低32位存_mask(因为arm64默认是小端)
_buckets就是存缓存函数地址的地方,实际上是一个哈希表,_mask总是2的n次幂-1,也就是0x00....1111,通过它和函数方法可以求出函数在哈希表中的索引。
and w12, w1, w11 // x12 = _cmd & mask
通过上面的运算,就可以得到函数方法在哈希表的索引,实际上就相当于_cmd%哈希表的大小,可以看出,也就是说哈希表的构造方法是除留余数法。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
PTRSHIFT 的定义在arm64-asm.h,也就是3。p10是_buckets的首地址,因为bucket_t的大小为16个字节,所以需要将索引乘以16,也就是左移3位。计算完之后,p12里面就是对应的bucket的指针了。
ldp p17, p9, [x12] // {imp, sel} = *bucket
将bucket加载到p17和p9,bucket_t的结构如下:
struct bucket_t {
MethodCacheIMP _imp;
cache_key_t _key;
...
通过这一运算,p17存放了_imp,p9存放了_key,而_key实际上就是sel。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
找到sel后,就让传进来的sel和找到的sel作对比,如果一样,则跳到CacheHit执行函数,如果没找到,可能是出现哈希冲突了,开始继续查找找的逻辑。
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
CheckMiss的作用是找出来的sel是否为nil,是的话就跳出汇编用C语言的方式找,其实现一会再讲,如果p12和p10相等,即找到的bucket是buckets的首地址,那就跳到3(跳到最后一个bucket继续查找)如果不是,则倒序查找跳到前一个bucket,跳回1继续查找。
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.
能来到3,说明找到的bucket是buckets的第一个,这个时候,跳到最后一个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
接下来执行的1,2跟上面的1,2一样,不一样的是,如果再次碰到第一个bucket,就跳出汇编。
CacheHit的宏如下:
.macro CacheHit
TailCallCachedImp x17, x12 // authenticate and call imp
.endmacro
对于TailCallCachedImp,它定义在arm64-asm.h
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp
brab $0, $1
.endmacro
这个时候,x12存了IMP的地址,x17存了保存的IMP,但是brab是什么命令我没查到,大意应该就是调用了这个缓存的函数。
总结一下CacheLookup这个流程,如果缓存高级语言的写法,那应该就是:
bucket_t bucket = class->cache->buctet[sel]
if (sel == bucket->_key){
bucket]->_imp()
} else{
//执行C语言的逻辑
}
缓存找不到的case
不管是JumpMiss还是CheckMiss的时候sel为空(也就是没有找到缓存的sel)最后都会来到
_class_lookupMethodAndLoadCache3这个C函数这个函数定义在objc-runtime-new.mm,代码如下:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
这个方法返回查找过后IMP指针,供汇编代码调用,这里就不贴出返回之后的逻辑了。而lookUpImpOrForward是一个查找方法的函数
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//因为传入的cache为NO,所以不会执行(汇编已经执行过一遍了)
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
//检查类是否是已知的类,如果是NSClassFromString()方法得到的,那有可能是未知的
checkIsKnownClass(cls);
// 判断类是否已经实现,如果没有先将其实现
if (!cls->isRealized()) {
realizeClass(cls);
}
//检查类是否被初始化,如果没有,则将其初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//尝试在缓存里找
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 在本类的方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 在父类中查找,也是先找缓存,再找方法列表,如果找到,则将该方法缓存到该类中
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
//判断这是不是消息转发的方法
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
//如果是消息转发,先不调用
//先调用resolveInstanceMethod
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 如果没找到实现,则调用+ (BOOL)resolveClassMethod:(SEL)sel
//和+ (BOOL)resolveInstanceMethod:(SEL)sel方法,重新试一次
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
//如果还是找不到,走消息转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
这个方法其实就是在类中查找方法的整套流程实现。这个过程是线程安全的。找到的方法都会调用cache_fill存到缓存里面。一旦方法被缓存起来,下次调用的时候则只需要执行汇编的代码就可以找到方法。大大地提高代码执行的效率。
参考文献
- 逐行剖析objc_msgSend汇编源码
- iOS Tagged Pointer
- Objective-C 中的消息与消息转发