前言
上一篇文章我们对 cache_t
进行了分析。了解了 cache_t
的结构、以及方法在底层的调用流程。这篇文章我们就一起来分析一些 Runtime
API objc_msgSend
。
Runtime
运行时分析
1. Runtime
概述
编译时 顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。(当然这只是一意义上这么说,实际上可能只是翻译成某个中间状态的语音。)
运行时 顾名思义就是在代码运行起来后,被装载到内存中。(当你的代码保存在磁盘中,没有写入内存之前都是“死”代码,只有写入内存中才变成“活”代码。而且运行时类型检查与编译时类型检查(或者静态类型检查)是不一样的,不是简单的扫描代码,而是在内存中做了一些操作和判断。
Runtime
有两个版本:一个 Legacy
版本(早期版本);一个 Modern
版本(现行版本)。
- 早期版本对应的编程接口:Objective-C 1.0
- 现行版本对应的编程接口:Objective-C 2.0
- 早期版本用于 Objective-C 1.0,32 位的 Mac OS X 的平台上
- 现行版本用于 iPhone 程序和 Mac OS v10.5 及以后系统中的64 位程序。
Objective-C Runtime Programming Guide & 苹果官方文档
2. Runtime
调用的三种方式
-
- Objective-C 方法调用;
-
- NSObject提供的API;
-
- Runtime 底层API,objc_msg_send方法;
Runtime
三种调用方式对应的发起者信息图
接下来我们一起通过代码查看 Runtime
调用的三种方式
添加如下代码:
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface TPerson : NSObject
- (void)sayNB;
@end
@implementation TPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TPerson * tp = [TPerson alloc];
[tp sayNB];
[tp sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
通过 Clang
生成 .cpp
文件。在 .cpp
文件中找到 main
函数,查看它底层代码。
- 发现在底层代码中,编译之后上层的代码都会得到有个解释。
- 是一个调用方法的过程(消息的发送),objc_msgSend(消息的接收者,消息的主体(SEL+主体))
那么我们是否也可以直接调用底层的消息发送方法呢?接下来我们就试一下。注意:我们调用的时候可能会编译失败,需要将Build Settings
-> Enable Strict Checking of objc_msgSend Calls
设置成为 NO
。
TPerson * tp = [TPerson alloc];
[tp sayNB];
objc_msgSend(tp, sel_registerName("sayNB"));
复制代码
程序正常运行!说明我们可以直接调用objc_msgSend()
。接下来我们做一个尝试,TPerson
继承 TTeacher
。
@interface TTeacher : NSObject
- (void)sayHello;
@end
@implementation TTeacher
- (void)sayHello{
NSLog(@"666 %s",__func__);
}
@end
@interface TPerson : TTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation TPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TPerson * tp = [TPerson alloc];
TTeacher * tt = [TTeacher alloc];
[tp sayNB];
// objc_msgSend(tp, @selector(sayNB));
[tp sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
运行代码,生成.cpp
文件,看看 Runtime
在运行时是怎么处理的。注意:在生成.cpp
文件时,需要注释objc_msgSend()
代码,否则会生成失败~
我们在查看objc_msgSend()
的时候,发现有一个objc_msgSendSuper()
。难道是子类没有,可以直接给父类发消息?接下来我们验证一下。
首页我们先来看看 objc_msgSendSuper()
的声明。
发现 objc_msgSendSuper()
声明需要的参数 struct objc_super * _Nonnull super
和 SEL _Nonnull op
,还有一些相应参数。SEL
我们知道,objc_super
是什么?我们不知道。接下来我们就一起看看 objc_super
是什么。全局搜索 objc_super
。
可以看出 objc_super
里面有两个参数:receiver
和 super_class
。接下来我们就调用一下 objc_msgSendSuper
struct objc_super tcd_objc_super;
tcd_objc_super.receiver = tp;
tcd_objc_super.super_class = TTeacher.class;
objc_msgSendSuper(&tcd_objc_super, @selector(sayHello));
// 输出:
2021-06-27 21:26:56.619214+0800 001-运行时感受[12241:1255935] 666
2021-06-27 21:26:56.619880+0800 001-运行时感受[12241:1255935] 666 -[TTeacher sayHello]
2021-06-27 21:26:56.619961+0800 001-运行时感受[12241:1255935] 666 -[TTeacher sayHello]
复制代码
objc_msgSend
分析
我们打开源码,搜索全局 objc_msgSend
。
这么多文件我们怎么看呢?怎么知道那个是汇编代码呢?接着我们一波骚操作快速定位。
汇编文件的后缀是 .s
,我们就找.s
文件查看。
接下来我们就看 objc-msg-arm64.s
文件,定位到 ENTRY _objc_msgSend
(进入到 objc_msgSend)。
接下来我们就一行一行的进行分析。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0代表消息接受者的地址,这里判断消息接收者是否存在
cmp p0, #0 // nil check and tagged pointer check
// 判断是否支持Taggedpointer类型
#if SUPPORT_TAGGED_POINTERS
// 如果支持Taggedpointer类型按照LNilOrTagged处理
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 如果不支持Taggedpointer类型返回LReturnZero
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa [x0]存放的是class
// 从 GetClassFromIsa_p16 中获取clsss:p16 = class
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// receiver->class 获取class,去class 中找method cache
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
中。接下来我们就看看 GetClassFromIsa_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
// 把当前的地址存入 p16(isa 存入 p16)
mov p16, \src
.else
// 64-bit packed isa
// $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
复制代码
接下来我们看看CacheLookup
的实现。
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 // stash the original isa // 将x16的值,赋值给x15
LLookupStart\Function:
// p1 = SEL, p16 = isa // => isa -> 类的 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
// x16(isa) 平移 16 就得到了 cache的地址(即为_bucketsAndMaybeMask的地址)
// 将 _bucketsAndMaybeMask的地址存入 p11 寄存器中。
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 = _bucketsAndMaybeMask & #0x0000fffffffffffe
// 如果_bucketsAndMaybeMask第 0 位不等于 0,就跳转到 LLookupPreopt\Function
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
// 通过哈希求 index 下标
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
复制代码
CACHE
的定义
#define CACHE (2 * __SIZEOF_POINTER__) // => CACHE = 16
复制代码
通过对objc_msgSend
底层源码分析。我们知道了objc_msgSend
的调用会根据不同的架构做不同的处理。今天我们分析了arm64
真机环境下,objc_msgSend
做了如下几件事:1、通过isa
找到class
;2、在CacheLookup
中通过isa
平移找到cache
即找到_bucketsAndMaybeMask
的地址。3、读取buckets
地址,即缓存的首地址。4、获取哈希
的下标index
。
整体流程:isa
-> cache
(_bucketsAndMaybeMask
) -> buckets
-> 哈希下标 index
总结
首先我们了解了Runtime
编译时和运行时的区别,然后我们知道了 Runtime
的三种调用方式,最后我们通过对objc_msgSend
进行分析,知道了objc_msgSend
的调用会根据不同的架构做不同的处理,以及在真机环境下整体调用流程:isa
-> cache
(_bucketsAndMaybeMask
) -> buckets
-> 哈希下标 index
。
Tips: 这篇文章整理的有些冲忙,如有遗漏的地方后续继续补充。