前言
前面探究了类里面的重要的变量,iOS 底层原理之cache分析分析了缓存方法调用流程。
追根溯源找到了objc_msgSend,下面探究下objc_msgSend。
准备工作
- objc4-818.2 源码
- Objective-C Runtime
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函数中调用。
代码语言:javascript复制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发消息呢。
代码语言:javascript复制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;
}
(滑动显示更多)
代码语言:javascript复制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方法。
代码语言:javascript复制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 类型。
代码语言:javascript复制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
代码语言:javascript复制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;
}
(滑动显示更多)
代码语言:javascript复制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
代码语言:javascript复制// 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是一个宏,宏定义如下:
代码语言:javascript复制// 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直接查找缓存流程就结束了。
遍历缓存流程图
疑问:为什么要判断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向前遍历和前面的循环遍历逻辑基本一样
缓存查询流程图
总结
探究底层发现一个问题,就是每个内容的底层都很复杂,进行了大量计算判断,不像大家平常在上层调用个方法看起来很简单。
俗话说的好表面上简单的东西往往越复杂,表面上复杂的往往很简单。我就是表面复杂的。
文章由作者:嘿嘿小开发 逻辑iOS学员提供