前言
书接上回cache_t缓存流程分析,我们知道方法的最终insert在_buckets(模拟器)
或_maskAndBuckets(arm64真机)
中,这是方法的存储流程
,那么方法的读取流程
是怎么样的?今天我们通过方法的调用objc_msgSend
一起来探究下方法的读取流程
。
1.Runtime知识点
我们都知道,OC这门编程语言,与其它语言不同,具有Runtime运行时
这一特殊的能力,那么什么是运行时
呢?先看看下面这个示例:
@interface LGPerson : NSObject
- (void)sayHello;
@end
----------------------------分割线----------------------------
@implementation LGPerson
- (void)sayHello{
NSLog(@"LGPerson say : %s",__func__);
}
@end
在Objective-C里面,方法的调用大致有三种方式:
- OC方式:先初始化一个实例
LGPerson person = [[LGPerson alloc] init];
,直接调用[person sayHello];
- NSObject方式:通过
performSelector
[person performSelector:@selector(sayHello)];
; - 底层Runtime方式:通过
objc_msgSend
((id(*)(struct objc_object *, SEL))objc_msgSend)((__bridge struct objc_object *)(person), @selector(sayHello));
调用代码如下:
由此可见,方法的调用既能通过对象
直接调用,也能通过NSObject
的performSelector
,还能通过更底层的objc_msgSend
,后面2个方式根本就不是类LGPerson
里声明的方法,但是却能触发sayHello
,很神奇,这个就是Runtime运行时
的一个特点。
1.1 Runtime概念
什么是Runtime运行时
?得和编译时
区分来说:
- 运行时是
代码跑起来,被装载到内存中的过程
,如果出错,则程序会崩溃,是一个动态的阶段。 - 编译时是
源代码翻译成机器能识别的代码的过程
,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段。
1.2 Runtime结构图
方法调用
上述示例我们见证了Runtime
的特点,那么方法调用时,调用的是底层c/c++
的哪个函数呢?我们可以通过clang指令
将OC的.m
文件编译生成.cpp
,看看对应的c++代码
,例如:
clang -rewrite-objc main.m -o main.cpp
在生成的main.cpp
中,搜索到的main方法
就是:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)person, sel_registerName("performSelector:"), sel_registerName("sayHello"));
((id(*)(struct objc_object *, SEL))objc_msgSend)((__bridge struct objc_object *)(person), sel_registerName("sayHello"));
}
return 0;
}
上面可见,[person sayHello]
和 performSelector
底层都是通过调用objc_msgSend
,跟之前搜索objc_object
或cache_t
一样,在源码工程全局搜索objc_msgSend
,找一找方法的实现,根本找不到。既然c/c++
层搜不到,那我们进入更底层汇编
层,再看看,发现了
汇编走流程
下面我们以真机arm64为例,看看objc_msgSend
汇编代码的大致流程。代码很长,我们分为一段段的看:
section 1
#if SUPPORT_TAGGED_POINTERS
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
#endif
其中SUPPORT_TAGGED_POINTERS
宏定义的解释:
// Define SUPPORT_TAGGED_POINTERS=1 to enable tagged pointer objects
// Be sure to edit tagged pointer SPI in objc-internal.h as well.
#if !(__OBJC2__ && __LP64__)
# define SUPPORT_TAGGED_POINTERS 0
#else
# define SUPPORT_TAGGED_POINTERS 1
#endif
因为是真机__LP64__
,所以值为0,后面的情况都不考虑SUPPORT_TAGGED_POINTERS
。
section 2
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
b.eq LReturnZero
-
ENTRY
进入send -
UNWIND _objc_msgSend, NoFrame
可忽略,哈哈 -
cmp p0, #0
-->cmp是compare比较的意思,p0是第一个入参,#0代表nil,这句意思就是判断p0是否为nil,那第一个入参是什么?根据方法声明:
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************/
p0
就是self
,可以理解是消息的接收者
-
b.eq LReturnZero
如果没有接收者,则执行LReturnZero
,直接终止流程return
section 3
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
-
ldr p13, [x0]
,根据后面的注释知道,p13指向对象的isa指针
,ldr
表示读取寄存器 -
GetClassFromIsa_p16 p13
,同理根据注释,p16指向类class
-
LGetIsaDone:
找到类class
地址完成后 -
CacheLookup NORMAL, _objc_msgSend
跳转到了CacheLookup NORMAL
流程
CacheLookup流程
搜索CacheLookup
,得到.macro CacheLookup
,这是定义的地方,详细代码也分片段释义:
CacheLookup --1
.macro CacheLookup
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
-
ldr p11, [x16, #CACHE]
读取[x16, #CACHE]
,存入p11-
[x16, #CACHE]
根据注释知道p16 = isa
,应该是根据isa,指针偏移找到cache_t
- 根据注释
p11 = mask|buckets
,因为amr64情况下cache_t结构体
成员是explicit_atomic
_maskAndBuckets;
-
- 只看
CACHE_MASK_STORAGE_HIGH_16
64位真机的情况: -
and p10, p11, #0x0000ffffffffffff
-
p11, #0x0000ffffffffffff
--> p11 & #0x0000ffffffffffff,将16进制转10进制,意思就是前16位值为0,后48位值为1,与p11与运算
,可以得到后48位的值 - 将后48位的值存入p10,那么
p10 = buckets()
-
-
and p12, p1, p11, LSR #48
-
p11, LSR #48
LSR表示逻辑右移,将p11逻辑右移48位,得到前16位,那么就是mask
, -
p1, mask
-->因为p1 = SEL
汇编对应的是_cmd
,所以就是_cmd & mask
- 最后就是
p12 = (_cmd & mask)
,其实对应的就是方法cache_hash
,得到哈希关键值key -->哈希下标索引值
,
static inline mask_t cache_hash(SEL sel, mask_t mask) { return (mask_t)(uintptr_t)sel & mask; }
-
CacheLookup --2
add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12]
-
p12, LSL #(1+PTRSHIFT)
:首先LSL表示逻辑左移, 搜索PTRSHIFT如下,那么#(1+PTRSHIFT)表示逻辑左移1+3 = 4位,那么p12 = ((_cmd & mask) << 4)
-->哈希下标值左移4位,内存平移2^4 = 16
-->平移了一个bucket大小(_sel + _imp = 16)的下标
#if __LP64__
// true arm64
#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<
- 然后
add p12, p10, p12, LSL #(1+PTRSHIFT)
-- > 因为p10 = buckets()
-->buckets()+平移了一个bucket的下标
-->获取到这个bucket的值,存入p12
-
ldp p17, p9, [x12]
读取p12,将_imp->p17, _sel->p9
CacheLookup --3 遍历
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
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
- 比较
bucket->sel 和_cmd
,不等去第2步;相等CacheHit $0(缓存命中,返回)
- 循环遍历:
-
CheckMiss $0
如果从最后一个元素遍历过来都找到不到,就返回CheckMiss NORMAL
,定义如下,进入__objc_msgSend_uncached(慢速查找流程)
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
- 否则比较
p12(当前的bucket)
和p10(第一个bucket)
,如果相等去第3步,不等则ldp p17, p9, [x12, #-BUCKET_SIZE]!
-->p12(当前的bucket)内存向前偏移一个bucket大小
-->偏移后即前一个位置的bucket,还是将_imp->p17, _sel->p9
- 然后
b 1b
进入循环。
-
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
-->p11(maskBuckets())
右移48-(1+3)=44位,再跟第一次通过哈希算法的得到的下标p12,再次进行哈希算法 -->得到的是cache_t中的最后一个bucket。
CacheLookup --4
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
- 重复CacheLookup --3 遍历的流程,唯一区别是
当前指向的bucket == buckets时
-->JumpMiss $0
-->__objc_msgSend_uncached(慢速查找流程)
- JumpMiss流程如下:
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
此时$0 = NORMAL,进入__objc_msgSend_uncached
流程-->慢速查找流程
(后面分析)
总结
以上通过对objc_msgSend
汇编代码的流程分部解读,大致了解了,方法调用是如何从cache_t
中遍历寻找imp的一个过程,流程图如下: