运行时理解
通过这张图片我们可以看到,我们平时调用
oc
方法其实本质就是调用 runtime
的 api
,就是发消息。那么我们平常的 oc
方法调用,在底层又是如何实现的呢?
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface CXPerson : NSObject
- (void)sayHello;
@end
@implementation CXPerson
- (void)sayHello {
NSLog(@"CXPerson - sayHello");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
CXPerson *person = [CXPerson alloc];
[person sayHello];
}
return 0;
}
我们先建立一个工程,在 main.m
文件定义一个 CXPerson
类,并调用 sayHello
方法。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
CXPerson *person = ((CXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CXPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
return 0;
}
然后打开终端,cd 到 main.m 所在文件目录,然后输入 clang -rewrite-objc main.m -o main.cpp 命令,最后会看到生成了一个 main.cpp 文件。 然后找到 main
函数的实现代码。可以看到 alloc
跟 sayHello
方法的调用在底层都会转为 objc_msgSend
的调用形式。
通过代码可以看到消息的发送需要两个参数 objc_msgSend(消息接受者, 消息的主题(sel + 参数))
,消息接收者跟消息主题。那么我们是否能直接调用底层的消息发送方法呢?这里我们来试一下。
这里可以看到,我们自己直接调用
objc_msgSend
方法也是可以的。
objc_msgSend
源码探究
这里我们来看一下
objc_msgSend
方法再源码中的实现,全局搜索 objc_msgSend
我们可以看到,不同架构下 objc_msgSend
对应的汇编代码。因为我们真机常用的是 arm64
架构,这里我们就分析一下 arm64
架构下的汇编实现。
-
ENTRY _objc_msgSend
(进入到 objc_msgSend )
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0代表消息接受者的地址,这里判断消息接收者是否存在
cmp p0, #0 // nil check and tagged pointer check
// 如果消息接受者不存在,就会走到这里。这这里也会有判断
#if SUPPORT_TAGGED_POINTERS
// 如果为SUPPORT_TAGGED_POINTERS类型走到这里
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 不为SUPPORT_TAGGED_POINTERS类型走到这里
b.eq LReturnZero
// 如果消息接收者存在就会走到这里
#endif
ldr p13, [x0] // p13 = isa (消息接收者的首地址)
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// 这个通过消息接收者 receviece 获取 class 的原因是因为方法存在 cache 里面,而 cache 是类结构体的属性。cache 中有 cache_getIMP方法。通过 sel 获取 imp。
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
这一步主要是判断消息接受者是否存在,如果存在的话就把 isa
赋值给 p13
,并作为参数带入到 GetClassFromIsa_p16
方法。
-
.macro GetClassFromIsa_p16 src, needs_auth, auth_address
(通过 isa 获取 class)
.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
// and $0, $1, #ISA_MASK $1就是 p13,就是 isa,与上ISA_MASK得到 class,并赋值给 p16
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
这一步主要是通过 isa
获取 class
,因为 cache
是类结构体的一个属性。而 chache_getIMP
存在于 cache
中。
全局搜索可以看到
ExtractISA
的实现,代表 $1
与上 ISA_MASK
并赋值给 $0
。
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// 把 `p16` 移动到 `x15`
mov x15, x16 // stash the original isa
// 开始进入objc_msgSend流程
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
// 如果是 arm64 结果会走到这里
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
// 全局搜索CONFIG_USE_PREOPT_CACHES(#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST#define CONFIG_USE_PREOPT_CACHES 1) 得到CONFIG_USE_PREOPT_CACHES等于 1,所以走到这里
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p11` 与上 `0x0000fffffffffffe` 得到 `buckets` 并赋值给 `p10
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11跟 0 相比较,如果不为零就跳转到到 LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
#endif
// 如果为零会走到这里。p1 右移7 个位置赋值给 x12
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// 如果模拟器会走到这里,p11为 cache,与上0x0000ffffffffffff得到 buckets 并赋值给 p10
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p11右移动 48 位就会得到 mask 的值
// p1(_cmd) & mask -> index -> p12 = index
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
// p11 cache -> p10 = buckets
// p1(_cmd) & mask = index -> p12
// 全局搜索 PTRSHIFT 等于 3 #define PTRSHIFT 3
// (_cmd & mask) << (1+PTRSHIFT) -> (_cmd & mask) << 4
// buckets + 内存平移(1,2,3,4) = b[i]
// p13 当前查找的 bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
// * bucket-- p17, p9同时存入bucket--
// 拿到相应的bucket里面的东西 imp(p17) sel(p9)
// 查到的 sel 跟我们要查的 sel 进行比较,如果一样,就会走到 2 里面,不等于就会走到 3 里面,其实就是循环查找
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
调用 CacheLookup
方法并带入 NORMAL
, _objc_msgSend
, __objc_msgSend_uncached
这几个参数。
-
ldr p11, [x16, #CACHE]
把x16
的地址平移CACHE
大小。 -
#define CACHE (2 * __SIZEOF_POINTER__)
全局搜索#define CACHE
可以得到CACHE
是两个指针的大小为 16个字节。 -
[x16, #CACHE] -> p11 -> cache_t
所以p11
就是等于cache_t
。 CONFIG_USE_PREOPT_CACHES
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
全局搜索 CONFIG_USE_PREOPT_CACHES
,得到 CONFIG_USE_PREOPT_CACHES
等于 1。
//p11` 与上 `0x0000fffffffffffe` 得到 `buckets` 并赋值给 `p10
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11跟 0 相比较,如果不为零就跳转到到 LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
-
LLookupPreopt\Function
-
CacheHit
.macro CacheHit
.if $0 == NORMAL
// 如果 $0 等于 NORMAL就会走到TailCallCachedImp
TailCallCachedImp x17, x10, x1, x16 // authenticate and
TailCallCachedImp
//CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// $0(x17) ^ $3(isa) = 编码(imp)
eor $0, $0, $3
// 跳转到$0(imp),就是执行 imp 方法
br $0
最后总结
objc_msgSend(recevier, _cmd) sel -> imp
- 判断
recevier
是否存在 - 通过
recevier
找到isa
找到class
(p16) -
class
内存平移得到cache
(bucket mask) - bucket掩码 -> bucket
- mask掩码 -> mask
- insert哈希函数(mask_t)(value & mask);
- 第一次查找 index
- bucket + index -> 整个缓存里的第几个 bucket
- bucket{imp, sel}
- sel 与我们查找的 _cmd比较,如果相等 -> cacheHit(缓存命中) -> imp ^ class(isa) = imp -> (br)调用 imp
- 如果不相等 bucket-- ->再次平移 -> 找到 bucket 再次比较
- 死循环 遍历查找
- 一直找不到就会走的
__objc_msgSend_uncached
(执行CacheLookup方法带入的第三个参数)
未完待续。。。
网友评论