在上一篇 cache_t 原理分析 分析了 cache
的写入流程,在写入之前还有 cache
的读取流程
runtime
runtime
即我们常说的 运行时
,是 OC
底层的一套 C/C++ 的 API,编译器最终都会将 OC 代码转化为运行时代码,我们通过 clang -rewrite-objc xxx.m可以看到编译后的 .cpp 文件。
运行时
是相对于 编译时
而言
运行时:代码跑起来,被装载到内存中的过程,如果代码有问题,会崩溃,是一个动态阶段
编译时:是将源代码编译成计算机能识别的代码的过程,主要是检查语法、词法分析,是一个静态的阶段
交互方式
Objective-C 代码:例如 [person say666];
NSObject 方法: 例如
isKindOfClass
、isMemberOfClass
Runtime API:例如
class_getInstanceSize
compiler
就是我们熟知的编译器,即 LLVM
,runtime system libarary
就是底层库
方法的本质
准备环境
- 创建一个
LCPerson
类继承自NSObject
,添加一个实例方法
@interface LCPerson : NSObject
-(void)say666;
@end
@implementation LCPerson
-(void)say666 {
NSLog(@"say666");
}
@end
- 在
main
函数中创建LCPerson
的实例,并调用实例方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [LCPerson alloc];
[person say666];
}
return 0;
}
- 通过
Clang
命令,将main.m
文件编译成main.cpp
文件,找到main
函数的实现代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("say666"));
}
return 0;
}
可以看到,LCPerson
类调用 alloc
类方法以及对象调用 say666
实例方法,最终都会转化为 objc_msgSend
消息发送,由此,我们可以判断方法的本质就是消息发送。
验证
- 我们可以通过调用
objc_msgSend
方式调用,查看与对象直接调用结果是否一致
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [LCPerson alloc];
[person say666];
objc_msgSend(person, sel_registerName("say666"));
}
return 0;
}
- 打印结果
可以看到 [person say666] 与 objc_msgSend(person, sel_registerName("say666")) 是一致的
注意:
直接调用objc_msgSend
的方法会报错,需要导入头文件#import <objc/message.h>
,此时编译还是会报错,需要将严厉检查机制设置为NO
调用父类方法
我们再创建一个 LCSon
类,继承自 LCPerson
类,并声明 -(void)say666
实例方法,不实现
@interface LCSon : LCPerson
-(void)say666;
@end
@implementation LCSon
@end
- 通过调用
objc_msgSendSuper
方式调用,查看与子类对象直接调用结果
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCSon *son = [LCSon alloc];
[son say666];
struct objc_super lcsuper;
lcsuper.receiver = son;
lcsuper.super_class = [LCPerson class];
objc_msgSendSuper(&lcsuper, sel_registerName("say666"));
}
return 0;
}
- 打印结果
我们发现 [son say666]
和 objc_msgSendSuper
执行的都是父类中 say666
的实现,由此我们可以猜测,方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
拓展-objc_msgSendSuper
上面我们用到了 objc_msgSendSuper
调用,我们看下它的源码定义
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);
#endif
有两个参数(objc_super
结构体,sel 方法名),其结构体类型是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 */
};
#endif
从源码我们可以知道 objc_super
有两个成员 receiver
(消息接收者)以及 super_class
(父类)。这个是告诉编译器,优先从父类中查找方法实现;直接调用方法是优先从子类中找方法实现。
objc_msgSend
在 objc-781 源码中查找 objc_msgSend
,发现都是用汇编实现的,汇编的特性
- 快:更容易被机器识别
- 参数的动态性:汇编调用函数时传递的参数是不确定的
消息查找机制
- 快速查找:通过
cache
缓存查找 - 慢速查找:methodList 中查找,查找不到会走消息转发流程
源码分析
在 objc4-781 源码中我们在 objc-msg-arm64.s
文件找到 ENTRY _objc_msgSend 入口部分
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0 是传入的第一个参数:消息的接收者。
//cmp(compare) p0与nil比较,如果p0为空,那么就直接返回
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//小对象类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//消息接收者(p0)为空,返回空
b.eq LReturnZero
#endif
//从x0寄存器指向的地址 取出消息接收者的 isa,存入 p13寄存
ldr p13, [x0] // p13 = isa
//在 64 位架构下,p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//等于空,返回空
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
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
1. 消息接收者(receiver)是否为空
- 如果支持
tagged pointer
,跳转至LNilOrTagged
- 如果小对象为空,直接返回空(LReturnZero)
- 不为空,继续下面流程
- 如果不是小对象
- 消息接收者(p0)为空,返回空(LReturnZero)
- 从
receiver
中取出isa
存入p13
寄存器 - 通过
GetClassFromIsa_p16
得到class
信息
GetClassFromIsa_p16 的汇编代码如下
.macro GetClassFromIsa_p16 /* src */
//此处用于watchOS
#if SUPPORT_INDEXED_ISA
// Indexed isa
//将isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
//判断是否是 nonapointer isa
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
//将_objc_indexed_classes所在的页的基址 读入x10寄存器
adrp x10, _objc_indexed_classes@PAGE
//x10 = x10 + _objc_indexed_classes(page中的偏移量) x10基址 根据 偏移量 进行 内存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
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
//p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
2. CacheLookup 缓存查找
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
//#define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//p11 = mask|buckets,从 x16(isa)中平移16字节,得到 cache,存入p11。objc_class 结构,偏移 isa(8字节)+ superClass(8字节)就是 cache(mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//p11(cache) & 0x0000ffffffffffff ,高16位抹零,得到 buckets,存入p10寄存器
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p11(cache)右移48位,得到 mask,mask&p1(sel,msgSend的第二个参数 cmd),就会得到 sel-imp的下标 index,存入p12
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
//define PTRSHIFT 3
//p12是下标,p10是 buckets 的首地址,下标左移16位(1<<4)得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
//LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
ldp p17, p9, [x12] // {imp, sel} = *bucket
//比较 sel 与 p1
1: cmp p9, p1 // if (bucket->sel != _cmd)
//不相等,跳转到2f
b.ne 2f // scan more
//相等,即 cacheHit 缓存命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
//判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)
cmp p12, p10 // wrap if bucket == buckets
//如果等于,则跳转至第3f
b.eq 3f
//从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//跳转至第1步,继续对比 sel 与 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//人为设置到最后一个元素,p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找
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.
//再查找一遍缓存,拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//如果不相等,即走到2f
b.ne 2f // scan more
//如果相等 即 CacheHit,直接返回imp
CacheHit $0 // call or return imp
//如果一直找不到,即CheckMiss
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
//判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)表示前面已经没有了,但是还是没有找到
cmp p12, p10 // wrap if bucket == buckets
////如果等于,跳转至第3步
b.eq 3f
//从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//跳转至第1步,继续对比 sel 与 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached
JumpMiss $0
.endmacro
源码解析
- 获取
cache
- 获取
通过 isa
首地址平移 16 字节(在 objc_class 结构中,isa
占8字节,superClass
占8字节) ,即 p11 = cache
-
- 计算下标
- 通过上一步获取的
cache
&(与上)掩码(0x0000ffffffffffff)的到buckets
,(cache 首地址存储的是 ‘高16位存mask,低48位存buckets’ ),
即 p10 = buckets - 将
cache
右移48位,得到mask
,即 p11 = mask - p1 是
objc_msgSend
的第二个参数(_cmd),p1 & mask,通过哈希算法,得到需要查找存储 sel-imp 的 bucket 下标 index(在存储 sel-imp 时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取)
-
- 根据
下标
和buckets
去除对应的bucket
- 一个
bucket
实际占用16字节(sel 占8字节,imp 占8字节),左移4位(2^4 = 16) - 通过 首地址 + 实际偏移量,获取哈希下标 index 对应的 bucket
- 根据
- 根据获取的
bucket
,取出其中的imp
存入p17,即 p17 = imp,取出sel
存入 p9,即 p9 = sel
- 根据获取的
-
- 第一次递归循环
- 比较获取的
bucket
中sel
与objc_msgSend
的第二个参数的_cmd
(即p1)是否相等 - 相等,直接跳转至
CacheHit
,即缓存命中,返回 imp - 不相等,分两种情况
- 一直都找不到,直接跳转至 CheckMiss,进入慢速查找流程
- 如果根据 index 获取的
bucket
等于buckets
的第一个元素,则人为的将当前bucket
设置为buckets
的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker
的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环) - 如果当前
bucket
不等于buckets
的第一个元素,则继续向前查找,进入第一次递归循环
-
- 第二次递归循环
- 重复
5
的操作,与第一次递归唯一区别是,如果当前的bucket
还是等于buckets
的第一个元素,则直接跳转至JumpMiss
,进入慢速查找流程
整体流程图