一、Runtime
编译时与Runtime
运行时
- 编译时:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码翻译成机器能够识别的代码。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查。
Runtime
运行时:代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断。
Runtime
版本
Runtime
有两个版本: 一个Legacy
版本(早期版本),一个Modern
版本(现行版本)
Legacy
早期版本对应的编程接口:Objective-C 1.0
,用于32位的Mac OS X
的平台Modern
现行版本对应的编程接口:Objective-C 2.0
,源码中经常看到的OBJC2
,用于iPhone
程序和Mac OS X v10.5
及以后的系统中的64位程序
Runtime
调用三种方式
Objective-C
方式:[penson sayNB]
Framework & Serivce
方式:isKindOfClass
Runtime API
方式:class_getInstanceSize
二、方法的本质
方法底层的实现
探究方法的底层有两种方式。第一种汇编,第二种C++
代码。汇编方式的方法的参数需要读寄存器不方便,所以采用第二种方式生成main.cpp
文件。
- 首先自定义
LGPerson类
,在类中添加实例方法,在main
函数中调用
- 代码:
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface LGPerson : NSObject
- (void)sayHello;
- (void)say:(NSString *)str;
@end
@implementation LGPerson
- (void)sayHello{
NSLog(@"666 %s",__func__);
}
- (void)say:(NSString *)str{
NSLog(@"----say %@---",str);
}
@end
@interface LGTeacher: LGPerson
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGTeacher
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
LGTeacher *teach = [LGTeacher alloc];
[teach sayNB];
[teach say:@"runtime -- msg"];
}
return 0;
}
- 在终端使用
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
把main.m
生成main64.cpp
文件,查询main
函数的实现:
- c++代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGTeacher *teach = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teach, sel_registerName("sayNB"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teach, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_gy__p7f70ys54s2c4h_8hjgv8k80000gn_T_main_2163a1_mi_3);
}
return 0;
}
- 结论:
所有的方法调用都是通过objc_msgSend
发送的,所以方法的本质就是消息发送
验证objc_msgSend
发送消息
既然方法调用都是通过objc_msgSend
的,那么我直接通过objc_msgSend
发消息
- 代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
LGTeacher *teach = [LGTeacher alloc];
[teach sayNB];
objc_msgSend((id)teach, sel_registerName("sayNB"));
[teach say:@"runtime -- msg"];
objc_msgSend((id)teach, sel_registerName("say:"),@"runtime -- msg2");
}
return 0;
}
- llvm打印:
2021-07-05 23:16:26.051908+0800 001-运行时感受[7416:606713] 666
2021-07-05 23:16:26.052382+0800 001-运行时感受[7416:606713] 666
2021-07-05 23:16:26.052454+0800 001-运行时感受[7416:606713] ----say runtime -- msg---
2021-07-05 23:16:26.052491+0800 001-运行时感受[7416:606713] ----say runtime -- msg2---
- 结论:
-
通过
objc_msgSend
和[teach sayNB]
、[teach say:@"runtime -- msg"]
无论带不带输入参数,结果是一样的;验证了方法的本质是消息发送。 -
在
main
函数里LGTeacher
的对象是直接调用objc_msgSend
,传入的id
也是一样的LGTeacher
对象teach
,说明objc_msgSend
内部会自动判断当前类class
是否有这个方法;没有就会找到该类的父类看看有没有,有就调用父类方法。 -
在用
objc_msgSend
方式发送消息,验证过程需要注意两点:- 导入消息发送的头文件
#import <objc/message.h>
- 关闭
objc_msgSend
检查机制:target
-->Build Setting
-->搜索objc_msgSend
-->Enable strict checking of objc_msgSend calls
设置为NO
- 导入消息发送的头文件
父类方法[super function]
由于在objc
的对象object
是可以直接使用父类的方法,而调用本类中的方法实际是通过objc_msgSend
发送的,那么在子类复写override
方法时调用父类的方法[super function]
,消息发送是什么样的呢?
- 自定义
LGTeacher
类,LGTeacher
继承LGPerson
类。在LGPerson
类中自定义方法sayHello
,子类LGTeacher
对象调用sayHello
方法。
父类的方法[super function]
的底层
- 在
LGTeacher
中复写调用父类LGPerson
方法[super sayHello]
。
- 代码:
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface LGPerson : NSObject
- (void)sayHello;
- (void)say:(NSString *)str;
@end
@implementation LGPerson
- (void)sayHello{
NSLog(@"666 %s",__func__);
}
- (void)say:(NSString *)str{
NSLog(@"----say %@---",str);
}
@end
@interface LGTeacher : LGPerson
- (void)sayNB;
@end
@implementation LGTeacher
- (void)sayNB{
NSLog(@"666");
}
- (void)sayHello {
[super sayHello];
NSLog(@"7777");
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
LGTeacher * teach = [LGTeacher alloc];
[teach sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
- 使用
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
把main.m
生成mian-arm64.cpp
文件。
c++
的main
方法:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGTeacher * teach = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teach, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_4);
}
return 0;
}
c++
的LGPerson
、LGPerson
的函数:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
// - (void)sayHello;
// - (void)say:(NSString *)str;
/* @end */
// @implementation LGPerson
static void _I_LGPerson_sayHello(LGPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_0,__func__);
}
static void _I_LGPerson_say_(LGPerson * self, SEL _cmd, NSString *str) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_1,str);
}
// @end
- c++的
LGTeacher
和LGTeacher
函数:
#ifndef _REWRITER_typedef_LGTeacher
#define _REWRITER_typedef_LGTeacher
typedef struct objc_object LGTeacher;
typedef struct {} _objc_exc_LGTeacher;
#endif
struct LGTeacher_IMPL {
struct LGPerson_IMPL LGPerson_IVARS;
};
// - (void)sayNB;
/* @end */
// @implementation LGTeacher
static void _I_LGTeacher_sayNB(LGTeacher * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_2);
}
static void _I_LGTeacher_sayHello(LGTeacher * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_3);
}
// @end
- 结论:
- 子类对象可以通过
objc_msgSendSuper
方式调用父类的方法,方法的本质还是消息发送,只不过通过的不同发送流程,同样现在用objc_msgSendSuper
向父类发消息。
objc_msgSendSuper
底层实现
现在回到objc4.866.9
源代码中搜索objc_msgSendSuper
objc
源代码的message.h
:
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
- 结论:
objc_msgSendSuper
的第一个参数是struct objc_super * _Nonnull super
类型,继续在源码中查找objc_super
类型
objc_super
结构体
objc
源代码的message.h
:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};
- 结论:
objc_super
结构体类型里面有两个参数:id receiver
和Class super_class
验证objc_msgSendSuper
方法
在源代码工程中,屏蔽LGTeacher
里的复写方法sayHello
,声明一个objc_super
结构体变量,赋值后验证方法。
- 代码:
#import <objc/message.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
LGTeacher *teach = [LGTeacher alloc];
[teach sayHello];
// struct objc_super {
// /// Specifies an instance of a class.
// __unsafe_unretained _Nonnull id receiver;
// __unsafe_unretained _Nonnull Class super_class;
// #endif
// /* super_class is the first class to search */
// };
struct objc_super kc_objc_super;
kc_objc_super.receiver = teach;
kc_objc_super.super_class = LGPerson.class;
objc_msgSendSuper(&kc_objc_super,@selector(sayHello));
}
return 0;
}
llvm
打印:
2021-07-06 01:29:15.208080+0800 001-运行时感受[9801:834295] 666 -[LGPerson sayHello]
2021-07-06 01:29:15.208545+0800 001-运行时感受[9801:834295] 666 -[LGPerson sayHello]
- 结论:
-
[teach sayHello]
和直接通过objc_msgSendSuper
给父类发消息的是一样的。子类的对象可以调用父类的方法。 -
验证了一个原理: 方法调用,首先在本类中找,如果没有就到父类中找。
三、objc_msgSend
汇编原理
首先找到objc_msgSend
所在的libobic.A.dylib
汇编库,按住control
+单点调试进入objc_msgSend
- 结论:
汇编显示objc_msgSend
在libobjc.A.dylib
系统库,在objc4
源码中全局搜索objc_msgSend
,找到真机的汇编objc-msg-arm64.s
objc_msgSend汇编入口
- 通过查找
objc_msgSend
的入口ENTERY
发现了
- 通过
p0
的定义搜索头文件arm64-asm.h
发现了
- 结论:
下面的汇编会用到p0-p17
;大家可能对汇编中x0
,x1
比较熟悉,知道是寄存器。p0-p17
就是对x0-x17
重新定义。
分析MSG_ENTRY _objc_msgSend
- 汇编源代码:
MSG_ENTRY _objc_msgSend // _objc_msgSend 入口,此时有两个输入参数:(isa)id receiver 还有一个是SEL _cmd
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // receiver 和 0 比较 nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //__LP64__ 64位系统 支持Taggedpointer类型
b.le LNilOrTagged // <= 0时,支持Taggedpointer类型 走LNilOrTagged流程 (MSB tagged pointer looks negative)
#else
b.eq LReturnZero // == 0时,直接返回nil 就是给空指针发送消息
#endif // 对象有值或者isa有值
ldr p14, [x0] // p14 = isa 把x0寄存器里面的地址读取到p14寄存器,对象地址等于isa地址
GetClassFromIsa_p16 p14, 1, x0 // p16 = class // p14,1,x0作为参数传到GetClassFromIsa_p16方法里
LGetIsaDone: //这是一个标记符号,拿到isa操作完以后继续后面的操作
// calls imp or objc_msgSend_uncached //这个函数传递3个参数 NORMAL _objc_msgSend _objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached //下一步调用CacheLookup
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//== 0 直接返回nil
b.eq LReturnZero // nil check
GetTaggedClass
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
//结束进入_objc_msgSend
END_ENTRY _objc_msgSend
- 结论:
-
判断
receiver
是否等于nil
,再判断是否支持Taggedpointer
小对象类型 -
支持
Taggedpointer
小对象类型;小对象为空,返回nil
;不为nil
处理isa
获取class
跳转CacheLookup
流程 -
不支持
Taggedpointer
小对象类型且receiver == nil
,跳转LReturnZero
流程返回nil
-
不支持
Taggedpointer
小对象类型且receiver != nil
,通过GetClassFromIsa_p16
把获取到class
存放在p16
的寄存器中,然后走CacheLookup
流程
GetClassFromIsa_p16
获取Class
- 汇编源代码:
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // 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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
- 结论:
GetClassFromIsa_p16
核心功能获取class
存放在p16
寄存器
ExtractISA
- 在汇编
arm-asm.h
里:
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
...
.macro ExtractISA
and $0, $1, #ISA_MASK // and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
.endmacro
// not JOP
#endif
- 结论:
ExtractISA
主要功能 isa & ISA_MASK = class
存放到p16
寄存器
CacheLookup
流程
buckets
和下标index
- 汇编源代码:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function 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\Function,
// then our PC will be reset to LLookupRecover\Function 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
//
mov x15, x16 // x15 = x16的值是class; stash the original isa
LLookupStart\Function:
// p0 = 接受对象, p1 = SEL, p16 = isa(这里的isa是类的isa)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // arm64的模拟器
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // arm64真机
//define CACHE (2 * __SIZEOF_POINTER__)
//[x16, #CACHE] = isa + 0x10 = cache地址 = _bucketsAndMaybeMask地址
//将_bucketsAndMaybeMask的地址存放到p11寄存器 p11 = mask|buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES // arm64真机
#if __has_feature(ptrauth_calls) // A12以上
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//tbnz p11, #0; 表示若_bucketsAndMaybeMask第0位!=0,则跳转到LLookupPreopt\Function
tbnz p11, #0, LLookupPreopt\Function
#endif
//p1 = _cmd, eor是异或,p12 = p1 ^(p1 >> 7) = _cmd ^ (_cmd >> 7)
eor p12, p1, p1, LSR #7
//p11 >> 48 = _bucketsAndMaybeMask >> 48 = mask
//p12 = p12 & (p11 >> 48) = _cmd ^ (_cmd >> 7) & mask
//p12 = (_cmd ^ (_cmd >> 7)) & mask 这一步的作用就是hash求下标index
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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
- 结论:
首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了三件事:
-
获取
_bucketsAndMaybeMask
地址也就是cache
的地址:p16 = isa(class)
,p16 + 0x10 = _bucketsAndMaybeMask = p11
-
获取
buckets
地址就是缓存内存的首地址:buckets =((_bucketsAndMaybeMask >> 48 )- 1 )
-
获取
hash
下标:p12 =(cmd ^ ( _cmd >> 7))& mask
这一步的作用就是获取hash
下标index
流程如下:isa
--> _bucketsAndMaybeMask
--> buckets
-->hash
下标
遍历缓存
- 汇编源代码:
// p12是哈希下标,p10是buckets的首地址
//((_cmd & mask) << (1+PTRSHIFT)),就是把p12(hash值下标)左移动4位(也就是*16),得到实际偏移大小,通过buckets(首地址)+偏移地址得到index所对应bucket,bucket的结构为{sel,imp}
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- 先读取bucket,再减减
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
- 结论:
-
根据
hash
下标index
找到index
对应的bucket
。p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
-
先获取对应的
bucket
然后取出imp
和sel
存放到p17
和p9
,然后*bucket--
向前移动- 1流程:
p9 = sel
和传入的参数_cmd
进行比较。如果相等走2流程,如果不相等走3流程 - 2流程:缓存命中直接跳转
CacheHit
流程 - 3流程:判断
sel == 0
条件是否成?如果成立说明buckets
里面没有传入的参数_cmd
的缓存,没必要往下走直接跳转__objc_msgSend_uncached
流程。如果sel = 0
说明这个bucket
被别的方法占用了,去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket
和第一个bucket
地址大小,如果大于第一个bucket
的地址跳转1流程循环查找,如果小于等于则接继续后面的流程
- 1流程:
-
如果循环到第1个
bucket
里都没有找到符合的_cmd
,那么会接着离开查找缓存往下走__objc_msgSend_uncached
。
CacheHit
流程
CacheHit \Mode
的Mode = NORMAL
:
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
TailCallCachedImp
是一个宏,宏定义如下:
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class类(也就是isa)
eor $0, $0, $3 // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
br $0 //调用 imp 执行方法
.endmacro
...
#endif
- 结论:
缓存查询到以后直接对bucket
的imp
进行解码操作。即imp = imp ^ class
,然后调用解码后的imp
。
遍历缓存流程图
疑问:为什么要判断bucket
中的sel = 0
,等于0
直接查找缓存流程就结束了?
- 如果既没有
hash
冲突又没有目标方法的缓存,那么hash下标
对应的bucket
就是空的直接跳出缓存查找 不会出现中间是有空的bucket
,两边有目标bucket
这种情况。
mask向前遍历缓存
向前遍历缓存没有查询到就会跳转到mask
对应的bucket
继续向前查找i
。
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // 模拟器
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // arm64 真机,p11 = _bucketsAndMaybeMask,p10 = buckets
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p11, LSR #(48 - (1+PTRSHIF)) = p11 >> 48 再 << 4位
// p11 >> 48 = mask, mask = capacity - 1 //开辟的内存的容量-1,buckets + mask << 4 找到最后一个bucket位置
// p13 = buckets + (mask << 1+PTRSHIFT) 找到最后一个bucket的位置
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 // 32位老机型,不考虑
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
//p12 = (_cmd ^ (_cmd >> 7)) & mask -> 下标index刚开始确认的下标,p10 = buckets
add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = p10 + p12 >> 4 => buckets + ((_cmd ^ (_cmd >> 7)) & mask) << 4找到下标index对应的bucket的地址
// p12 = first probed bucket // 第一次定位的bucket地址
//首先先取出x13对应bucket的imp和sel存放到 p17 和 p9 寄存器
//然后x13=*buckets-- 最后一个bucket默认放的是imp = buckets sel = 1
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit // 如果相等就是找到缓存 跳转到2流程 缓存命中
cmp p9, #0 // } while (sel != 0 && // sel和0比较
ccmp p13, p12, #0, ne // bucket > first_probed) // bucket和第一次定位的bucket地址 比较且ne = (sel != 0)
b.hi 4b // hi表示无符号大于0 bucket 大于 第一次定位的bucket的地址 跳转4流程
// 如果一直找到 直到bucket最后一个 还没有找到就跑下面代码
LLookupEnd\Function: // 查询结果
LLookupRecover\Function:
b \MissLabelDynamic // 没有找到,就走__objc_msgSend_uncached
- 结论:
-
找到最后一个
bucket
的位置:p13 = buckets + (mask << 1+3)
找到最后一个bucket
的位置 -
先获取对应的
bucket
然后取出imp
和sel
存放到p17
和p9
,然后*bucket--
向前移动 -
p9 = sel
和 传入的参数_cmd
进行比较。如果相等走2流程 -
如果不相等再判断
(sel != 0 && bucket > 第一次确定的hash下标bucket)
接着循环缓存查找,如果整个流程循环完仍然没有查询到或者遇到空的bucket
;说明该缓存中没有缓存)sel = _cmd
的方法,缓存查询结束跳转__objc_msgSend_uncached
流程 -
mask
向前遍历和前面的循环遍历逻辑基本一样
伪代码
- 整个
objc_msgsend
消息发送的伪代码流程:
//
// objc_msgSend伪代码实现.c
// 001-运行时感受
//
// Created by cooci on 2021/3/18.
//
#include <stdio.h>
[person sayHello] -> imp ( cache -> bucket (sel imp))
// 获取当前的对象
id person = 0x10000
// 获取isa
isa_t isa = 0x000000
// isa -> class -> cache
cache_t cache = isa + 16字节
// arm64
// mask|buckets 在一起的
buckets = cache & 0x0000ffffffffffff
// 获取mask
mask = cache LSR #48
// 下标 = mask & sel
index = mask & p1
// bucket 从 buckets 遍历的开始 (起始查询的bucket)
bucket = buckets + index * 16 (sel imp = 16)
int count = 0
// CheckMiss $0
do{
if ((bucket == buckets) && (count == 0)){ // 进入第二层判断
// bucket == 第一个元素
// bucket人为设置到最后一个元素
bucket = buckets + mask * 16
count++;
}else if (count == 1) goto CheckMiss
// {imp, sel} = *--bucket
// 缓存的查找的顺序是: 向前查找
bucket--;
imp = bucket.imp;
sel = bucket.sel;
}while (bucket.sel != _cmd) // // bucket里面的sel 是否匹配_cmd
// CacheHit $0
return imp
CheckMiss:
CheckMiss(normal)
缓存查询流程图
- 流程图:
graph TB
A[objc_msgSend] -->B{cmp p0, #0<br>判断接受者是否存在?<br>}
B -->|否| D[通过对象isa获取class]
B -->|是| C{是否支持<br>taggegpointer对象}
C -->|是| E{小对象或者空 <br>判断LNilOrTagged<br>}
D --> G[获取isa完毕 LGetIsaDone]
E -->|小对象Isa处理| G
C -->|否| F[直接返回空LReturnZero]
E -->|空| F
G --> H[开去缓存查找流程 <br>CacheLookup NORMAL<br>]
H --> I[通过类的指针平移16得到<br>cacheldr p11,x16 #CACHE<br>]
I --> J[通过获取的mask_buckets掩码运算得到<br>bucketsand p10,p11, #0x0000ffffffffffff<br>]
I --> K1[通过逻辑右移得到<br>mask p11, LSR #48<br>]
K1 --> K2[通过and p12, p1 计算哈希函数<br>得到下标 _cmd &mask<br>]
J --> K["p12=buckets+((_cmd & mask)<<(1+PTRSHIFT))<br>通过内存平移得到bucket<br>"]
K2 --> K
K -->|第一次通过下标向前查找| L["通过bucket结构体得到<br>{imp,sel}=*bucket--<br>"]
L --> M{判断要查询sel和当前_cmd<br>是否相等<br>}
M -->|是| N[命中缓存CacheHit NORMAL]
M -->|否| Q{"sel是否为0(nil)"}
Q -->|是| W["找不到缓存sel或者结束循环<br>进入慢速查找_objc_msgSend_uncache"]
Q -->|否| O{"判断当前查询bucket是否为<br>第一个元素<br>bucket == buckets<br>"}
O -->|是| P["把当前查询bucket设置为最后一个元素<br>p12 = buckets + (mask << 1+PTRSHIFT)<br>"]
O -->|否| L
P --> P1["计算出下标index所在bucket(first_probed)"]
P1 -->|第二次从buckets最后开始向前查找| L1["通过bucket结构体得到
{imp,sel}=*bucket--"]
L1 --> M1{判断要查询sel和当前_cmd<br>是否相等<br>}
M1 -->|是| N
M1 -->|否| O1{"判断当前查询sel是存在<br>且当前bucket大于first_probed"}
O1 -->|否| W
O1 -->|是| L1
- 总结:
-
由于上一节
cache_t
引入的objc_msgSend
的分析,既然会有这么多内容,说明苹果底层是用了大量方式方法才保证了objc运行时
与发送消息
的稳定性。 -
了解其中的实现方式有利于学习苹果架构原理.