iOS底层探索之Runtime:运行时&方法的本质
Posted 卡卡西Sensei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之Runtime:运行时&方法的本质相关的知识,希望对你有一定的参考价值。
1. 回顾
在之前的几篇博客里面,已经对OC类的底层结构进行了分析,并对内部主要的成员变量(isa/bits)做了详细的分析。在上两个博客
iOS底层探索之类的结构—cache分析(上)
ios底层探索之类的结构—cache分析(下)
对类中的cache
做了比较详细的分析。后面通过断点
查看汇编
可以发现在insert
方法调用流程之前,还有一个cache
读取流程,objc_msgSend
和 cache_getImp
。这就涉及到Runtime
的知识点了,之前的内容都是承上启下的,是互相关联的。
2. Runtime
2.1 什么是Runtime
runtime
翻译过来称为运行时,与之对应的是编译时
。大部分的iOS开发人员,都听过runtime
这个词,也知道运行时。但只是停留在表面,只是知道而已,并没有去深入的去探索和分析过。
OC
语言是一门动态语言
,拥有动态语言的三大特性:动态类型
、动态绑定
、动态加载
。而底层实现就是熟悉又陌生的Runtime
。
运行时
是一种面向对象的编程语言(面向对象编程)的运行环境。运行时表明了在某个时间段内,哪个程序正在运行。运行时是计算机程序运行生命周期内的一个阶段,其它阶段还包括:编译时、链接时和加载时。简单理解就是, 代码跑起来,被装载到内存中的过程。(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。编译时
顾名思义就是正在编译的时候。那什么叫编译
呢?就是编译器帮你把源代码翻译成机器能识别的二进制代码 。- (当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。比如
Java
只有JVM
识别的字节码,C#
中只有CLR
能识别的MSIL
。另外还有链接器、汇编器、为了了便于理解我们可以统称为编译器) - 那编译时就是简单的作一些翻译工作,比如检查老兄你有没有粗心写错啥
关键字
了。 词法分析
,语法分析之类的过程。就像个老师检查学生的作文中有没有错别字和病句一样 。- 如果发现啥错误编译器就告诉你,平时使用
Xcode
时,点下build
那就开始编译。 - 如果下面有
errors
或者warning
信息,那都是编译器检查出来的。这时的错误就叫编译时
错误,这个过程中做的类型检查也就叫编译时类型检查
,或静态类型检查
(所谓静态嘛就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。
2.2 runtime的使用的三种方式
runtime
的使用的三种方式,其三种实现方法与编译层和底层的关系如图所示
-
通过
OC
上层的代码实现,例如[JPerson hello]
-
通过
NSObject
方法实现,例如isKindOfClass
-
通过
Runtime API
底层方法实现,例如class_getInstanceSize
图中的
compiler
就是编译器,就是我们熟悉的LLVM
3. OC方法的本质
在之前的一篇博客iOS开发之结构体底层探索我们知道平时写的OC
代码,底层实现其实都是C/C++
的代码实现的,再经过编译器LLVM
编译,最终转化为机器语言。
通过clang
编译的源码,理解了OC
对象的本质(结构体
),同样的,我们也可以使用clang
命令编译成main.cpp
文件,看看方法的本质
是什么?
3.1 objc_msgSend
- 编译前
@interface JPPerson : NSObject
@property (nonatomic, readwrite , copy) NSString *personName;
- (void)superTest;
@end
@implementation JPPerson
- (void)superTest {
NSLog(@"这是父类");
}
@end
@interface JPStudent : JPPerson
@property (nonatomic, readwrite , copy) NSString *studentName;
- (void)test;
@end
@implementation JPStudent
- (void)test {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
JPStudent *stu = [[JPStudent alloc]init];
[stu test];
[stu superTest];
}
return 0;
}
- 编译后
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
JPStudent *stu = ((JPStudent *(*)(id, SEL))(void *)objc_msgSend)((id)((JPStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JPStudent"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("test"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("superTest"));
}
return 0;
}
通过上述代码可以看出,OC
的方法调用,底层变成了objc_msgSend
,也就是我们熟悉的消息发送
我们可以通过模仿objc_msgSend
方法来实现,[stu test]
的调用
从控制台的输出可以看到,是一模摸一样样
。
由此可以断定 [stu test]
等价于objc_msgSend(stu,sel_registerName("test"))
注意
:不能直接调用
objc_msgSend
,需要导入头文件#import <objc/message.h>
需要将
target --> Build Setting -->
搜索msg
– 将enable strict checking of obc_msgSend calls
由YES
改为NO
,将严厉的检查机制关掉,否则objc_msgSend
的参数会报错。
- 未导入
#import <objc/message.h>
- 启用 objc_msgSend 调用的严格检查,设置为
NO
objc_msgSend
(消息的接受者,消息的主体(sel + 参数))
3.2 objc_msgSendSuper
在上面👆在main
函数中调用了父类的方法[stu superTest]
,clang
编译的源码里面发现了objc_msgSendSuper
。
这是子类完全调用了父类的方法,那么我们子类要是也有一个superTest
方法,但是子类并没有实现这个方法,那么我们看看结果如何?
@interface JPPerson : NSObject
@property (nonatomic, readwrite , copy) NSString *personName;
- (void)superTest;
@end
@implementation JPPerson
- (void)superTest {
NSLog(@"%s",__func__);
}
@end
@interface JPStudent : JPPerson
@property (nonatomic, readwrite , copy) NSString *studentName;
- (void)test;
- (void)superTest;
@end
@implementation JPStudent
- (void)test {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
JPStudent *stu = [[JPStudent alloc]init];
[stu test];
NSLog(@"-------华丽的分割线-----------");
objc_msgSend(stu,sel_registerName("test"));
[stu superTest];
}
return 0;
}
打印结果
-[JPStudent test]
-------华丽的分割线-----------
-[JPStudent test]
-[JPPerson superTest]
Program ended with exit code: 0
对象的方法调用,实际是父类的实现方法,为了验证这个说法,我们可以尝试通过objc_msgSendSuper
实现验证。
objc_msgSendSuper
方法中有两个参数(结构体,sel),其结构体类型是objc_super
定义的结构体对象,且需要指定receiver
和super_class
两个属性,源码实现定义如下
通过查看苹果的源码,找到了如下方法
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
代码改造
int main(int argc, const char * argv[]) {
@autoreleasepool {
JPStudent *stu = [[JPStudent alloc]init];
JPPerson *person = [JPPerson alloc];
[person superTest];
struct objc_super jpsuper;
jpsuper.receiver = stu; //消息的接收者
jpsuper.super_class = [JPStudent class]; //告诉父类是谁,改成 [JPPerson class]也是一样的
//消息的接受者还是自己 -> 父类 -> 方法么有找到请你直接找我的父亲
objc_msgSendSuper(&jpsuper, sel_registerName("superTest"));
}
return 0;
}
打印结果
[26066:278406] -[JPPerson superTest]
[26066:278406] -[JPPerson superTest]
4. 总结
-
OC调用方法,其实本质是
发送消息
(objc_msgSend
) -
OC方法的调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
-
子类调用父类的方法,底层会调用
objc_msgSendSuper
更多内容持续更新
🌹 请动动你的小手,点个赞👍🌹
🌹 喜欢的可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹
以上是关于iOS底层探索之Runtime:运行时&方法的本质的主要内容,如果未能解决你的问题,请参考以下文章
iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析
iOS底层探索之Runtime: lookUpImpOrForward慢速查找分析
iOS开发底层之RuntimeObjc_msgSend探究 - 08