前言
前面探究了类里面的重要的变量,IOS 底层原理之cache分析分析了缓存方法调用流程。追根溯源找到了objc_msgSend
,下面探究下objc_msgSend
准备工作
Runtime
Runtime
简介
Runtime
通常叫它运行时
,还有一个大家常说的编译时
,它们之间的区别是什么
编译时
:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码翻译成机器能够识别的代码
。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查运行时
:代码跑起来,被装装载到内存中
。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断
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
调用三种方式
Objective-C
方式,[penson sayHello]
Framework & Serivce
方式,isKindOfClass
Runtime API
方式,class_getInstanceSize
方法的本质
方法底层的实现
探究方法的底层有两种方式。第一种汇编,第二种C++
代码。汇编方式的方法的参数需要读寄存器不方便,所以采用第二种方式生成main.cpp
文件。首先自定义LWPerson
类,在类中添加实例方法,在main
函数中调用
int main(int argc, char * argv[]) {
@autoreleasepool {
LWPerson * perosn = [LWPerson alloc];
[perosn sayHello];
[perosn showTime:10];
}
return 0;
}
复制代码
clang
把main.m
生成main.cpp
文件,查询main
函数的实现
源码分析:所有的方法调用都是通过objc_msgSend
发送的,所以方法的本质
就是消息发送
既然方法调用都是通过objc_msgSend
的,那么我可不可以直接通过objc_msgSend
发消息呢
int main(int argc, char * argv[]) {
@autoreleasepool {
LWPerson * perosn = [LWPerson alloc];
[perosn sayHello];
//objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend((id)perosn, sel_registerName("sayHello"));
}
return 0;
}
复制代码
2021-06-26 18:14:22.659269+0800 objc_msgSend[5461:254082] sayHello
2021-06-26 18:14:22.659722+0800 objc_msgSend[5461:254082] sayHello
复制代码
通过objc_msgSend
和[perosn sayHello]
结果是一样的,同时也验证了方法的本质是消息发送。在用objc_msgSend
方式发送消息。验证过程需要注意两点
- 必须导入相应的头文件
#import <objc/message.h>
- 关闭
objc_msgSend
检查机制:target
-->Build Setting
-->搜索objc_msgSend
--Enable strict checking of obc_msgSend calls
设置为NO
调用父类方法
调用本类中的方法实际是通过objc_msgSend
发送的,那么调用分类的方法消息发送是什么样的呢?自定义LWAllPerson
类,LWPerson
继承LWAllPerson
类。在LWAllPerson
类中自定义helloWord
,子类对象调用helloWord
方法
int main(int argc, char * argv[]) {
@autoreleasepool {
LWPerson * perosn = [LWPerson alloc];
[perosn helloWord];
}
return 0;
}
复制代码
clang
把main.m
生成mian.cpp
文件,查询main
函数的实现
clang
把LWPerson.m
生成LWPerson.cpp
文件,查询LWPerson
函数的实现
子类对象可以通过objc_msgSendSuper
方式调用父类的方法,方法的本质还是消息发送,只不过通过的不同发送流程,同样现在用objc_msgSendSuper
向父类发消息,objc_msgSendSuper
的第一个参数是void /* struct objc_super *super
类型,在源码中查找objc_super
类型
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 */
};
复制代码
objc_super
结构体类型里面有两个参数id receiver
和Class super_class
int main(int argc, char * argv[]) {
@autoreleasepool {
LWPerson * perosn = [LWPerson alloc];
//(void *)objc_msgSendSuper)((__rw_objc_super){(id)self,
//(id)class_getSuperclass(objc_getClass("LWPerson"))}, sel_registerName("helloWord"))
[perosn helloWord];
struct lw_objc_super{
id receiver;
Class super_class;
};
struct lw_objc_super lw_super;
lw_super.receiver = perosn;
lw_super.super_class = [LWAllPerson class];
objc_msgSendSuper(&lw_super, sel_registerName("helloWord"));
}
return 0;
}
复制代码
2021-06-26 19:21:05.047243+0800 objc_msgSend[6976:329613] 我是父类方法
2021-06-26 19:21:05.047989+0800 objc_msgSend[6976:329613] 我是父类方法
复制代码
[perosn helloWord]
和直接通过objc_msgSendSuper
给父类发消息的结过是一样的。子类的对象可以调用父类的方法。猜想:方法调用,首先在本类中找,如果没有就到父类中找
objc_msgSend
汇编探究
探究objc_msgSend
首先找到objc_msgSend
所在的底层库。怎么找呢?必须拿出yysd
-汇编
汇编显示objc_msgSend
在libobjc.A.dylib
系统库,实际上看objc_msgSend
前缀是objc
猜测应该在
objc
源码中。在objc
源码中全局搜索objc_msgSend
,找到真机的汇编objc-msg-arm64.s
objc_msgSend
汇编入口
下面的汇编会用到p0
-p17
,大家可能对汇编中x0
,x1
比较熟悉知道是寄存器。p0
-p17
就是对x0
-x17
重新定义
判断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
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
...
.macro ExtractISA
and $0, $1, #ISA_MASK // and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
.endmacro
// not JOP
#endif
复制代码
ExtractISA
主要功能 isa
& ISA_MASK
= class
存放到p16
寄存器
CacheLookup
流程
buckets
和下标index
源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了
三
件事
- 获取
_bucketsAndMaybeMask
地址也就是cache
的地址:p16
=isa
(class
),p16
+0x10
=_bucketsAndMaybeMask
=p11
- 获取
buckets
地址就是缓存内存的首地址:buckets
= ((_bucketsAndMaybeMask
>>48
)- 1 ) - 获取
hash
下标:p12
=(cmd ^ ( _cmd >> 7))& msak
这一步的作用就是获取hash
下标index
- 流程如下:
isa
-->_bucketsAndMaybeMask
-->buckets
-->hash
下标
遍历缓存
- 根据下标
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流程
CacheHit \Mode
的 Mode
= NORMAL
TailCallCachedImp
是一个宏
,宏定义
如下
// 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
遍历缓存流程图
疑问:为什么要判断bucket中的sel
= 0
,等于0
直接查找缓存流程就结束了
- 如果既没有
hash
冲突又没有目标方法的缓存,那么hash
下标对应的bucket
就是空的直接跳出缓存查找 - 不会出现中间是有空的
bucket
,两边有目标bucket
这种情况
mask
向前遍历缓存
向前遍历缓存没有查询到就会跳转到mask
对应的bucket
继续向前查找
- 找到最后一个
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
向前遍历和前面的循环遍历逻辑基本一样
缓存查询流程图
总结
探究底层发现一个问题,就是每个内容的底层都很复杂,进行了大量计算判断。不像大家平常在上层调用个方法看起来很简单。俗话说的好表面上简单的东西往往越复杂,表面上复杂的往往很简单。我就是表面复杂的。