[OC学习笔记]objc_msgSend:方法快速查找
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]objc_msgSend:方法快速查找相关的知识,希望对你有一定的参考价值。
一、OC runtime运行时
在探索objc_msgSend
的时候,我们需要先了解OC的runtime机制。
(一)runtime简介
runtime 是 OC底层的一套C/C++的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码。
(二)runtime 交互的三种方式
Objective-C语言与runtime系统的交互主要通过三个不同的层次:
- 通过Objective-C 源码
比如直接调用方法[self say]
等 - 通过定义在Foundation framework 中的NSObject类方法
比如NSSelectorFromString
、isKindeofClass
、isMemberOfClass
等方法。 - 通过直接调用
runtime
函数
比如sel_registerName
、class_getInstanceSize
等底层方法。
二、探索OC方法本质
(一)先手准备
我们先常规的去创建一个类:
@interface Person : NSObject
+ (void)whoAmI;
- (void)sayHello;
- (void)sayMarvelous;
@end
在main.m文件中:
Person *person = [Person alloc];
[person sayHello];
接下来看一下编译后的c++代码:
int main(int argc, const char * argv[])
/* @autoreleasepool */ __AtAutoreleasePool __autoreleasepool;
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
return 0;
(二)仔细分析
1. cpp代码:
- 在调用
alloc
、sayHello
时,都调用了objc_msgSend
方法,意思是objc消息发送
objc_getClass(“Person”)
获取Person
类sel_registerName(“XXX”)
调用方法,类似于@selector
、NSSelectorFromString()
2. 使用objc_msgSend
方法:
int main(int argc, const char * argv[])
@autoreleasepool
// Person *person = [Person alloc];
// [person sayHello];
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
return 0;
输出:
Hello!
证明成功调用了sayHello
方法。
(三)继续探究
我们创建一个新类,继承于Person
,叫它Student
。让它调用父类的方法:
Student *stu = [Student alloc];
[stu sayHello];
转成源码:
Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("sayHello"));
实际还是调用了objc_msgSend
。那么,有没有方法区分调用的是父类方法还是自己的方法呢?通过查阅资料,我知道了objc_msgSendSuper
方法,立即去试一试:
Student *stu = [Student alloc];
//[stu sayHello];
struct objc_super lsuper;
lsuper.receiver = stu;
lsuper.super_class = [Person class];
((void (*)(id, SEL))(void *)objc_msgSendSuper)((__bridge id)(&lsuper), sel_registerName("sayHello"));
结果:
Hello!
(四)小结
- 方法的本质:发送消息
- OC调用方法等价于
runtime
中objc_msgSend
和objc_msgSendSuper
消息发送
三、objc_msgSend
在objc4源码中我们会发现objc_msgSend
是使用汇编实现的,汇编主要的特性是:
- 速度快:汇编更容易被机器识别。
- 方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息。
(一)消息查找机制
- 快速查找:
cache
中查找 - 慢速查找:
a.methodList
中查找
b. 消息转发
(二)在cache中快速查找
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("sayHello"));
在前面我们也看到了,objc_msgSend
需要传入两个参数。此外,如果方法本身有参数,会把本身的参数拼接到这两个参数后面。
在objc源码的objc-msg-arm64.s
中,可以看到下面部分:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0 是传入的第一个参数:消息的接收者。
//cmp p0与nil比较,如果p0为空,那么就直接返回。
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//小对象类型(是否是taggedPointer类型的指针)
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//消息接收者为空,返回空
b.eq LReturnZero
#endif
//消息接收者为不空
//p13 是获取消息接收者的isa
ldr p13, [x0] // p13 = isa
//p16 是根据isa p13获取到Class
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 在cache中开始找imp
// 如果有就调用,如果没有走objc_msg_uncached分支
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
接下来看下CacheLookup
源码,我们在objc-msg-arm64.s
中找.macro CacheLookup
:
.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.
// 一旦我们越过LLookupStartFunction标签,我们可能已经加载了无效的缓存指针或掩码。
// 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:
// 当我们通过LLookupEndFunction之前调用task_restartable_ranges_synchronize
// (或当信号击中我们时),我们的PC将被重置为LLookupRecoverFunction,
// 该函数会强制跳转到具有以下要求的缓存未命中代码路径:
// 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 // stash the original isa 藏匿原始的isa
LLookupStart\\Function:
// p1 = SEL, p16 = isa
//便于观察。将前面的#define搬过来了,对照着看一下:
/*
#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4
*/
/*
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
*/
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
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 // 64位真机⚠️⚠️⚠️
ldr p11, [x16, #CACHE] // p11 = mask|buckets isa 平移16字节得到 cache_t,cache首地址是mask_buckets
#if CONFIG_USE_PREOPT_CACHES // p11 = _bucketsAndMaybeMask,即cache的第一个8字节
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else// _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位) p10 = buckets
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
add p13, p10, p12, LSL #(1+PTRSHIFT)// p12 逻辑左移4位即扩大16倍,指针平移到对应的bucket位置上
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // p13 指向哈希下标对应的bucket
// insert bucket的时候,do-while写入,哈希和二次哈希,读取的时候也是do-while读取cache
// do // p17 = imp, p9 = sel,bucket中imp和sel分别赋给p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // imp, sel = *bucket-- //赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket
cmp p9, p1 // if (sel != _cmd) //获取的sel和_cmd,如果不相等,调整到3f
b.ne 3f // scan more
// else
2: CacheHit \\Mode // hit: call or return imp //获取的sel和_cmd,如果相等,缓存命中,call or return imp
//
3: cbz p9, \\MissLabelDynamic // if (sel == 0) goto Miss;// 如果取出的sel位nil,则goto Miss
cmp p13, p10 // while (bucket >= buckets) //如果bucket >= buckets,即没有到最前面
b.hs 1b // 则继续比较前一个bucket,如果到最前面了,就继续执行后续代码
// 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.
// 当CACHE_ALLOW_FULL_UTILIZATION时可能会发生完全缓存。
// 因此,当我们循环回到第一个探测的bucket时停止,而不是再次击中第一个bucket时。
// 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))//p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
// 再左移4位获取到mask指向的bucket,相当于p11右移了44位
// bucket >= buckets,再次从最后到最前面进行一次do-while循环查找
#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-- // 这里重复1:标签,从mask--->0 查找,从后到前查找
cmp p9, p1 // if (sel == _cmd) // 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑
b.eq 2b // goto hit
cmp p9, #0 // while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\\Function:
LLookupRecover\\Function:
b \\MissLabelDynamic
...
.endmacro
我也不会汇编语言,只能看着源码给的注释和别人的注释理解。主要分为下面几步:
1. 流程:
1.1 获取到指向 cache 和 _bucketsAndMaybeMask
在前面类的结构分析文章中我们清楚的知晓objc_class
的属性为:isa
、superClass
、cache
等:
struct objc_class : objc_object
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
通过p16 = class = isa
,首地址平移16字节(因为在objc_class
中,首地址距离cache
正好16字节,即isa
首地址 占8字节,superClass
占8字节),获取cahce
,p11
指向cache
中第一个8字节_bucketsAndMaybeMask
, _bucketsAndMaybeMask
中高16位存mask
,低48位存buckets
(高16位 | 低48位 = mask | buckets), _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位)
,即p11 = _bucketsAndMaybeMask
1.2 从 _bucketsAndMaybeMask 中分别取出 buckets 和 mask,并由 mask 根据哈希算法计算出哈希下标
p10
= _bucketsAndMaybeMask
& 0x0000ffffffffffff
= buckets
_bucketsAndMaybeMask >> 48
= mask
p12
= _cmd & mask
= 哈希下标
,记作 begin
将objc_msgSend
的参数p1
(即第二个参数_sel
)& mask
,通过哈希算法,得到需要查找存储sel-imp
的bucket
下标begin
,即p12 = begin = _sel & mask
,因为在存储sel-imp
时,也是通过同样哈希算法计算哈希下标进行存储
static inline mask_t cache_hash(SEL sel, mask_t mask)
return (mask_t)(uintptr_t)sel & mask;
1.3 根据所得的哈希下标 begin 和 buckets 首地址,取出哈希下标对应的 bucket
PTRSHIFT
是一个宏定义,固定值为3:
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
根据计算的哈希下标begin
乘以单个bucket占用的内存大小
,得到buckets首地址
距离begin
下标指向的bucket
在实际内存中的偏移量。通过首地址 + 实际偏移量,获取哈希下标begin
对应的bucket
。bucket
是有sel
和imp
两个属性组成,每个属性都是8个字节的大小,所以bucket
的大小是16
1.4 进入 do-while 循环,根据 bucket 中的 sel 查找
- 将
bucket
中的属性属性imp
和sel
分别赋值为p17 和 p9。
1: ldp p17, p9, [x13], #-BUCKET_SIZE//imp, sel = *bucket--
//赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket
- 将
bucket
中的属性属性imp
和sel
分别赋值为p17 和 p9。
cmp p9, p1
//p9 == p1,缓存命中执行CacheHit
//不相等,执行下面的逻辑
- p9是否为
nil
cbz p9, \\MissLabelDynamic // if (sel == 0) goto Miss;
// 如果p9 == nil,则指向goto Miss,默认没找到,
// 这里忽略了哈希冲突后二次哈希可能导致begin下标和真实写入的index之间存在差异
// 而且初始化或扩容后,里面的bucket都是空的,sel和imp都是nil,直接简单粗暴,即p9指向的sel为nil,则认为丢失,也是为了更快
- p9 != nil,判断p13是否 已经执行到最前面了
cmp p13, p10// while (bucket >= buckets)
// 如果bucket >= buckets,则跳转到第一步,while循环开始,while (bucket < buckets)
// while循环结束,依然没有找到,则跳转到最后的bucket,即mask下标所指向的bucket,从后到前再次查找一遍
- begin --> 0,依然没有找到,跳转到最后,mask指向的bucket
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
// p13 = buckets + (mask << 1+PTRSHIFT) 指向最后的bucket
// 正常是p11右移48位获取到mask,再左移4位,相当于_bucketsAndMaybeMask右移44位
// 此时p13,指向最后的bucket,while循环,跳转到第一步
小结:
- 第一次
do-while
循环,从begin ---> 0
查找一遍,如果没命中,p9不为nil,开始第二次do-while
循环 - 第二次
do-while
循环,从mask ---> 0
再次查找一遍, - 依然如此,则执行
__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward
开始查找方法列表
2. CacheHit
下面我们看一下CacheHit
源码:
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// 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//调用imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP//返回imp
.elseif $0 == LOOKUP// 执行__objc_msgSend_uncached,开始方法列表查找
// 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, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
以上是关于[OC学习笔记]objc_msgSend:方法快速查找的主要内容,如果未能解决你的问题,请参考以下文章
[OC学习笔记]objc_msgSend:动态方法决议和消息转发
iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析