方法的本质,就是消息传递…
本页所使用的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[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayHello];
// Setup code that might create autoreleased objects goes here.
}
return NSApplicationMain(argc, argv);
}
其中LGPerson 的内部实现如下:
@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end
在这里,着重查看LGPerson alloc
方法,以及其实例 person sayHello` 在汇编里的实现 :
进入到目录下,输入编译代码:
clang -rewrite-objc main.m
得到main.cpp 的文件
打开main.cpp 结构如下:
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
return NSApplicationMain(argc, argv);
}
上文代码中,可以简化为,
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 *person = objc_msgSend([LGPerson class], @selector(alloc));
解释为: 向LGPerson
的类,发送了alloc
方法
扩展代码实践
开始设定一个类Student
#import "Student.h"
interface Student : Person
- (void)sayCode;
@end
OC语法
#import "Student.h"
Student *student = [Student new];
[student sayCode];
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
UNWIND _objc_msgSend, NoFrame
1 cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
2 b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
3 b.eq LReturnZero
#endif
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 - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
序号 | 代码 | 解释 |
---|---|---|
1 | ldr p13, [x0] | 将13位存储器isa 字,加载到0位寄存器 LDR = LoaD woRd |
2 | GetClassFromIsa_p16 p13 | 通过加载的isa,宏逻辑获取到当前的类 GetClassFromIsa_p16 是一个汇编宏 |
GetClassFromIsa_p16 的汇编实现如下:
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
分析流程图如下:
4、用isa查询方法缓存
当在3步,isa 拿到之后,现在要做的事情,就是对当前要执行的方法进行缓存查找。
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
CacheLookup 在这里的做法是查询类里是否含有方法到缓存。 一般有两种结果:拿到缓存IMP,或者未曾缓存。
查询可以得到有三种查询方式:
- NORMAL 正常查找
- GETIMP 获取IMP
- LOOKUP 慢速查询方法
根据源码,做了一些注释:
其中多次出现CheckMiss ,也是个汇编宏,使用在缓存查找失败后。
源码如下:
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
根据查找的模式位NORMAL, 对应的*__objc_msgSend_uncached *。
在源码中搜索,得到相关逻辑如下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_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
总流程如下: