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 1b
cmp 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 4b
cmp 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++方法
时,需要将需要查找的方法去掉一个下划线。