我们知道了cache是用于方法的缓存, 并分析了cache插入sel/imp
的流程.
在消息发送objc_msgSend
流程中, 会先通过cache_getImp()
在cache中查找方法, 找到了就走调用流程, 如果没找到继续走后续查找,转发流程.
我们知道苹果的操作系统是基于Runtime
实现的, 当然objc_msgSend
也是, 下面让我们先了解一下Runtime.
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
This document looks at the NSObject class and how Objective-C programs interact with the runtime system. In particular, it examines the paradigms for dynamically loading new classes at runtime, and forwarding messages to other objects. It also provides information about how you can find information about objects while your program is running.
You should read this document to gain an understanding of how the Objective-C runtime system works and how you can take advantage of it. Typically, though, there should be little reason for you to need to know and understand this material to write a Cocoa application.
网页翻译:
Objective-C语言尽可能多地推迟从编译时间和链接时间到运行时的决定。只要有可能,它就会动态地做事。这意味着该语言不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。运行时系统是Objective-C语言的一种操作系统;它使该语言工作。
本文关注NSObject类以及Objective-C程序如何与运行时系统交互。特别是,它检查了在运行时动态加载新类并将消息转发到其他对象的范式。它还提供了有关如何在程序运行时查找对象信息的信息。
您应该阅读此文档,以了解Objective-C运行时系统如何工作以及如何利用它。然而,通常情况下,编写可可应用程序时,您不需要知道和理解这些材料。
Runtime 介绍
- 运行时是相对于编译时来讲的
- 编译时, 主要是做一些词法语法分析, 好比检查作文中的错别字、语法问题, 也就是编译时类型检查. 编译并不会把代码运行到内存.
- 运行时, 可执行文件被装载到内存中, 代码跑起来了. 运行时过进行运行时类型检查, 与编译时类型检查不一样, 不是简单的扫描代码. 而是在内存中做些操作, 做些判断.
方法的本质
调用类本身的方法
首先我们通过断点查看汇编代码:
@interface LVPerson : NSObject
- (void)personFun;
@end
@implementation LVPerson
- (void)personFun {
NSLog(@"LVPerson -- %s", __func__);
}
@end
@interface LVTeacher : LVPerson
@end
@implementation LVTeacher
- (void)teacherFun {
NSLog(@"LVTeacher -- %s", __func__);
}
@end
// 探索方法调用本质
void testCase1(void) {
LVTeacher *p = [LVTeacher alloc];
[p personFun];
[p teacherFun];
}
我们发现方法调用统一走的call --> objc_msgSend.
objc_demo`testCase1:
0x1000012c0 <+0>: push rbp
0x1000012c1 <+1>: mov rbp, rsp
0x1000012c4 <+4>: sub rsp, 0x10
0x1000012c8 <+8>: mov rax, qword ptr [rip + 0x33b9] ; (void *)0x0000000100004710: LVTeacher
0x1000012cf <+15>: mov rsi, qword ptr [rip + 0x328a] ; "alloc"
0x1000012d6 <+22>: mov rdi, rax
0x1000012d9 <+25>: call qword ptr [rip + 0x2d91] ; (void *)0x00007fff204b4d00: objc_msgSend
0x1000012df <+31>: mov qword ptr [rbp - 0x8], rax
0x1000012e3 <+35>: mov rax, qword ptr [rbp - 0x8]
0x1000012e7 <+39>: mov rsi, qword ptr [rip + 0x327a] ; "personFun"
0x1000012ee <+46>: mov rdi, rax
0x1000012f1 <+49>: call qword ptr [rip + 0x2d79] ; (void *)0x00007fff204b4d00: objc_msgSend
-> 0x1000012f7 <+55>: mov rax, qword ptr [rbp - 0x8]
0x1000012fb <+59>: mov rsi, qword ptr [rip + 0x326e] ; "teacherFun"
0x100001302 <+66>: mov rdi, rax
0x100001305 <+69>: call qword ptr [rip + 0x2d65] ; (void *)0x00007fff204b4d00: objc_msgSend
我们使用clang查看OC对象方法调用底层是怎么做的?
// clang编译:
LVTeacher *p = ((LVTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LVTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("personFun"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("teacherFun"));
发现[p teacherFun] --> objc_msgSend(p, sel_registerName("teacherFun")).
也就是说OC对象方法调用本质就是objc_msgSend实现的.
那么我们是不是也可以直接使用objc_msgSend实现OC对象方法调用呢?
发现我们直接使用objc_msgSend会报错:
Implicitly declaring library function 'objc_msgSend' with type 'id (id, SEL, ...)'
需要进行如下操作来解决这个问题:
target --> Build Setting --> 将Enable strict checking of obc_msgSend calls改为NO
将严厉的检查机制关掉
让我们一起测试一下下面的代码, 可发现objc_msgSend(p, sel_registerName("teacherFun"))效果跟实际应用开发中的方法调用[p teacherFun]一样.
// 测试直接调用 objc_msgSend
// [p teacherFun] vs objc_msgSend(p, sel_registerName("teacherFun"))
void testCase2(void) {
LVTeacher *p = [LVTeacher alloc];
[p personFun];
[p teacherFun];
NSLog(@"-----");
objc_msgSend(p, sel_registerName("personFun"));
objc_msgSend(p, sel_registerName("teacherFun"));
}
打印结果
LVPerson -- -[LVPerson personFun]
LVTeacher -- -[LVTeacher teacherFun]
-----
LVPerson -- -[LVPerson personFun]
LVTeacher -- -[LVTeacher teacherFun]
我们发现[p teacherFun]是调用自己的方法, 这点没错; 但是问题来了[p personFun]不是自己的方法, 而是他爹(superclass继承链)LVPerson的对象方法, 怎么也能正常调用了?
继续研究, 我们又发现objc_msgSendSuper可以实现对象调用其父类方法.
调用父类方法
// 测试: 调用父类方法
void testCase3(void) {
LVTeacher *teacher = [LVTeacher alloc];
[teacher personFun];
struct objc_super lv_super;
lv_super.receiver = teacher; // 消息的接收者还是自己 teacher
lv_super.super_class = [LVPerson class]; // 告诉父类是我爹 LVPerson
objc_msgSendSuper(&lv_super, sel_registerName("personFun"));
}
打印结果:
LVPerson -- -[LVPerson personFun]
LVPerson -- -[LVPerson personFun]
补充 objc_super的结构, 包含接受者receiver, 父类指针super_class.
/// 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 */
};
#endif
总结:
testCase2() 中可以通过[p personFun]-->objc_msgSend(p, sel_registerName("personFun"))调用父类方法.
testCase3() 中也可以通过objc_msgSendSuper(&lv_super, sel_registerName("personFun"))对父类的方法调用.
于是我们猜想调用方法可能是, 自己的这里找不到会从找父类那里找?
带着这个猜想, 我们展开了下面的研究.
objc_msgSend 缓存查找流程 -- arm64
为了提升性能, 使用汇编代码实现.
脱离代码, 如果自己去实现objc_msgSend 缓存查找流程, 大致步骤应该是这样的:
objc_msgSend(receiver, _cmd);
1.通过对象receiver获取isa, 得到class
2.class -> cache
3.cache -> _bucketsAndMaybeMask
4._bucketsAndMaybeMask & bucketsMask -> buckets指针
5._bucketsAndMaybeMask >> 48 -> mask
6._cmd 通过哈希算法得到开始查找下标 first_probed
7.向前遍历查找 [0 <-- first_probed] 区间, 命中则调用imp
8.向前遍历查找 (first_probed <-- mask] 区间, 命中则调用imp
9.否则Miss