前言
前面探究了类里面的重要的变量,iOS 底层原理之cache分析分析了缓存方法调用流程。
追根溯源找到了objc_msgSend,下面探究下objc_msgSend。
准备工作
Runtime
Runtime简介
Runtime通常叫它运行时,还有一个大家常说的编译时,它们之间的区别是什么?
Runtime版本
Runtime有两个版本,一个Legacy版本(早期版本),一个Modern版本(现行版本)
Runtime调用三种方式

方法的本质
方法底层的实现
探究方法的底层有两种方式。第一种汇编,第二种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方式发送消息。验证过程需要注意两点:
调用父类方法
调用本类中的方法实际是通过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小对象类型
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

源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了三件事。
遍历缓存

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直接查找缓存流程就结束了。

遍历缓存流程图
疑问:为什么要判断bucket中的sel = 0,等于0直接查找缓存流程就结束了。
mask向前遍历缓存
向前遍历缓存没有查询到就会跳转到mask对应的bucket继续向前查找。

缓存查询流程图

总结
探究底层发现一个问题,就是每个内容的底层都很复杂,进行了大量计算判断,不像大家平常在上层调用个方法看起来很简单。
俗话说的好表面上简单的东西往往越复杂,表面上复杂的往往很简单。我就是表面复杂的。
文章由作者:嘿嘿小开发 逻辑iOS学员提供
本文分享自 HelloCoder全栈小集 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!