这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
1 Runtime
1.1 什么是Runtime
runtime翻译过来称为运行时,与之对应的是编译时。大部分的iOS开发人员,都听过runtime这个词,也知道运行时,但只是停留在表面,只是知道而已,并没有去深入的去探索和分析过。
OC语言是一门动态语言,拥有动态语言的三大特性:动态类型、动态绑定、动态加载。而底层实现就是熟悉又陌生的Runtime。
- 运行时是一种面向对象的编程语言(面向对象编程)的运行环境。运行时表明了在某个时间段内,哪个程序正在运行。运行时是计算机程序运行生命周期内的一个阶段,其他阶段还包括:编译时、链接时和加载时。简单理解就是,代码跑起来,被装载到内存中的过程。
- 编译时顾名思义就是正在编译的时候。那什么叫编译呢?就是编译器帮你把源代码翻译成机器能识别的二进制代码。(只是一般意义上说,实际上可能只是翻译成某个中间状态的语言。比如Java只有JVM 识别的字节码,C#中只有CLR能识别的MSIL。另外还有链接器、汇编器,为了便于理解我们可以统称为编译器)
- 编译时就是简单的做一些翻译工作,比如检查有没有关键字的错误
- 词法分析,语法分析之类的过程。
- 如果发现错误,编译器就会告诉你,平时使用Xcode时,点击build就开始编译
- 如果由errors或者warning信息,那就是编译器检查出来的。这是的错误叫做编译时错误,这个过程中做的类型检查也就是编译时类型检查,或静态类型检查(所谓静态就是没有把代码放内存中运行起来,而只是把代码当作文本来扫描下)
1.2 runtime饿使用的三种方式
runtime的使用的三种方式,其三种实现方法与编译层和底层的关系如图所示
- 通过OC上层的代码实现,例如 [LGPerson Hello]
- 通过
NSObject
方法实现,例如isKindOfClass
- 通过
Runtime API
底层方法实现,例如class_getImstanceSize
图中的compiler就是编译器,就是我们熟悉的LLVM
2 OC方法的本质
我们知道平时写的OC代码,底层实现其实都是C/C++的代码实现的,再经过编译器LLVM编译,最终转化为机器语言。
通过clang编译的源码,理解了OC对象的本质是结构体,同样的,我们也可以使用clang命令编译成main.cpp文件,看安方法的本质是什么?
2.1方法底层的实现
在main函数中,写入以下代码
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person say:@"NB"];
复制代码
生成cpp文件,底层代码如下
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, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_d8842a_mi_3);
复制代码
可以看出,方法的本质:objc_msgSend
消息发送
objc_msgSend
的参数
- 消息接收者
- 消息主体(
SEL
+参数)
2.2objc_msgSend
在Build Setting中,将Enable Strict Checking of obc_msgSend Calls
设置为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
导入头文件
#import <objc/message.h>
复制代码
在main函数中,写入以下代码
@interface LGPerson : NSObject
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{ NSLog(@"666"); }
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
}
return 0;
}
-------------------------
//输出结果:
666
复制代码
- 和利用OC方法的执行结果相同
- sel_registerName为@selector()的底层实现
2.2 objc_msgSendSuper
子类调用父类方法时,可使用objc_msgSendSuper,向父类发送消息
在objc源码中,找到objc_msgSendSuper的定义
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
- 传入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.
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};
复制代码
- receiver:消息接收者
- super_class传入第一查找的类,如果找不到,继续向所属父类一层一层的查找
在项目中调用objc_msgSendSuper
定义LGeacher,继承于LGPerson
@interface LGTeacher : LGPerson
- (void)sayNB;
@end
@implementation LGTeacher
@end
复制代码
- 定义sayNB当法,但不实现
在main函数中,写入以下代码
struct lg_objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGTeacher *teacher = [LGTeacher alloc];
struct lg_objc_super lgSuper;
lgSuper.receiver=teacher;
lgSuper.super_class=[LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayNB));
}
return 0;
}
-------------------------
//输出结果:
666
复制代码
- 按照
objc_super
相同结构,定义lg_objc_super
结构体 - 我们已经知道
LGTeacher
中没有sayNB
方法的实现,所以super_class
直接传入LGPerson
的类对象 - 如果
super_class
传入LGTeacher
的类方法,打印结果相同。区别在于需要向父类多查找一层。
3 objc_msgSend汇编代码
在objc4-818.2源码中,不同系统架构的汇编指令都有差异,我们只针对最常用的arn64架构下的汇编代码进行探索
3.1 objc_msgSend
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
复制代码
ENTRY _objc_msgSend
:入口- p0寄存器,存储消息接收者
cmp p0, #0
:消息接收者和#0比较- 汇编代码中,没有nil,只有0和1
- 这里的#0比较,可以理解消息接收者为nil
SUPPORT_TAGGED_POINTERS
:真机arm64架构,SUPPORT_TAGGED_POINTERS
定义为1b.le LNilOrTagger
:小于等于0,进入LNilOrTagged
流程b.eq LReturnZero
:等于0,进入LReturnZero流程- 否则,消息接收者存在,继续执行代码
ldr p13, [x0]
:将X0寄存器取地址,赋值p13寄存器p13
:存储消息接收者的isa
- 进入
GetClassFromIsa_p16
流程,传入isa、1、消息接收者
3.2 LNilOrTagged
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
复制代码
- b.eq:等于0,进入
LReturnZero
流程 - 否则,小于0,继续执行代码
- 进入
GetTaggedClass
流程 - 进入
LGetIsaDone
流程
3.3 LReturnZero
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
复制代码
- 返回nil
3.4 GetClassFromIsa_p16
.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
1:
#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
复制代码
- 入参:
src
:isa
needs_auth
:1auth_address
:消息接收者
- 当前不符合
SUPPORT_INDEXED_ISA
条件,跳过 - 符合
__LP64__
条件,不满足needs_auth
等于0 的条件,进入else
分支 ExtractISA p16, \src, \auth_address
:进入ExtractISA流程- 传入p16寄存器,isa,消息接收者
- p16寄存器的值,存储的是什么不重要,因为传入到
ExtractISA
中会被赋值
3.5 ExtractISA
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
复制代码
- $0:p16寄存器
- $1:isa
- and 1, #ISA_MASK:将isa&ISA_MASK的结果,存储到p16寄存器
- p16:存储类对象地址
3.6 LGetIsaDone
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
复制代码
objc_msgSend
的后续流程- 消息接收者存在,并且得到类对象,继续
LGetIsaDone
流程
- 消息接收者存在,并且得到类对象,继续
- 进入
CacheLookup
缓存查找流程,也就是所谓的sel-imp
快速查找流程 - 内部会调用imp或
objc_msgSend_uncached
4 流程图
— 为什么要获取类对象?
-
在快速查找流程中,查找的
cache
存储在类对象中,所以必须拿到类对象才能进行后面的流程