iOS runtime之objc_msgSend快速查找流程

1,201 阅读9分钟

前言

前文iOS类的结构之cache_t分析中介绍了cache_t的相关信息,了解了方法缓存的相关知识,本文将对方法缓存的读取存储流程进行探索。

想要探索cache的读写流程可以从我们之前探索过的cache_t::insert函数开始,首先全局搜索cache_t::insert函数,发现了苹果在objc-cache.mm源码里面注释的cache读写流程。

 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
 * cache_t::copyCacheNolock    (caller must hold the lock)
 * cache_t::eraseNolock        (caller must hold the lock)
 * cache_t::collectNolock      (caller must hold the lock)
 * cache_t::insert             (acquires lock)
 * cache_t::destroy            (acquires lock)

图解:

image.png

从这里我们可以看出cache的读取是从objc_msgSend开始的,接下来我们就从objc_msgSend开始探索。

一、runtime简介

探索objc_msgSend之前先简单介绍下runtime

Objective-C 语言是一门动态语言。它把一些决策从编译阶段、链接阶段推迟到运行时阶段,实现该机制的基础就是runtime(又叫作运行时)。

  • 静态语言:在编译阶段就已确定所有变量的数据类型,同时也确定要调用的函数,以及函数的实现。常见的静态语言,如:C/C++、Java和C#等。
  • 动态语言:程序在运行时可以改变其结构。也就是说在运行时检查变量数据类型,同时在运行时才会根据函数名查找要调用的具体函数,如Objective-C

1、runtime是什么

  • runtime是由CC++汇编实现的一套API
  • runtimeObjective-C语言的动态属性提供支持,充当一种用于Objective-C语言的操作系统,使得该语言正常运转工作。
  • runtime为OC语言加入了面向对象、运行时的功能。
  • 平时编写的Objective-C代码,在程序运行过程中,最终会转换成runtime的相关代码-runtimeObjective-C的幕后⼯作者。

2、runtime的版本和平台

Runtime Versions and Platforms

在不同的平台上有不同的Objective-C runtime版本。runtime开源代码

2.1、版本

Objective-C runtime有两个版本:modern(现代版本)legacy(遗留版本)。现代版本是在 Objective-C 2.0中引入的,其中包括许多新功能。旧版运行时的编程接口在 Objective-C 1.0 运行时参考中有所描述;Objective-C Runtime Reference中描述了现代版本的运行时的编程接口。

最值得注意的新功能是现代运行时中的实例变量是non-fragile(非脆弱的):

  • 在旧版运行时中,如果更改类中实例变量的布局,则必须重新编译从其继承的类。
  • 在现代运行时中,如果更改类中实例变量的布局,则不必重新编译从其继承的类。

另外,现代的运行时支持声明的属性的实例变量综合(请参见 The Objective-C Programming LanguageDeclared Properties)。

2.2、平台

  • iPhone应用程序和OS X v10.5及更高版本上的64位程序使用modern版本的runtime
  • 其他程序(OS X桌面上的32位程序)使用legacy版本的runtime

二、runtime的消息机制

OC中方法调用通过runtime实现,runtime进行方法调用最重要的底层本质就是消息机制,下面就来探索runtime的消息机制。

objc_msgSend函数定义:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

1、runtime架构简单介绍

runtime架构.001.jpeg

我们日常写代码在Objective-C Code层,里面有很多的FrameworkServices以及APIRuntime System Libraryruntime底层系统库,通过complier(编译器)在中间层进行转换,对上层Objective-C Code层提供支持。

runtime架构.002.jpeg

2、消息发送的三种方式

2.1、OC层面方法调用

XJPerson *p1 = [XJPerson alloc];
[p1 thatGirlSayToMe:@"I love you"];

************************ 底层实现 ************************

