我们在上一个篇章cahce的结构分析中,对cache的整个闭环流程进行了探索,最终停留在了_objc_msgSend,那么想要继续探索就得对Runtime的知识要有一定的了解了,Runtime官方文档
一.Runtime运行时简介
1.概念
-
编译时 :顾名思义就是正在编译的时候 . 那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码 (当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)
编译时就是简单的作一些翻译工作 :比如检查 兄die 你有没有粗心写错啥关键字了啊,有啥词法分析,语法分析之类的过程。就像个老师检查学生的作文中有没有错别字和病句一样 。如果发现啥错误编译器就告诉你,如果你用微软的VS的话,点下build,那就开始编译,如果下面有errors或者warning信息,那都是编译器检查出来的,所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译时类型检查,或静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。所以有时一些人说编译时还分配内存啥的肯定是错误的说法
-
运行时:就是代码跑起来了,被装载到内存中去了,(你的代码保存在磁盘上没装入内存之前是个死家伙。只有跑到内 存中才变成活的)。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断
-
runtime 其实c/c++/汇编 混编写出的一套API
2.版本信息
Runtime有两个版本 一个Legacy版本(早期版本) ,一个Modern版本(现行版本)
-
Legacy版本,如果你改变类中的实例变量的布局,必须从它继承的类重新编译。
-
Modern版本,如果你改变类中的实例变量的布局, 不必从它继承的类重新编译。
-
早期版本对应的编程接口:Objective-C 1.0
-
现行版本对应的编程接口:Objective-C 2.0
-
早期版本用于Objective-C 1.0, 32位的MacOSX的平台上
-
现行版本:iPhone程序和Mac OS X v10.5及以后的系统中的 64 位程序
3.交互方式
Objective-C程序有三种途径和运行时系统交互:
- 1.通过Objective-C代码,比如:[person sayNB]。
- 2.通过Framework&Serivce的方法,比如:isKindofClass。
- 3.通过Runtime提供的API接口,比如:class_getInstanceSize。
二.引入objc_msgSend
1.测试
接下来我们来做一下测试,代码如下
#import <objc/message.h>
LGPerson *person = [LGPerson alloc];
[person sayNB];
// 子类是不是没有 -> 父类
[person sayHello];
2.clang编译
通过clang编译main.m文件生成.cpp文件,查看相关代码如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_c501bc_mi_2);
}
return 0;
}
可以看到编译之后会发现Objective-C方法的调用,最终在底层是通过objc_msgSend进行消息发送实现
- 调用方法 = 消息发送 : objc_msgSend(消息的接受者,消息的主体(sel + 参数))
3.objc_msgSend
我们也直接使用objc_msgSend去调用
前提:在使用时一定要检查机制选项,如图
执行代码如下
objc_msgSend(person, @selector(sayNB));
输出结果
2021-06-29 15:59:32.669926+0800 001-运行时感受[27390:315754] 666
4.objc_msgSendSuper
在第一步中调用的方法sayHello,其实是调用得父类的方法,继续调试就发现了objc_msgSendSuper,那么我们去源码中看下objc_msgSend,objc_msgSendSuper函数信息
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
5.objc_super源码
从上面函数的参数可以看到,需要传入objc_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 */
//父类第一个搜索
};
6.仿写源码
我们也可以参照源码,去尝试调用,结果一样实现了方法调用
//参照源码写法
struct objc_super kc_objc_super;
//让person为消息接受者
kc_objc_super.receiver = person;
//我们也可以改变第一个要搜索的类,只不过默认是父类
kc_objc_super.super_class = LGPerson.class;
objc_msgSendSuper(&kc_objc_super,@selector(sayHello));
其实通过源码的描述,以及测试可以得出结论:
- 默认情况父类作为第一接受者,如果父类处理不了的消息会转发给receiver
- 方法的本质:消息接收者通过sel查找imp的过程。
三.objc_msgSend源码分析
1.查找objc_msgSend
首先还是全局搜索objc_msgSend,查找arm64架构的源码
进入之后找到objc_msgSend相关代码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判断p0(消息接受者)是不是为0(也就是空)
cmp p0, #0 // nil check and tagged pointer check
//判断SUPPORT_TAGGED_POINTERS 决定是走LNilOrTagged 还是LReturnZero,但是你会发现不管走那个最终都会通过LReturnZero中的END_ENTRY _objc_msgSend又返回_objc_msgSend
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
//说白了,上面的操作就是为取isa,[x0]就是开始存放的p0消息接受者
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//到这里就是通过消息接受者receiver取需找class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#endif
LReturnZero 和 LNilOrTagged相关汇编
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
GetClassFromIsa_p16 相关汇编
//.macro 表示宏定义
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
//说白了就是class 存在p16 旁边有注释
1:
//and $0,$1,#ISA_MASK
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
可以看到objc_msgSend得源码,其实是汇编写的,那么问题1来了为什么使用汇编写呢?其次通过上面的部分汇编可以看出就是通过消息接受者receiver去寻找class,那么寻找class得目的想必通过前面的学习,肯定是为了获取cache里面的方法缓存,那么是如何执行的呢
预知后续详情,请听下回方法快速与慢速查找分解