引子:我们在很早时候就听过OC是一个运行时语言,那么什么是运行时?
引入两个概念,编译时 和 运行时
-
编译时 :顾名思义就是正在编译的时候 . 那啥叫编译呢?就是编译器帮你把
源代码翻译成机器能识别的代码 .
(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言.)
2.运行时 :就是代码跑起来被装载到内存中去的阶段.
运行时语言主要就是讲OC将数据类型的确定由编译时推迟到了运行时即runtime.
运行时机制使我们知道运行时采取决定一个对象的类别,以及调用该类对象指定方法
而不同对象以自己的方式响应相同消息的能力叫做多态。
那么这个runtime对我们开发来说有什么作用?
runtime
runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 runtime库里
面包含了跟类、成员变量、方法相关的API,比如获取类里面的所有成员变量,为类动态添 加成员变量,动态改变
类的方法实现,为类动态添加新的方法等 需要导入<objc/message.h><objc/runtime.h>
在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码,比如类转成了
runtime库里面的结构体等数据类型,方法转成了runtime库里面的C语言函数,平时调方法都是转成了objc_msgSend
函数(所以说OC有个消息发送机制)因此,可以说runtime是OC的底层实现,是OC的幕后执行者有了runtime库,
能做什么事情呢?runtime库里面包含了跟类、成员变量、方法相关的API,runtime是属于OC的底层, 可以进
行一些非常底层的操作(用OC是无法现实的, 不好实现)
1.在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
2.在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
3.遍历一个类的所有成员变量(属性)\所有方法
有了runtime,想怎么改就怎么改, runtime算是OC的幕后工作者.
在网上,我找到了这样一段关于runtime的描述。那么接下来,我们尝试一下其中提到的runtime库,以及里面最基础的objc_msgSend
方法。
我们依旧使用之前的工程,在main中,我们写这样一段代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
FQPerson *person = [FQPerson alloc];
Class personClass = [FQPerson class];
[person eat1];
[person eat2];
[person eat3];
[person eat4];
[person sayHelloWorld];
NSLog(@"%@",personClass);
}
return 0;
}
这里可以说是我们最基础的写法。
现在,我们通过终端使用clang指令 生成main.m对应的main.cpp文件。
我们在其中找到对应上面 main 函数的方法
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
FQPerson *person = ((FQPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FQPerson"), sel_registerName("alloc"));
Class personClass =((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat1"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat2"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat3"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat4"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHelloWorld"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_bs_g7z1ww_14gq20hnx_3hcvm2w0000gn_T_main_3fd2f3_mi_5,personClass);
}
return 0;
}
我们惊奇发现经过clang编译后,本身我们写的方法就已经被转为了我们之前提到的objc_msgSend方法。
那么,对我们来时就轻松了 可以直接的来“抄”一下这段系统调用方法的代码
首先引入头文件
#import <objc/message.h>
然后,在main函数中,我们将之前的方法改成objc_msgSend()的调用。
注意,我们需要先将工程中target -> BuildSetting -> enableStrictChecking of objc_msgSend calls -> NO 否则会报错
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
FQPerson *person = [FQPerson alloc];
objc_msgSend(person, sel_registerName("sayHelloWorld"));
[person sayHelloWorld];
}
return 0;
}
运行后打印
可见两种代码的实现方式一模一样。
接下来,进入今天的正题,让我们来研究一下objc_msgSend()
objc_msgSend()的方法查找流程
下面我们来看看objc_msgSend()。
在我们一般的oc代码中
[person sayHelloWorld]
通过对象person,去调用他的类FQPerson的sayHelloWorld方法
这个很好理解,因为直接在.h中声明,.m中实现,很清楚。
但在objc_msgSend中,我们是通过发送消息 通过 对象,和方法名SEL,让他们直接去查找对应的IMP进而找到方法的实现。
那么这个由sel-> imp的过程 objc_msgSend 是如何实现的
首先回忆一下之前的内容,关于方法的存储。
对象->类->cache_t或者 bits中的methodlist
回到我们的781版本的源码
全局搜索objc_msgSend
茫茫多的结果,我们不可能每个去看。
简单分析一下。
objc_msgSend是runtime中的,我们之前提到OC运行时装载是在编译时之后,编译后代码是汇编的,那我们现在要研究的也应该是在汇编中的,所以结尾应该是.s文件,我们开发面向的又是真机,那么我们第一要研究的必然是真机对应的arm64
打开对应的obj-msg-arm64.s。好了,研究开始。
继续研究源码
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 // p16 = class
其中p0为第一个参数,即为当前objc_msgSend中的消息接收者。
将p0与nil比较,即为判断第一个obj是否为空
若为tagged pointer或为空都会跳转到其他流程,则剩下的为不为空的流程
此时取首地址即isa 然后通过GetClassFromIsa_p16获取class
GetClassFromIsa_p16 流程
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
例如
在LP64 中,直接通过isa与上ISA_MASK 得到类,与我们之前探索一致
继续之前流程
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
- 得到isa后 开始CacheLookup流程。
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
汇编源码比较难懂,所幸苹果在旁边写了清楚的注释。
提醒我们自己写代码多写点注释,不然时间久了自己都看不懂
闲话扯完继续分析
之前我们得到了class的地址
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#define CACHE (2 * __SIZEOF_POINTER__)
所以即为平移16个字节,得到maskAndBuckets
然后通过位运算求得 buckets 以及 _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
这里回到了我们之前研究的地址平移的内容
- buckets的地址,直接加上_cmd&mask<<(1+PTRSHIFT)个单位的长度,那么就能知道找到_cmd&mask为下标的bucket.
_cmd&mask的哈希运算与我们之前存储时计算下标的方式一致
PTRSHIFT = 3 cmd&mask<<(1+PTRSHIFT)即向左移4位,等于值cmd&mask * 16
分别取出SEL 存入P17 IMP 存入P9
开始递归循环
- 判断sel是否与_cmd一致,如果一致,则跳转 CacheHit 表示找到方法,返回imp
- 如果不相等 则分情况
- 若一直找不到,直接跳checkMiss 进入慢速查找流程
- 找到buckets的最后一个地址(根据首地址和_mask 平移计算)向前循环递归
流程简图为
这样,我们就研究了objc_msgSend的缓存查找,或者说快速查找流程
后续的慢速查找下次继续研究更新