// clang 指令编译成.cpp文件
XJPerson *p1 = ((XJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XJPerson"), sel_registerName("alloc"));

((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p1, sel_registerName("thatGirlSayToMe:"), (NSString *)&__NSConstantStringImpl__var_folders_14_tbs866611ll6tx7n__d__41m0000gn_T_main_267efe_mi_0);

图解:

image.png

2.2、 Framework&Services层方法调用

XJPerson *p = [XJPerson performSelector:@selector(alloc)];

[p performSelector:@selector(thatGirlSayToMe:) withObject:@"I love you"];

************************ 底层实现 ************************

+ (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)((id)self, sel);
}

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

图解:

image.png

image.png

2.3、runtime API调用

XJPerson *p2 = ((XJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XJPerson"), sel_registerName("alloc"));

((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p2, sel_registerName("thatGirlSayToMe:"), @"I love you");

备注:

OC层使用objc_msgSend准备工作:

1、在文件中导入相关API头文件,即#import <objc/message.h>。

2、Xcode设置:target->Build Settings->Apple Clang Preprocessing->Enable Strict Checking of objc_msgSend Calls设置为NO,关闭Clang编译器对objc_msgSend的检查。

3、Xcode 12之后步骤2可能无效,就需要对objc_msgSend进行强转,然后使用。例:((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p2, sel_registerName("thatGirlSayToMe:"), @"I love you");

2.4、代码示例

//OC层方法调用
void methodCall(void) {
    //给XJPerson分配内存
    XJPerson *person = [XJPerson alloc];
    //调用方法:OC
    [person thatGirlSayToMe:@"I love you"];
    //调用方法:Framework
    [person performSelector:@selector(thatGirlSayToMe:) withObject:@"I love you"];
}

//objc_msgSend调用
void objc_msgSendCall(void) {
    //给XJPerson分配内存
    XJPerson *person = ((XJPerson *(*)(id, SEL))(void *)objc_msgSend)(objc_getClass("XJPerson"), sel_registerName("alloc"));
    //调用方法
    ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("thatGirlSayToMe:"), @"I love you");
}

//objc_msgSendSuper调用
void objc_msgSendSuperCall(void) {
    //给XJPerson分配内存
    XJBoy *boy = ((XJBoy *(*)(id, SEL))(void *)objc_msgSend)(objc_getClass("XJBoy"), sel_registerName("alloc"));
    //创建objc_super对象
    struct objc_super boySuper;
    boySuper.receiver = boy;
    
    // * super_class is the first class to search */
    // 这里的super_class可以是当前类XJBoys或者XJPerson,这里的super_class只是指定方法的第一查找类,如果当前类找不到,将会去父类中查找
//    boysSuper.super_class = objc_getClass("XJBoys");
    boySuper.super_class = objc_getClass("XJPerson");
    //调用super方法
    ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSendSuper)((id)&boySuper, sel_registerName("thatGirlSayToMe:"), @"I love you");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        methodCall();
        
        objc_msgSendCall();
        
        objc_msgSendSuperCall();
        
    }
    return 0;
}

************************ 打印输出 ************************

2021-07-01 17:52:26.163465+0800 KCObjcBuild[2748:71486] -[XJPerson thatGirlSayToMe:] I love you
2021-07-01 17:52:26.163880+0800 KCObjcBuild[2748:71486] -[XJPerson thatGirlSayToMe:] I love you
2021-07-01 17:52:26.163944+0800 KCObjcBuild[2748:71486] -[XJPerson thatGirlSayToMe:] I love you
2021-07-01 17:52:26.163980+0800 KCObjcBuild[2748:71486] -[XJPerson thatGirlSayToMe:] I love you
Program ended with exit code: 0

总结:

  • objc_msgSend函数构成:objc_msgSend(消息接收者, 消息主体(sel + 参数))。
  • objc_msgSendSuper函数objc_super结构体里的super_class指定的是方法第一个查找的类。
  • OC方法调用的本质就是消息发送。

三、objc_msgSend汇编分析(arm64架构)

objc4-818.2源码里全局搜索objc_msgSend,然后按住command,鼠标左键点击文件名旁边的小箭头,收起所有文件,选择objc-msg-arm64.s文件点开,选择ENTRY _objc_msgSend开始查看objc_msgSend的汇编源码。

备注:支持__has_feature(ptrauth_calls)指针身份验证功能的情况(A12及之后的仿生芯片),本文不做分析。

图解:

image.png

1、_objc_msgSend汇编源码

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
        // p0,寄存器第一个值x0(id receiver)的地址
        // 检查p0(id receiver)是否为0(nil),
        // 如果没有接受者,则此次objc_msgSend无意义
	cmp	p0, #0			// nil check and tagged pointer check
        // 如果支持tagged pointer 
#if SUPPORT_TAGGED_POINTERS
        // receiver为0,是tagged pointer类型,
        // 执行b.le	LNilOrTagged	
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
        // receiver为0,不是tagged pointer类型
        // 执行b.eq	LReturnZero
	b.eq	LReturnZero
#endif
        // receiver不为空,p13 = x0栈内存中的值,
        // 对象和类的首地址就是isa,就相当于将isa给了p13
	ldr	p13, [x0]		// p13 = isa
        // 通过isa拿到类或者元类,存入p16(见后文分析)
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
// 通过receiver获取isa结束,
// 之所以这样做,是因为消息发送先查找是否有缓存,
// 而缓存在类或者元类里面
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
        // 调用CacheLookup,见后文分析
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	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

	END_ENTRY _objc_msgSend

解析:

  • cmp p0, #0检查p0x0的地址,receiver,消息接收者)是否为空,如果为空,那么此处objc_msgSend无意义。
  • 判断是否是tagged pointer类型(SUPPORT_TAGGED_POINTERS),如果是就执行LNilOrTagged,然后里面执行b.eq LReturnZero,否则就执行LReturnZero,结束本次objc_msgSend
  • 如果p0存在,就将x0存入p13,对象或类的首地址就是isa,那么p13 = isa
  • 进入宏(macro)GetClassFromIsa_p16,传入参数src = p13, needs_auth = 1, auth_address = x0
  • LGetIsaDone:获取isa完成,进入宏CacheLookup,传入参数传入参数Mode = NORMAL, Function = _objc_msgSend, MissLabelDynamic = __objc_msgSend_uncachedMissLabelConstant为默认参数。

2、GetClassFromIsa_p16汇编源码

// 传入参数 p13即isa(src), 1(needs_auth), x0即isa(auth_address)
// .macro表示方法宏定义
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

// #if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
// #   define SUPPORT_INDEXED_ISA 1
// #else
// #   define SUPPORT_INDEXED_ISA 0
// #endif
// arm64不是Indexed isa,不走这里
#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__  // 64位类unix系统,arm64走这里
// needs_auth传入为1,不走这里
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else // needs_auth == 1,走这里
	// 64-bit packed isa
        // 通过isa拿到类或者元类,存入p16(见后文分析)
	ExtractISA p16, \src, \auth_address
.endif
#else  // 32位,不走这里
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

解析:

  • 不满足SUPPORT_INDEXED_ISA(Indexed isa32isa),进入__LP64__Unix 和 Unix 类的系统 (Linux,Mac OS X))分支,needs_auth == 1所以进入宏ExtractISA,传入参数p16, \src = p13 = isa, \auth_address = isa

3、ExtractISA汇编源码

#if __has_feature(ptrauth_calls) // A12或之后的仿生处理器支持指针身份验证
// JOP

// 传入参数p16($0),src即isa($1),auth_address即isa($2)
.macro ExtractISA
        // 取出class(或metaclass),存入p16($0)
        // p16 = isa & ISA_MASK,
	and	$0, $1, #ISA_MASK
        // isa指针身份验证相关操作
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
	xpacd	$0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
	mov	x10, $2
	movk	x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
	autda	$0, x10
#endif
.endmacro

// JOP
#else
// not JOP

// 传入参数p16($0),src即isa($1),auth_address即isa($2)
.macro ExtractISA
        // 取出class(或metaclass),存入p16($0)
        // p 16 = isa & ISA_MASK,
	and    $0, $1, #ISA_MASK
.endmacro

// not JOP
#endif

解析:

  • ExtractISA$1isa)和ISA_MASK按位与操作(isa & ISA_MASK),得到类(class)或元类(metaclass),然后把结果存入$0p16)。返回_objc_msgSend

