objc_msgsend
runtime运行时
编译时:顾名思义就是正在编译的时候,把源代码编译成机器能识别的语言,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段。
command + b
。-
运行时:就是代码跑起来被装载到内存中去了,如果此时出错,程序会崩溃,是一个动态的阶段。
command + R
runtime
:一套由C/C++``汇编
写成的为我们OC
提供运行时
功能的API
。现行对应的编程接口:Objective2.0
。runtime的使用有以下三种方式,其中三种实现方法与编译层和底层的关系图,如下图所示:
通过
OC
代码,如直接调用方法[LGPerson sayHello]
等。通过
NSObject
方法,如isKindOfClass
,isMemberOfClass
等。通过
runtim API
,如class_getInstanceSize
等底层方法。
其中Compiler
就是我们了解的编译器,即LLVM
,runtime System Library
--底层库。
探索OC方法本质
- 1、新建一个Person类,并声明一个实例方法。
@interface Person : NSObject
-(void)eat;
@end
@implementation Person
-(void)eat {
NSLog(@"%s",__func__);
}
@end
- 2、在main.h文件中,初始化Person,并调用eat方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person * p = [Person alloc];
[p eat];
NSLog(@"Hello, World!");
}
return 0;
}
- 3、执行clang,将main.m文件编译成main.cpp文件,在main.cpp文件中我们找到main函数。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jf_j8m194qs4517fp_kg01ywhhh0000gn_T_main_b774b2_mi_0);
Person * p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
}
return 0;
}
分析main.cpp
文件的main
函数我们发现,在调用alloc
,eat
方法时都使用了objc_msgSend
这个方法,消息发送。然后通过runtime
直接在main
函数使用这个objc_msgSend
方式调用一下eat
方法,看能否打印出结果。
1、直接调用objc_msgSend
,需要导入头文件#import <objc/message.h>
。
2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改为NO
,将严厉的检查机制关掉,否则objc_msgSend
的参数会报错。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person * p = [Person alloc];
[p eat];
objc_msgSend((id)p, sel_registerName("eat"));
NSLog(@"Hello, World!");
}
return 0;
}
运行打印结果如下:
调用父类方法
新建Teacher类继承于Person类。
#import "Person.h"
@interface Teacher : Person
@end
@implementation Teacher
@end
在main.m文件中,使用声明并teacher,并调用eat方法;使用objc_msgSendSuper调用eat方法。
struct objc_super teacher;
Teacher * t = [Teacher alloc];
[t eat];
teacher.receiver = t;
teacher.super_class = [t class];
objc_msgSendSuper(&teacher, sel_registerName("eat"));
打印结果如下:
发现:两种方式都调用了父类Person
的eat
的方法。
方法的本质就是发送消息,OC调用方法等价于runtime中的objc_msgSend和objc_msgSendSuper消息发送
。方法的调用,首先是从类中查找,如果类中没有找到,就会去类的父类中查找。
objc_msgSend快速查找流程分析
在objc4-781
源码中,搜索objc_msgSend
,由于我们日常开发的都是架构是arm64
,所以需要在arm64.s
后缀的文件中查找objc_msgSend
源码实现,发现是汇编实现。
汇编的主要特性:速度快,更容易被机器识别
。方法参数的动态性,汇编调用函数时传递的参数是不确定的
,那么发送消息时,直接调用一个函数就可以发送所有的消息。
快速查找:cache
中查找
慢速查找:methodList
中查找,消息转发
流程如下图:
源码分析:
//消息发送 --汇编入口--obj_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//--无窗口
UNWIND _objc_msgSend, NoFrame
//--比较p0和空做对比,判断p0(recevier)消息接收者对象是否为空,其中p0是objec_msgSend的第一个参数-消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
//--le小于--支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)如果支持了tagerpointer类型
#else
//--p0 等于0时 ,直接返回空
b.eq LReturnZero //返回0
#endif
//--p0即receiver 肯定存在流程
//--根据对象拿出isa,即x0寄存器指向的地址 取出isa,存入 p13寄存器
ldr p13, [x0] // p13 = isa 消息存在,拿出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、首先判断p0
是否为空,objc_msgSend
方法的第一个参数receiver
是否为空
* 如果为空:直接返回0 LReturnZero
* 如果支持小对象tagged pointer
,跳转至LNilOrTagged
* 如果存在receiver(p0存在)
,不是小对象,从receiver
中取出isa
存入p13
寄存器,
通过GetClassFromIsa_p16
中,arm64
架构下通过 isa & ISA_MASK
获取shiftcls
位域的类信息,即class
,GetClassFromIsa_p16
的汇编实现如下:
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
//---- 将isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa--判断是否是nonapointer 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:
//--用于64位系统
#elif __LP64__
// 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
// 32-bit raw isa ---- 用于32位系统
mov p16, $0
#endif
.endmacro
2、开启缓存查找流程: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 #CACHE = 2 * pointer->平移16个字节得到cache_t
//_maskAndBuckets 高16位为mask bucket为低48位
//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//-- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--p11(cache) & 0x0000ffffffffffff,mask高16位抹零,得到buckets 存入p10寄存器--即去掉mask,留下buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets 获取buckets
//--p11(cache)右移48位,得到mask(即p11 存储 mask),mask & p1(msgSend的第二个参数cmd-sel),得到sel-imp的下标index(即搜索下标)存入p12(cache insert写入时的哈希下标计算是通过 sel & mask,读取时也需要通过这种方式)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask 逻辑右移 获取mask
//--非64位真机
#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
//--p12是下标,p10是buckets数组首地址,下标 * 1 << 4(即16)得到实际内存的偏移量,通过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(传入的参数_cmd)
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) 是否等于 p0 (bucket是数组的第一个元素),如果等于则跳转至第3步
cmp p12, p10 // wrap if bucket == buckets
//--- 定位到最后一个元素(即第一个bucket)
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)
//--- 如果不相等,即走到第二步
b.ne 2f // scan more
//--- 如果相等 即命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果一直找不到,则CheckMiss
CheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f //如果等于,跳转至第3步
//--- 从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
汇编流程分析:
LLookupStart$1
:-->很好解释,就是开始查找
ldr p11, [x16, #CACHE]
-->从x16(即isa)
中平移16
字节,取出cache
存入p11
寄存器 -- isa
距离cache
正好16
字节:isa(8字节)
-superClass(8字节)
-cache(mask高16位 + buckets低48位)
and p10, p11, #0x0000ffffffffffff
-->p11(cache) & 0x0000ffffffffffff
,mask
高16
位抹零
,得到buckets
存入p10
寄存器--即去掉mask
,留下buckets
。
and p12, p1, p11, LSR #48
-->p11(cache)右移48位
,得到mask
(即p11
存储 mask
),mask & p1
(msgSend
的第二个参数cmd-sel
),得到sel-imp
的下标index
(即搜索下标)存入p12
(cache insert
写入时的哈希下标计算是通过sel & mask
,读取时也需要通过这种方式)
add p12, p10, p12, LSL #(1+PTRSHIFT)
-->p10
上面说了是bucket
的首位,将其左移p12*4
位(p12
是传进来方法,通过哈希算法得到的下标,每个bucket包含sel以及imp,所以每个bucket是16字节
,左移4位,就是左移16字节,下标乘于16
,就拿到cache_t
这个下标的bucket
。)
1:方法
ldp p17, p9, [x12]
-->通过bucket
的结构体得到{imp, sel} = *bucket
cmp p9, p1
-->是取的sel
跟p1
传进来的_cmd
(就是传进来的sel
)不相等
b.ne 2f
-->如果不相等,进入2
CacheHit $0
-->如果相等,返回imp
2:方法:上面查找不相等,下面的方法是递归查找
CheckMiss $0
-->如果从最后一个元素遍历过来都找到不到,就返回CheckMiss
。
cmp p12, p10
-->我们知道p10
是第一个bucket
,p12
是算的下标,这意思(判断下标发现不是第一个)
b.eq 3f
-->如果下标是第一个,走3
ldp p17, p9, [x12, #-BUCKET_SIZE]!
-->如果不是第一个,就向前取bucket
,循环一次对内存偏移-1
,把取的bucket
给p17
b 1b
-->执行1
3:方法:上面发现是第一个bucket
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
-->p11
右移48-(1+3)=44
位,再跟第一次通过哈希算法
的得到的下标p12
,再次进行哈希
算法。这次得到的这个下标是cache_t
的最后
一位。
再查找一遍缓存后面再执行1,2方法。
如果当前的bucket
还是等于 buckets
的第一个元素,则直接跳转至JumpMiss
,此时的$0
是normal
,也是直接跳转至__objc_msgSend_uncached
,即进入慢速查找流程
CheckMiss和JumpMiss的汇编流程
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
//--- 如果为GETIMP ,则跳转至 LGetImpMiss
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.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