1.运行时和编译时
-
编译时:顾名思义就是正在编译的时候,编译时简单的作⼀些翻译⼯作,⽐如检查关键字是否错误,词法分析,语法分析之类的过程。 -
运行时:就是代码跑起来了,被装载到内存中去了。如果出现错误会导致程序崩溃。
2.Runtime调用的三种途径
Runtime调用的三种途径下图:
Objective-C Code代码中,比如对象方法的调用,[user sayHello];;Framework&Service中,接口的调用,比如isKindOfClass、isMemberOfClass等;Runtime API中,c/c++源码方法的使用,比如objc_msgSend,objc_msgSendSuper。
上图中:
Compiler为编译器层,会将代码翻译成某个中间状态的语⾔,同时会做一些LLVM编译器的优化,比如将alloc方法优化执行objc_alloc方法。runtime system libarary就是底层库。
3.方法调用的本质
1.探索本质
使用clang把oc代码编译成c/c++代码,查看本质。
((void (*)(id, SEL))(void *)objc_msgSend)((id)user, sel_registerName("sayHello"));
在Objective-C中,user对象的sayHello方法的调用,最终是通过c/c++的objc_msgSend来实现。objc_msgSend很眼熟,因为我们在上一篇章,cache_t结构分析与底层探索中,发现过他的踪迹!
回顾一下我们探索cache_t的思路和流程:
思路:决定一个类功能的是函数,所以从cache_t的函数中去寻找突破口。
-
cache_t结构体中,有一个方法void incrementOccupied();,增加占用,内部实现为:_occupied++;,很容易理解:向cache_t中插入内容,占用数加1; -
全局搜索
incrementOccupied()方法,只有一个地方用到了该方法,向cache_t中插入数据,cache_t::insert方法。 -
继续全局搜索
cache_t::insert方法找到了一段非常重要的注释,解读注释:cache_t分为cache读取和cache写入两个点。见下图:这里给了我们很大的启发,结合上面的分析,方法的调用最终都是转为了消息发送,也就是objc_msgSend,此过程中应该会通过cache_getImp方法从缓存cache_t中获取方法实现,从而完成方法的一个查找流程,最终完成函数的执行。
2.OC和c/c++源码转换
将OC代码做个转换,使用objc/message.h c/c++源码来实现方法调用。例如:
GFPerson * user = [GFPerson alloc];
[user sayHello];
objc_msgSend(user, sel_registerName(@"sayHello"));
运行发现调用结果是一致的。需要注意的是需要将objc_msgSend严厉的检查机制关掉。见下图:
3.objc_msgSendSuper
引入一个案例
Son继承自Father,Son只进行了sayHello方法的声明,并没有实现sayHello方法,father实现了sayHello方法。在main中,Son调用了方法sayHello。参考下面的示例:
@interface Father : NSObject
-(void)sayHello;
@end
@implementation Father
-(void)sayHello{
NSLog(@"sayHello %s", __func__);
}
@end
@interface Son : Father
-(void)sayHello;
@end
@implementation Son
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Son *son = [[Son alloc]init];
[son sayHello];
}
return 0;
}
打印结果:
虽然Son没有实现sayHello方法,但程序运行后并没有异常,而是调用了父类的sayHello方法。说明消息的接受者虽然是自己,自己没有实现,调用到了父类的方法。我们可以使用objc_msgSendSuper直接调用父类的方法。
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
该方法的两个参数:
- 参数1:结构体
objc_super指针; - 参数2:方法编号
sel。
objc_super结构体如下:
/// 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 */
};
最终实现代码:
Son *son = [[Son alloc]init];
[son sayHello];
struct objc_super obj;
obj.receiver = son;
obj.super_class = [father class];
objc_msgSendSuper(&obj, sel_registerName("sayHello"));
打印结果为:
同样可以调用成功。
4.快速方法查找
objc_msgSend的快速方法查找流程是通过汇编实现的,使用汇编的原因是速度快。在libobjc.A.dylib全局搜索_obj_msgSend,汇编核心实现在objc_msg_arm64.s文件中。
下面是_objc_msgSend汇编的核心代码
1._objc_msgSend函数
ENTRY _objc_msgSend // 进入_objc_msgSend方法
UNWIND _objc_msgSend, NoFrame
// p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
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
ldr p13, [x0] // p13 = isa 拿出ISA
GetClassFromIsa_p16 p13, 1, x0 // p16 = class 获取对应的类
LGetIsaDone:
// 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
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
流程简述:
-
首先对方法接受者
p0,判断是否为空,如果为空直接返回; -
如果不为空,获取
isa指针,放到p13中,也即是对象的首地址; -
GetClassFromIsa_p16是定义的一个宏,在实现中,通过isa找到对应的类;ExtractISA也是个宏定义,将传入的isa&isaMask,得到class,并将class赋给p16;.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */ #if SUPPORT_INDEXED_ISA // Indexed isa mov p16, \src // 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__ .if \needs_auth == 0 // _cache_getImp takes an authed class already mov p16, \src .else // 64-bit packed isa ExtractISA p16, \src, \auth_address .endif #else // 32-bit raw isa mov p16, \src #endif .endmacro // 宏定义ExtractISA .macro ExtractISA and $0, $1, #ISA_MASK .endmacro -
类找到后,进入
宏CacheLookUp缓存查找流程。
2.宏CacheLookUp
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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 p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 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
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
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
……省略
.endmacro
流程简述,只分析CACHE_MASK_STORAGE_HIGH_16环境:
-
获取类对象地址后,进行指针平移
16个字节,得到cache_t的首地址;因为objc_class中,isa占8个字节,superclass占8个字节;平移16个字节,即可获取cache_t首地址,赋值给p11,也就是_bucketsAndMaybeMask;源码如下:#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 ldr p11, [x16, #CACHE] // p11 = mask|buckets -
在arm64环境下,
mask和buckets放在一起共占用8个字节,64位;其中mask在高16位,buckets在低48位。通过掩码运算�x0000fffffffffffe将高16位抹零获取buckets;将buckets赋值给p10。cache_t结构分析与底层探索#if CONFIG_USE_PREOPT_CACHES #if __has_feature(ptrauth_calls) tbnz p11, #0, LLookupPreopt\Function and p10, p11, #0x0000ffffffffffff // p10 = buckets #else // 走该流程获取buckets and p10, p11, #0x0000fffffffffffe // p10 = buckets tbnz p11, #0, LLookupPreopt\Function #endif // 此部分就位cache_hash算法 eor p12, p1, p1, LSR #7 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask #else and p10, p11, #0x0000ffffffffffff // p10 = buckets and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHES -
在缓存插入的时候,是以
hash下标的形式储存的,而下标的算法是:static inline mask_t cache_hash(SEL sel, mask_t mask) { uintptr_t value = (uintptr_t)sel; #if CONFIG_USE_PREOPT_CACHES value ^= value >> 7; #endif return (mask_t)(value & mask); }- 汇编实现采用同样的算法,现将
_cmd右移7位,并异或运算eor,赋值给p12; p11右移48位获取mask,然后p12和mask通过and运算获取下标,再赋值给p12,这样就获得了_cmd的下标!
至此:
p11等于_bucketsAndMaybeMask;p10等于buckets,也就是首个bucket_t地址;p12等于 要查找的方法的hash下标。
- 汇编实现采用同样的算法,现将
-
hash下标找到了,buckets首个元素的地址也找到了,那么怎么找到_cmd的位置呢,没错地址平移,平移多少单位呢?16个字节的倍数,因为bucket_t中的两个属性是imp和sel两个指针地址,8+8=16个字节。add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))根据
buckets首地址偏移下标index * 16个单位,p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),其中PTRSHIFT = 3,(_cmd & mask) << (1+PTRSHIFT)相当于下标左移4,即乘以16,相对于16的倍数进行平移。所以最后加上buckets首地址的话,就获得了当前_cmd对应的bucket地址。 -
开启一个循环,
[x13]取寄存器x13里的值,p17指向一个地址,这个指令是向这个地址中赋值,而ldp是一个出栈指令,出栈后地址自动平移。进行sel比对,将获取的当前_cmd的bucket,也就是p13赋值给p17和p9,p17=imp,p9=sel。// 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 1bcmp p9, p1,如果当前获取的sel与要查找的sel相同,则缓存命中,CacheHit;- 如果不相等,则进入
3流程中,判断当前获取的sel,p9是否为空,如果为空,则Miss,缓存没有命中; - 如果获取的
sel不为空,说明存在下标冲突;则以当前获取的bucket的地址与首个bucket的地址进行比较; - 如果获取地址,大于等于首地址,继续比较流程,向前查找,循环下去!
- 直到查询到首地址位置。
-
如果上面的循环结束依然没有找到,则会进入下面的流程,
CACHE_MASK_STORAGE_HIGH_16环境下,同样p11右移48位获取mask,而mask等于开辟的总空间容量减1,所以获取最后一个存储空间所在的位置,也即是首地址的基础上,添加mask*16的位置,所以这里p13就是当前最大的那个存储空间,也就是最后一个存储空间。#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS add p13, p10, w11, UXTW #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p13 = buckets + (mask << 1+PTRSHIFT) // see comment about maskZeroBits #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 add p13, p10, p11, LSL #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif -
重新设定
p12的值,上面已经知道p12是要查找方法_cmd的存储下标,首地址添加index*16,即可获取当前要查找的方法_cmd对应bucket地址,并赋值给p12。add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket -
再开启一个循环,此次循环是从最后一个位置,向要查找的
_cmd对应位置,进行向前查找。// 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 4bcmp p9, p1,如果当前获取的sel与要查找的sel相同,跳转至流程2,即缓存命中,CacheHit;- 如果不相等,判断
sel是否为空,如果不为空,并且循环获取的地址大于p12的位置,继续循环流程。
-
如果以上流程均未能命中缓存,则进入
MissLabelDynamic流程,未能命中缓存。
3.缓存命中CacheHit
CacheLookup中,Mode传入的为NORMAL,会执行TailCallCachedImp。见下面源码:
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
// 调用imp
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0
.endmacro
在TailCallCachedImp实现中进行了位异或运算,获取imp。因为在存储imp时,对imp进行了编码处理,见下图:
取出执行调用时,需要进行解码操作。
4.静态函数__objc_msgSend_uncached
如果缓存没有命中,则会进入MissLabelDynamic流程。全局搜索MissLabelDynamic,发现MissLabelDynamic即为CacheLookUp的第三个参数:
也就是_objc_msgSend中传入的__objc_msgSend_uncached。见下图:
全局搜索__objc_msgSend_uncached,获得以下定义:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
流程简述:
在该函数中执行宏MethodTableLookup,继续跟踪MethodTableLookup。在MethodTableLookup的汇编实现中,我们可以看到最重要的是_lookUpImpOrForward的方法,然后全局搜索_lookUpImpOrForward发现搜不到实现方法, 说明该方法并不是汇编实现的,需要去C/C++源码中查找。
5.知识点补充
c/c++中调动汇编,去查找汇编时, 需要将需要搜索的方法多加一个下划线。
汇编中调用c/c++方法,去查找c/c++方法时,需要将需要查找的方法去掉一个下划线。