4、CacheLookup汇编源码

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// 传入参数NORMAL, _objc_msgSend, __objc_msgSend_uncached, 
// MissLabelConstant未传,为默认参数
.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
	//
        
        // 将x16赋值给x15
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // M1 iMAC
	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 // iPhone,arm64
        // #define CACHE            (2 * __SIZEOF_POINTER__)
        // 将x16平移CACHE大小,然后赋值给p11
        // class首地址偏移16字节就得到cache_t,
        // cache_t首地址就是_bucketsAndMaybeMask
	ldr	p11, [x16, #CACHE]		// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES // iPhone为1,走这里
#if __has_feature(ptrauth_calls) // A12及之后走这里
        // 判断p11的0号bit位是否为0,不为0->LLookupPreopt,为0继续走下面流程
	tbnz	p11, #0, LLookupPreopt\Function
        // p10 = p11 & 0x0000ffffffffffff = buckets
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
        // p10 = p11 & 0x0000fffffffffffe = buckets
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
        // p11的0号bit位是否为0,不为0->LLookupPreopt,为0继续走下面流程
	tbnz	p11, #0, LLookupPreopt\Function
#endif
        // cache_hash算法,算出bucket的index
        // p1 就是 _cmd
        // p12 = _cmd ^ (_cmd >> 7)
	eor	p12, p1, p1, LSR #7
        // p11 >> 48 得到 mask
        // p12 = (_cmd ^ (_cmd >> 7)) & mask,得到哈希index
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
        // cache_hash算法,算出bucket的index
	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
        
        // p10 = buckets, p12 = index(第一次查询的index), PTRSHIFT = 3
        // #define PTRSHIFT 3
        // 一个bucket_t占用16字节,即 << 4
        // p12(index)左移4位就是index * 16
        // 其实就是buckets首地址加上index个bucket_t内存大小,
        // 找到index位置的bucket,赋值给p13
        // p13 = p10 + (p12 << 4) = buckets + index * 16,内存平移
        // p13 = index位置的bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
        // #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__)
        // 先取值x13,imp给p17,sel给p9,
        // 然后再减一个BUCKET_SIZE的内存大小,得到上一个bucket
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        // 比较p9和p1,不相等走3:,相等走2:
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
        // 判断sel是否为空,为空就走__objc_msgSend_uncached
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
        // b.hs 指令是判断是否无符号小于,判断p13无符号小于p10,是走1流程
        // 不是就继续往下面走
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b
        
        // 如果直到第一个bucket都没找到符合的sel,那么会接着往下面走
        // 因为第一次定位的index后面可能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.
	//
	// 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 // M1版iMac
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // iPhone,arm64
        // p11 = _bucketsAndMaybeMask, p10 = buckets, PTRSHIFT = 3
        // p11 >> 48 -> mask
        // p13 = buckets + (mask << 4) = buckets + mask * 16,
        // 得到mask位置(最后一个)的bucket
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT) 获得最后一个bucket
						// 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
        // p12 = (_cmd ^ (_cmd >> 7)) & mask = index 第一次确定的下标
        // p10 = buckets, PTRSHIFT = 3
        // p12 = p10 + p12 << 4 = buckets + index * 16,得到第一次定位的bucket
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
        // BUCKET_SIZE = 16字节
        // 先取值x13,imp给p17,sel给p9,然后再减一个BUCKET_SIZE的内存大小,
        // 得到上一个bucket
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        // 比较sel和imp
	cmp	p9, p1				//     if (sel == _cmd)
        // 相等就表示找到了缓存,跳转到流程2,缓存命中
	b.eq	2b				//         goto hit
        // b.hi 比较结果是无符号大于,执行地址中的方法,否则不跳转,
        // ccmp,表示对比两个条件,
        // sel != 0 && bucket > first_probed,
        // 满足就继续走4流程,不满足就继续往下面走
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function: // 查询结束
LLookupRecover\Function:
	b	\MissLabelDynamic // 没有找到缓存,执行__objc_msgSend_uncached

