前言
在《cache底层分析》一文中详细得剖析了cache
的底层原理以及其相关的流程。那么我们有没有留意到cahche
调用insert
方法之前做了哪些操作呢?哪些操作又是以什么形式传递的呢?
那么查看objc-cache.mm文件的头部注释中写着insert()的插入时机是通过最上层的objc_msgSend触发的,如下图:
准备资料
- objc4-818.2 源码
- Objective-C Runtime
- arm汇编指令
runtime
runtime定义
编译时
-
编译时
顾名思义就是正在编译的时候. 编译器把源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析、词法分析等编译时类型检查(静态类型检查)
来发现代码中的errors
或warning
等编译时的错误信息。 -
静态检查
不会把代码放内存中运⾏起来,⽽只是当作⽂本
来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法。
运行时
-
运⾏时
就是代码通过dyld
被装载到内存中执行
的过程。运⾏时类型检查
就与前⾯讲的编译时类型检查(或者静态类型检查
)不⼀样。不是简单的扫描代码,⽽是在内存中做操作和判断
。
runtime的版本
runtime
有两个版本,一个Legacy
版本(早期版本),一个Modern
版本(现行版本)。
- 早期版本对应的编程接口:
Objective-C 1.0
- 现行版本对应的编程接口:
Objective-C 2.0
,源码中经常看到的OBJC2
- 早期版本用于
Objective-C 1.0
,32
位的Mac OS X
的平台 - 现行版本用于
Objective-C 2.0
,iPhone
程序和Mac OS X v10.5
及以后的系统中的64
位程序
注意:runtime就是c/c++/汇编写的一套API
。
runtime三种实现方式
-
Objective-C
方式,[penson sayHappy]
-Framework & Serivce
方式,isKindOfClass
-
Runtime API
方式,class_getInstanceSize
方法的本质
探究底层又两个方式,第一种就是看汇编代码,其次就是C/C++编译之后的代码。如果分析汇编代码的话会设计到寄存器数据的一系列读取操作,过程比较繁琐,那么我们就采用第二种方式来看看方法底层的实现是怎么样子的。首先编译生成main.cpp文件,然后自定义XXPerson类,在XXPerson类中添加实例方法,在main函数中调用如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXPerson *person = [[XXPerson alloc]init];
[person saySomething];
[person sayHappy:@"happy!"];
}
return 0;
}
xrun导出main.cpp文件,查看到main函数的底层实现如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy:"), (NSString *)&__NSConstantStringImpl__var_folders_mq_n7r4vx491nz9b2b3wpmz1mg00000gn_T_main_12c37c_mi_1);
}
return 0;
}
分析:
- 通过底层的代码,方法的实现是通过
objc_msgSend
来发送的。 - 方法的本质其实就是
消息的发送
。
通过底层objc_msgSend来实现法法,情况如下:
-
objc_msgSend
能够调用类的方法,跟对象调用的结果一样。
注意:
- 运行项目之前必须导入
头文件。 - 关闭
objc_msgSend
检查机制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls设置为NO
。
调用类方法
创建XXPerson
类方法sayNB
,通过调用,已经底层的main.cpp
可以得出一下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXPerson *person = [[XXPerson alloc]init];
[person saySomething];
[XXPerson sayNB];
}
return 0;
}
//底层代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("sayNB"));
}
return 0;
}
- 类方法的调用也是通过
objc_msgSend
来进行消息发送。
注意:
- 通过之前类结构的学习,底层是
不分
类方法跟实例方法的,只是查找方法的地方不一样(实例方法保存在本类钟,类方法保存在元类中)
。 - 类方法其实就是
元类的实例方法
。
调用父类方法
穿件XXTeacher类继承XXPerson类,并用XXTeacher实例调用父类的saySomething方法如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXTeacher *teacher = [[XXTeacher alloc]init];
[teacher saySomething];
}
return 0;
}
用xrun
导出main.cpp
文件,查看底层代码实现
在用XXTeacher.m文件重写saySomething方法,然后用
xrun
把XXTeacher.m
生成XXTeacher.cpp
文件,查询XXTeacher
函数的实现:
- 子类调用父类的方法可以通过
objc_msgSendSuper
来进行消息的发送
,其本质也就是消息的发送
。 -
objc_msgSendSuper
是通过向父类发送消息,与objc_msgSend
流程有点不一样。
objc_msgSendSuper的数据结构
通过查找objc4
的源码发现:
//objc_msgSendSuper的定义
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
查看参数objc_super
的结构如下(提取__OBJC2__
的部分):
struct objc_super {
//消息的接收者
__unsafe_unretained _Nonnull id receiver;
//方法最先查找的class是super_class ,如果super_class查找不到会查找super_class的父类
__unsafe_unretained _Nonnull Class super_class;
};
objc_msgSendSuper代码案例
通过objc_msgSendSuper
的方式,调用XXPerson
的saySomething
方法:
-
objc_msgSendSuper
能够向父类发送消息,调用父类的方法。 - 方法调用,首先在本类中找,如果没有就到父类中找。
(receiver只是指定调用的是谁,但是方法是在super_class找)
objc_msgSend汇编探究
首相我们在saySomething方法调用时候下个汇编断点,如下图:
然后我们进入objc_msgSend的汇编实现(打objc_msgSend的符号断点),如下图:
- 汇编显示
objc_msgSend
在libobjc.A.dylib
系统库。 -
objc_msgSend
也可以在objc4
的源码中找到。
objc_msgSend在objc4源码中的实现
到这里源码的实现,有些同学就会想到objc_msgSend
可能是c
或者是c++
来实现的。可是实践告诉我们,objc_msgSend
的底层实现在源码中是汇编语言。
源码查找流程:在objc
源码中全局搜索objc_msgSend
,找到真机的汇编objc-msg-arm64.s
源码中寄存器的对应发生了一丢丢改变
(如p0 = x0)
,为了方便理解方法体代码,如下图:
objc_msgSend
入口汇编代码
判断
receiver
是否等于nil
, 再判断是否支持Taggedpointer
小对象类型。
- 支持
Taggedpointer
小对象类型,小对象为空 ,返回nil
,不为nil
处理isa
获取class
跳转CacheLookup
流程 。 - 不支持
Taggedpointer
小对象类型且receiver = nil
,跳转LReturnZero
流程返回nil
。 - 不支持
Taggedpointer
小对象类型且receiver != nil
,通过GetClassFromIsa_p16
把获取到class
存放在p16
的寄存器中,然后走CacheLookup
流程。
GetClassFromIsa_p16
获取Class
汇编流程
GetClassFromIsa_p16
核心功能获取class
存放在p16
寄存器。(那么就是着重看ExtractISA方法的实现)
ExtractISA
方法的汇编实现
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
...
.macro ExtractISA //ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
and $0, $1, #ISA_MASK // and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
.endmacro
// not JOP
#endif
ExtractISA
主要功能isa & ISA_MASK = class
存放到p16
寄存器。
重点:CacheLookup
汇编实现流程
在《cache底层分析》一文中已经根据objc4底层源码分析过整个insert
的流程了,那么通过CacheLookup
汇编的形式来看看这个流程跟之前的是否能够衔接上,拭目以待!!
buckets
和下标index
源码分析:
- 获取
_bucketsAndMaybeMask
地址也就是cache
的地址:p16 = isa(class)
,p16 + 0x10 = _bucketsAndMaybeMask = p11
。 - 获取buckets容器的首地址:
buckets = _bucketsAndMaybeMask & 0xffffffffffff(maskShift不同架构也会不同)
。 - 获取
hash
下标:p12 =(cmd ^ ( _cmd >> 7))& msak
这一步的作用就是获取hash
下标index
。
流程:isa
--> _bucketsAndMaybeMask
-->buckets
-->hash
-->index
。
遍历缓存
源码分析:
- 根据下标
index
找到index
对应的bucket
。p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
。 - 先获取对应的
bucket
然后取出imp
和sel
存放到p17
和p9
,然后*bucket--
向前移动。 -
1
流程:p9= sel
和 传入的参数_cmd
进行比较。如果相等走2
流程,如果不相等走3流程。 -
2
流程:缓存命中直接跳转CacheHit
流程。 -
3
流程:判断sel = 0
条件是否成立。如果成立说明buckets
里面没有传入的参数_cmd
的缓存,没必要往下走直接跳转__objc_msgSend_uncached
流程。如果sel != 0
说明这个bucket
被别的方法占用了。你去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket
和第一个bucket
地址大小,如果大于第一个bucket
的地址跳转1
流程循环查找,如果小于等于则接继续后面的流程。 - 如果循环到第
1
个bucket
里都没有找到符合的_cmd
。那么会接着往下走,因为下标index
后面的可能还有bucket
还没有查询。
CacheHit
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else //这是我们需要研究的
.macro TailCallCachedImp
// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
eor $0, $0, $3 // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
br $0 //调用 imp,意思是找到方法了并调用了
.endmacro
...
#endif
缓存查询到以后直接对bucket
的imp
进行解码
操作。即imp = imp ^ class
,然后调用解码后的imp
。
遍历缓存流程图
带着疑问:为什么sel = 0 的时候就直接跳出了缓存的查找呢?
分析得出:
- 如果既没有
hash冲突
又没有目标方法的缓存
,那么hash
下标对应的bucket
就是空的直接跳出缓存查找。 - 不会出现中间是有空的
bucket
,两边有目标bucket
这种情况。
mask
向前遍历缓存
分析:
- 找到最后一个
bucket
的位置:p13 = buckets + (mask << 1+3)
找到最后一个bucket
的位置。 - 先获取对应的
bucket
然后取出imp
和sel
存放到p17
和p9
,然后*bucket--
向前移动。 -
p9= sel
和 传入的参数_cmd
进行比较。如果相等走2
流程。 - 如果不相等在判断(
sel != 0 && bucket > 第一次确定的hash下标bucket
)接着循环缓存查找,如果整个流程循环完仍然没有查询到或者遇到空的bucket
。说明该缓存中没有缓存)sel = _cmd
的方法,缓存查询结束跳转__objc_msgSend_uncached
流程。 -
mask
向前遍历和前面的循环遍历逻辑基本一样。