objc_msgSend() 快速查找流程
上次objc_class 中 cache 分析 分析了方法
是怎样缓存的,这篇文章分析下方法是怎么被查找的。
1. 编译时和运行时
- 编译时: 即编译器对语言的编译阶段,只是对语言进行最基本的检查报错,包括语法分析、词法分析等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味程序可以成功运行。
- 运行时: 即程序通过了编译这一关后,把编译好的代码装载到内存中跑起来的阶段,这时候会具体对类型进行检查,不仅仅是对代码的简单扫描分析,若出错程序会崩溃。
可以说编译时
是一个静态
的阶段,类型错误很明显可以直接检查出来,而运行时
是动态
阶段,开始具体与运行环境结合。
Runtime 怎么被调用?
- 通过
OC
代码,[person say666]
。 - 通过
NSObject
代码,isKindOfClass()
。 - 通过
Runtime API
,class_getInstanceSize
。
调用关系
compiler
是编译器,runtime system libarary
是底层库
。
1、方法本质
定义一个 LGPerson
类。
@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
- (void)sayHello
{
NSLog(@"hello");
}
@end
main.m
调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
}
return 0;
}
通过clang
编译器指令编译出来main.cpp
文件。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
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))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_93c270_mi_1);
}
return 0;
}
可以看出不管是alloc
还是 sayNB
、sayHello
都被编译为 objc_msgSend()
,可以得出结论方法的本质其实就是 objc_msgSend 消息发送
。
我们来验证下objc_msgSend
效果是否与方法调用一致,加上代码。
objc_msgSend(person,sel_registerName("sayNB"));
objc_msgSend(person, sel_registerName("sayHello"));
Tip:
需要引入 #import <objc/message.h>
打印结果:
修改设置
修改下代码:
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"hello");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// sel_registerName = �@seletor() = NSSeletorFromString()
// 方法: 消息 : (消息的接受者 . 消息主体)
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];
// 消息的接受者其实是 LGTeacher
[person sayHello];
// objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
struct objc_super lgsuper;
lgsuper.receiver = person;
lgsuper.super_class = [LGTeacher class];
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));
}
return 0;
}
打印结果
2、objc_msgSend 快速查找流程源码分析
打开源码工程搜索objc_msgSend
查找objc-msg-arm64.s
,主要分析arm64架构
的实现。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0(receiver)和空对比
cmp p0, #0
//支持taggedpointer(小对象类型)
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// p0 等于 0 时,直接返回 空
b.eq LReturnZero
#endif
// 通过 指针首地址拿到 isa存入p13
ldr p13, [x0] // p13 = isa
//在64位架构下通过 p16 = isa(p13) & ISA_MASK 拿到类信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// 从类的缓存中寻找 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
第一步分析
重点来了!
.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] // #define CACHE (2 * __SIZEOF_POINTER__) 通过首地址 内存偏移(2*8) 找到 cache 首地址,在arm64架构中 cahe首地址为 p11 = _maskAndBuckets.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets 类似isa&ISA_MASK取shiftcls。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask p11 右移48位得到 mask, mask&sel(第二个参数 p1)得到 hash下标 p12。(insert的时候就是这么计算的下标。)
#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(3))) buckets指针地址偏移 下标(一个buket占的大小 16)
ldp p17, p9, [x12] // {imp, sel} = *bucket 从p12中取出 imp 存到 p17 sel存到p9
1: cmp p9, p1 // if (bucket->sel != _cmd) 比较取出的p9和p1是否是相同的sel
b.ne 2f // 没有找到 调到 2
CacheHit $0 // call or return imp 命中返回imp
2: // not hit: p12 = not-hit bucket 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets p12是否等于第一个元素p10
b.eq 3f // 等于跳到 3(第二次递归)
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket // 首地址平移 一个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) p11 右移48为mask, -(4) 为左移 4 相当于乘以16(一个bucket大小),其实为把p12置到最后一个位置。
#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: // 跳转至JumpMiss
JumpMiss $0
.endmacro
//*********************其他声明******************
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.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
流程分析:
通过 p16
(isa)地址平移,isa
+ superClass
= 16
得到 cache
首地址 -->p11
,在arm 64
中是这么储存的,即p11
也为_maskAndBuckets
的首地址。
p11 & #0x0000ffffffffffff
抹掉高 16
位得到后48位即为 -->p10
bukets,p11
右移48
位得到 mask
, mask&sel
(第二个参数 p1
)得到 hash
下标 p12
。(insert
的时候就是这么计算的下标),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT(3)))
buckets
指针地址偏移 下标(一个buket
占的大小 16
),然后从p12
中取出 imp
存到 p17
sel
存到p9
,下面开始循环,用图标的方式说明总结。