#if CONFIG_USE_PREOPT_CACHES // iPhone为1
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls) // A12及之后走这里
        // p10 = p11 & 0x007ffffffffffffe
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif
        // 共享缓存的其他操作
	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

解析:

  • mov x15, x16,隐藏初始isax16isa)赋值给x15
  • arm64架构进入CACHE_MASK_STORAGE_HIGH_16分支,执行ldr p11, [x16, #CACHE],即x16平移CACHE大小(2个指针,16字节,即#define CACHE (2 * __SIZEOF_POINTER__)),得到cache_t指针地址,cache_t指针首地址既是第一个成员变量_bucketsAndMaybeMask的地址,既是mask|buckets然后赋值给p11p11 = mask|buckets)。
  • 进入CONFIG_USE_PREOPT_CACHES分支,本文不分析__has_feature(ptrauth_calls)(指针身份验证)的情况,所以进入else分支,执行and p10, p11, #0x0000fffffffffffe,即p110x0000fffffffffffepreoptBucketsMask掩码)做按位与操作,得到buckets赋值给p10p10 = buckets)。
  • 执行tbnz p11, #0,判断p110bit位是否为0,不为0,就走LLookupPreopt,为0则继续下面流程。
  • 执行eor p12, p1, p1, LSR #7p1就是objc_msgSend的第二个参数sel,即_cmd,所以此指令即p12 = _cmd ^ (_cmd >> 7)
  • 执行and p12, p12, p11, LSR #48p11右移48就是获得mask,所以此指令即p12 = p12 & (_bucketsAndMaybeMask >> 48)。第十二步和第十三步就是cache_hash函数的算法(x12 = (_cmd ^ (_cmd >> 7)) & mask),即通过selmask算出查找bucket第一次定位的index赋值给p12
  • 执行add p13, p10, p12, LSL #(1+PTRSHIFT)arm64架构下#define PTRSHIFT 3,所以此指令即p13 = p10 + p12 << (1 + 3),一个bucket_t占用16字节,p12index)左移4位就是index * 16,其实就是buckets首地址加上indexbucket_t内存大小,找到index位置的bucket赋值给p13
  • 执行1: ldp p17, p9, [x13], #-BUCKET_SIZE,取值x13imp赋值给p17sel赋值给p9,然后x13--拿到上一个位置的bucket(表达式即{imp, sel} = *bucket--)。
  • cmp p9, p1,比较当前bucketselobjc_msgSend的第二个参数_cmd
  • 相等就执行2: CacheHit \Mode,传入参数Mode = NORMAL
  • 不相等就执行3: cbz p9, \MissLabelDynamic,判断当前bucketsel是否为空,如果为空,则表示要查找的_cmd没有缓存,执行MissLabelDynamic(传入进来为__objc_msgSend_uncached函数)。
  • 如果当前bucketsel不为空,则执行cmp p13, p10b.hs 1bb.hs指令是判断是否无符号小于,即判断p13无符号小于p10,是走1流程,再次执行第十五步(表达式即while (bucket >= buckets))。
  • 如果直到第一个bucket都没找到符合的sel,那么会接着往下面走,因为第一次定位的index后面可能bucket没有查询,再次进入CACHE_MASK_STORAGE_HIGH_16分支,执行add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)),获取mask位置的bucket(最后一个)存入p13
  • 执行add p12, p10, p12, LSL #(1+PTRSHIFT),获取第一次定位的index位置的bucket,存入p12。
  • 执行ldp p17, p9, [x13], #-BUCKET_SIZE,首先取出x13对应的bucketimpsel分别存入p17p9,然后x13向前平移一个BUCKET_SIZE的内存大小(16字节),获得上一个bucket
  • 执行cmp p9, p1,比较selimp,相等就表示找到了缓存,跳转到流程2,缓存命中。
  • 执行cmp p9, #0ccmp p13, p12, #0, neb.hi 4bb.hi比较结果是无符号大于,执行地址中的方法,否则不跳转,ccmp表示对比两个条件,如果sel不为空,且当前查询的bucket大于第一次定位的bucket,就跳转到流程4,继续查找。
  • 如果不满足条件就往下面走,查询结束,执行__objc_msgSend_uncached

5、CacheHit汇编源码

// 传入参数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
.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
.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, 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
  • 传入ModeNORMAL,所以进入.if $0 == NORMAL分支,执行TailCallCachedImp x17, x10, x1, x16

6、TailCallCachedImp汇编源码

#if __has_feature(ptrauth_calls)
// JOP
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$1, $1, $2  // mix SEL into ptrauth modifier
	eor	$1, $1, $3  // mix isa into ptrauth modifier
	brab	$0, $1
.endmacro

// JOP
#else
// not JOP

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
        // cached imp ^= isa,imp解码
	eor $0, $0, $3
        // 跳转解码后的imp,即call imp
	br $0
.endmacro
// not JOP
#endif
  • 执行eor $0, $0, $3cached imp ^= isaimp解码。
  • 执行br $0,跳转解码后的imp,即call imp

7、缓存查找(快速查找)流程

缓存查找(快速查找)流程.jpg

8、总结

objc_msgSend就是通过sel查找imp,首先会查找方法缓存,这也就是方法的快速查找流程。