通过上一篇文章的分析,我们对cache_t的底层部分流程有一定的了解,我们是通过insert插入操作开始对缓存进行分析的,那是什么时候开始调用insert的呢?
insert()的闭环流程
- 我们在
insert()方法中下断点,查看调用堆栈信息
- 栈是一种
LIFO的数据结构,index = 0是最后调用的,从0开始依次查看调用信息,发现log_and_fill_cache方法调用了cache.insert方法;我们看一下log_and_fill_cache的实现:
- 调用栈显示,是
lookUpImpOrForward调用了log_and_fill_cache: - 查看
objc-cache.mm文件,可以根据注释看到insert()的插入时机最上层是通过objc_msgSend触发的,接下来我们就一起看下objc_msgSend;
runtime的运行时理解
什么是runtime
想要理解消息流程,那么必须先了解runtime的相关知识
编译时
- 编译时 顾名思义就是正在编译的时候. 编译器把源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析、词法分析等编译时类型检查(静态类型检查)来发现代码中的
errors或warning等编译时的错误信息. - 静态检查不会把代码放内存中运⾏起来,⽽只是当作⽂本来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法;
运行时
- 运⾏时就是代码通过dyld被装载到内存中执行的过程;运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做操作和判断.
Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)
runtime有两个版本:
Legacy版本为早期版本,对应Objective-C 1.0,用于32位的MacOS X系统中Modern为现行版本,对应Objective-C 2.0,用于iPhone和MacOS X v10.5之后的64位系统中
关于OC语言的更多信息可以在苹果官方文档进行查阅
Runtime调用方式一共有3种
OC方法:[p sayNB];NSObject提供的API:isKindofClass...;objc的下层APIobjc_msg_send方法:class_getInstanceSize...;
Runtime3种调用方式对应的发起者信息如下
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface XJPerson : NSObject
- (void)sayNB;
@end
@implementation XJPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson * tp = [XJPerson 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。
XJPerson * tp = [XJPerson alloc];
[tp sayNB];
objc_msgSend(tp, sel_registerName("sayNB"));
程序正常运行!说明我们可以直接调用objc_msgSend()。接下来我们做一个尝试,XJPerson 继承 XJTeacher。
@interface XJTeacher : NSObject
- (void)sayHello;
@end
@implementation XJTeacher
- (void)sayHello{
NSLog(@"666 %s",__func__);
}
@end
@interface XJPerson : XJTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation XJPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson * tp = [XJPerson alloc];
XJTeacher * tt = [XJTeacher 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 xj_objc_super;
xj_objc_super.receiver = tp;
xj_objc_super.super_class = XJTeacher.class;
objc_msgSendSuper(&xj_objc_super, @selector(sayHello));
// 输出:
666
666 -[XJTeacher sayHello]
666 -[XJTeacher sayHello]
objc_msgSend 分析
由于OC语言的动态特性,基本上所有方法都是通过objc_msgSend进行处理的,这样一个高频调用的底层方法,从效率层面考虑最好是使用接近机器语言的汇编语言来实现,同时汇编语言基于地址也更加安全,所以系统也是用汇编进行实现的;下面一起来看下objc_msgSend在在libobjc库中的汇编层面的实现吧
- 在
objc源码中搜索objc_msgSend - 关闭所有文件,查看后缀为
.s的汇编文件 - 接下来我们就看
objc-msg-arm64.s文件,定位到ENTRY _objc_msgSend(进入到 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做了如下几件事:
- 通过
isa找到class; - 在
CacheLookup中通过isa平移找到cache即找到_bucketsAndMaybeMask的地址。 - 读取
buckets地址,即缓存的首地址。 - 获取
哈希的下标index。
整体流程:isa -> cache(_bucketsAndMaybeMask) -> buckets -> 哈希下标 index
总结
首先我们了解了Runtime编译时和运行时的区别,然后我们知道了 Runtime 的三种调用方式,最后我们通过对objc_msgSend进行分析,知道了objc_msgSend的调用会根据不同的架构做不同的处理,以及在真机环境下整体调用流程:isa -> cache(_bucketsAndMaybeMask) -> buckets -> 哈希下标 index。