方法的本质,就是消息传递…
本页所使用的objc runtime 756.2,来自GITHUB
一、引子:Runtime
概念
我们都知道,在运行OC代码时,类或者对象在调用方法时会用到runtime,那么,到底什么是运行时呢?
寻找一些资料后可以给出概念:
In computer science, runtime, run time or execution time is the time when the CPU is executing the machine code.
在计算机科学里,runtime,run time 或execution time 是指CPU 执行机器语言的期间。
—— 维基百科
Runtime 是一套由C、C++、汇编混合写成的为OC提供运行时功能的api。
先看苹果开发者文档里对runtime 的介绍的介绍:
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at
/usr/lib/libobjc.A.dylib
.OC runtime 是一个给OC语言动态属性提供支持的运行时库,这些属性链接到所有的OC应用 。OC runtime库支持在shared library里实现的函数,这些函数库名为
/usr/lib/libobjc.A.dylib
版本
legacy:经典版
modern:现代版,即objc2.0,我们目前用到的版本。
二、方法的本质
概念
方法的本质,就是objc_msgSend 的消息传递。先看苹果开发者文档里对objc_msgSend的介绍:
Function
objc_msgSend
Sends a message with a simple return value to an instance of a class.
发送一个有简单返回值的消息给类的实例
相关源码如下:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) |
其中两个关键参数self
和 op
的解释如下:
* @param self A pointer to the instance of the class that is to receive the message.
self 一个指向由类生产的实例的指针,用来接收消息
* @param op The selector of the method that handles the message.
op 方法: 处理消息的方法的选择器
可见,objc_msgSend 的核心信息,就是向对象主体(self)传递相应的方法/消息(op)。
但是消息传递的机制到底怎样,还是用源码来解释。
源码分析
开始生成一个main.m 内代码如下
int main(int argc, const char * argv[]) { |
其中LGPerson 的内部实现如下:
@interface LGPerson : NSObject |
在这里,着重查看LGPerson alloc
方法,以及其实例 person sayHello` 在汇编里的实现 :
进入到目录下,输入编译代码:
clang -rewrite-objc main.m |
得到main.cpp 的文件
打开main.cpp 结构如下:
|
上文代码中,可以简化为,
Runtime 语法 | OC 语法 |
---|---|
(LGPerson ()(id, SEL))(void *) | – |
(id)objc_getClass(“LGPerson”) | [LGPerson class] |
sel_registerName(“alloc”) | @selector(alloc) |
即代码为:
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")); |
解释为: 向LGPerson
的类,发送了alloc
方法
扩展代码实践
开始设定一个类Student
|
OC语法
|
NSObject 写法
objc_msgSend(student, NSSelectorFromString(@"sayCode")); |
sel_registerName 的函数API
objc_msgSend(student, sel_registerName("sayCode")); |
三、底层分析
汇编源码
我们现在分析,当类对象发送消息是,底层发生了什么。
1、方法入口
新建工程,输入如下代码,进行断点检测;
另外在Xcode 的Debug–Debug Workflow—Always show Disassembly
很明显,这里的Student sayCode 在汇编里,执行了 objc_msgSend 方法,继续查看
打开Xcode, 搜索objc_msgSend,找到相关结果如下:
由于研究的环境是移动平台,选择arm64,通过ENTRY _objc_msgSend
结果进入
首先看到的代码如下:
ENTRY _objc_msgSend |
2、类Tagged Pointer 检查
代码解析:
序号 | 代码 | 解释 |
---|---|---|
1 | cmp p0 #0 | cmp = compare 检测0位寄存器 = 空?,以及tagged point 检测 |
2 | b.le LNilOrTagged | // 即1代码成立,跳转至LNilOrTagged的宏(下文叙述) |
3 | b.eq LReturnZero | // b.eq 即不成立,结果为空,返回并跳出 |
这一小节,主要是用来判断 tagged pointer 是否存在,存在则继续进行,否则跳出。
Tagged point是苹果推出的针对64位机器的特定的指针,概念如下:
苹果对于Tagged Pointer特点的介绍:
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
- 在内存读取上有着3倍的效率,创建时比以前快106倍。
3、加载isa
这一部分主要是通过加载的isa,获取当前底层的类的实现
// person - isa - 类 |
序号 | 代码 | 解释 |
---|---|---|
1 | ldr p13, [x0] | 将13位存储器isa 字,加载到0位寄存器 LDR = LoaD woRd |
2 | GetClassFromIsa_p16 p13 | 通过加载的isa,宏逻辑获取到当前的类 GetClassFromIsa_p16 是一个汇编宏 |
GetClassFromIsa_p16 的汇编实现如下:
.macro GetClassFromIsa_p16 /* src */ |
分析流程图如下:
4、用isa查询方法缓存
当在3步,isa 拿到之后,现在要做的事情,就是对当前要执行的方法进行缓存查找。
LGetIsaDone: |
CacheLookup 在这里的做法是查询类里是否含有方法到缓存。 一般有两种结果:拿到缓存IMP,或者未曾缓存。
查询可以得到有三种查询方式:
- NORMAL 正常查找
- GETIMP 获取IMP
- LOOKUP 慢速查询方法
根据源码,做了一些注释:
其中多次出现CheckMiss ,也是个汇编宏,使用在缓存查找失败后。
源码如下:
.macro CheckMiss |
根据查找的模式位NORMAL, 对应的**__objc_msgSend_uncached **。
在源码中搜索,得到相关逻辑如下:
STATIC_ENTRY __objc_msgSend_uncached |
执行方法查找的核心方法,就是MethodTableLookup, 继续点开查看,得到的是
这里的内容,则是到objc_class 的 class_data_bits_t 里寻找方法的具体实现了,下一篇文章我们来讲。
四、小结
这一篇,主要是开始从汇编的角度,来实现方法查找流程,流程草写了一下,图一定补。。。
- 拿到isa
- 查找Class
- 在Cache_t 查找bucket
- bucket 相同,返回IMP
- 否则 跳到BITS
- BITS 中
- 查找Rw
- 查找ro
- 查找methodList
- 查找ro
- 查找Rw
总流程如下: