前言
OC 类原理探索:cache 结构分析补充 中我们对cache
结构进行了补充,也引入到了objc_msgSend
,今天主要任务是探索objc_msgSend
的汇编源码。
准备工作
一、runtime 的运行时理解
Runtime 简介
Runtime
是一个用C
、C++
、汇编
编写的运行时库,包含了很多C
语言的API
,封装了很多动态性相关的函数。Objective-C
是一门动态运行时语言,允许很多操作推迟到程序运行时再进行。OC
的动态性就是由Runtime
来支撑和实现的,Rumtime
就是它的核心。- 我们平时编写的
OC
代码,底层都是转换成了Runtime API
进行调用。
编译时
编译时
顾名思义就是正在编译的时候。
那啥叫编译
呢?就是编译器帮你把源代码翻译成机器能识别的代码。
(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。)
那编译时
就是简单的作一些翻译工作,比如检查老兄你有没有粗心写错啥关键字了啊。有啥词法分析,语法分析之类的过程。就像个老师检查学生的作文中有没有错别字和病句一样,如果发现啥错误编译器就告诉你。
如果你用微软的VS
的话,点下build
。那就开始编译,如果下面有errors
或者warning
信息,那都是编译器检查出来的。所谓这时的错误就叫编译时错误
,这个过程中所做的类型检查也就叫编译时类型检查,或静态类型检查(所谓静态嘛就是没真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。
所以有时一些人说编译时还分配内存啥的肯定是错误的说法。
运行时
运行时
就是代码跑起来了,被装载到内存中去了。(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。
而运行时类型检查
就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。
Runtime 版本
Runtime
有两个版本,一个Legacy
版本(早期版本) ,一个Modern
版本(现行版本)。
- 早期版本对应的编程接口:
Objective-C 1.0
; - 现行版本对应的编程接口:
Objective-C 2.0
; - 早期版本用于
Objective-C 1.0
,32
位的Mac OS X
的平台上; - 现行版本:
iPhone
程序和Mac OS X v10.5
及以后的系统中的64
位程序。
可以在官方文档 Objective-C Runtime Programming Guide 中找到相关定义。
Runtime 的发起方式
Runtime
的层级结构:
Runtime
的三种发起方式OC 方法
、NSObject 接口
、objc api
:
方法调用的本质 objc_msgSend
创建SSPerson
类,say1:
方法有实现,say2
方法没有实现:
实例化SSPerson
,调用say1:
方法、say2
方法,command + B
进行编译,编译成功:
command + R
运行项目,项目报错:
这就是编译时
和运行时
的区别,接下来clang -rewrite-objc main.m -o main.cpp
编译main.m
得到main.cpp
文件。
在文件中查看main
函数:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
SSLPerson *person = objc_msgSend((id)objc_getClass("SSLPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("say1:"), (NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_d1fdf2_mi_1);
((id)person, sel_registerName("say2"));
}
return 0;
}
- 这里的方法调用,采用的是
Runtime API
调用方式; - 可以看到,不管是
类方法
还是对象方法
都是用到了objc_msgSend
,方法调用的本质是消息的发送
; - 调用方法 =
objc_msgSend(消息的接受者,消息的主体(sel + 参数))
;
我们添加一个say3
方法,然后用objc_msgSend
进行调用:
可以正常调用,注意下面的设置要改为NO
,正常是YES
的。
objc_msgSendSuper
找到objc_msgSendSuper
的定义:
我们看到有函数中有objc_super
参数,我们去源码中看下它的定义:
objc_super
中有receiver
和super_class
两个成员变量;super_class
是第一查找对象,如果没有方法实现的话会继续向上寻找,直到NSObject
类。
我们来重新定义下类,SSLPerson
继承SSLAnimal
,SSLAnimal
去实现say2
方法:
把super_class
赋值为SSLAnimal.class
,执行代码可以正常打印:
接下来把super_class
赋值为SSLPerson.class
,执行代码:
同样可以正常打印,这也证明了我们上面的说法,方法寻找是会向上寻找的。
二、objc_msgSend 流程
objc_msgSend 源码查找
打断点到方法调用处,用汇编跟源码的方式
找到objc_msgSend
所在源码库objc
:
源码中搜索objc_msgSend
,调用的地方会非常非常的多,很难查看:
按住command
+ 点击下拉箭头,收缩文件:
objc_msgSend
的源码在汇编中,汇编文件是以.s
结尾的;- 我们来看
objc-msg-arm64.s
文件,因为arm64
是真机架构,i386
是模拟器架构,x86_64
是Mac OS
架构。
我们接下来通过汇编来探索objc_msgSend
的流程,ENTRY
是汇编程序的入口点,我们找到它:
汇编源码 解析
1. 汇编源码 消息接受者判空
汇编源码:
- 源码解析
- 判断消息接受者
person
是否为空,如果为空; - 判断是否为
tagged pointer
,如果是的话进行相关处理; - 如果不是
tagged pointer
,置空,结束方法调用; - 如果消息接受者
person
不为空,向下继续执行。
- 判断消息接受者
2. 汇编源码 获取isa
- 源码解析
- 将
person
的isa
赋值给p13
; p13
以参数形式,传进GetClassFromIsa_p16
;- 通过
ExtractISA
,isa & ISA_MASK
赋值给p16
,也就得到了class
; - 得到
class
,是为了获取其成员变量cache
,进行方法的查找; GetIsaDone
:获取isa
结束。
- 将
3. 汇编源码 CacheLookup
CacheLookup
宏定义函数,Mode = NORMAL
,Function = _objc_msgSend
,MissLabelDynamic = __objc_msgSend_uncached
;
4. 汇编源码 获取hash index
汇编源码:
mov x15, x16
- 保存原始
isa
值,p16 = class
,x15 = x16 = isa
。
- 保存原始
CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
真机
架构,也是我们此流程分析的架构。
ldr p11, [x16, #CACHE]
#define CACHE (2 * __SIZEOF_POINTER__)
CACHE
=2 * __SIZEOF_POINTER__
=16
p11
=isa 平移 16
=cache_t
CONFIG_USE_PREOPT_CACHES
- 真机环境值为
1
- 真机环境值为
__has_feature(ptrauth_calls)
- 判断是否为
A12
以后机型
- 判断是否为
tbnz p11, #0, LLookupPreopt\Function
cache_t
的0
号位置是否为0
,不为0
跳转到LLookupPreopt\Function
去加载共享缓存,暂时不对LLookupPreopt\Function
进行探索
and p10, p11, #0x0000ffffffffffff
p10
=_bucketsAndMaybeMask & 0x0000ffffffffffff
=buckets
0x0000ffffffffffff
0000000000000000111111111111111111111111111111111111111111111111
eor p12, p1, p1, LSR #7
,and p12, p12, p11, LSR #48
- 根据
hash 函数
获取index
,p12 = (_cmd ^ (_cmd >> 7)) & mask
- 根据
5. 汇编源码 向前遍历查找
汇编源码:
add p13, p10, p12, LSL #(1+PTRSHIFT)
PTRSHIFT
的定义:#define PTRSHIFT 3
p13 = buckets + (index << 4)
,p13
就是index
下的bucket
,是我们第一个要查的bucket
index << 4
相当于index * 16
1: ldp p17, p9, [x13], #-BUCKET_SIZE
p17 = imp
,p9 = sel
,然后*bucket--
cmp p9, p1
- 比较
sel
和_cmd
是否不等
- 比较
b.ne 3f
- 如果不相等,向前查找下一个
bucket
,跳到3f
- 如果不相等,向前查找下一个
2: CacheHit
- 如果相等,进入
CacheHit
缓存命中,返回
- 如果相等,进入
3: cbz p9, \MissLabelDynamic
- 判断
sel
是否为0
,如果为0
进入__objc_msgSend_uncached
函数
- 判断
cmp p13, p10
,b.hs 1b
while (bucket >= buckets)
,如果bucket >= buckets
,跳到上面1b
,否则进行下面的代码
6. 汇编源码 最后位置 向前遍历查找
汇编源码:
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
p13 = buckets + (mask << 1+PTRSHIFT)
,平移到buckets
的最后一个位置
add p12, p10, p12, LSL #(1+PTRSHIFT)
4: ldp p17, p9, [x13], #-BUCKET_SIZE
{imp, sel} = *bucket--
,向前推移去取值
cmp p9, p1
,b.eq 2b
if (sel == _cmd)
,跳到2b CacheHit
cmp p9, #0
while (sel != 0 &&
ccmp p13, p12, #0, ne
bucket > first_probed)
b.hi 4b
,回到4b
继续执行
三、真机跑汇编
新创建一个项目,接下来用汇编跟源码
的方式进行探索,汇编跟源码
有不懂的可以去 OC 对象原理探索(一)中查看。
如下是真机调试的汇编,可以发现跟汇编源码非常相似。
我们进行一些简单的调试,和对汇编源码
的一些验证。
- 根据打印可以看到,
x1
确实是sel
。
- 读取
x0
得到<SSLPerson: 0x280d78060>
,确实是消息接受者person
。 - 读取
x16
地址为0x00000001044415e0
,与SLPerson.class
相同,可以证明x16
是class
,x16 = x13 & 0x7ffffffffffff8
也就是isa & isaMask
。
- 读取
x12
得到0x0000000000000001
也就是1
,通过cache_hash
函数得到的哈希 index
。