阅读 166

OC底层原理-objc 818(五)objc_msgSend方法快速查找

编译时与运行时

编译时

编译时顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。实际上只是翻译成某个中间状态的语言。

那编译时就是简单的做一些翻译工作,比如检查代码规范、语法分析之类的过程。

运行时

所谓运行时就是代码跑起来了,被装在到内存中去了,是一个动态过程,而运行时类型检查就是与前面讲的编译时类型检查不一样,不是简单的扫描代码,而是在内存中做了实际操作进行判断。

OC的运行时就是我们所说的RunTime。

Runtime交互的三种方式

  • Objective-C Code直接调用

比如直接调用方法[self say]、#selector()等。

  • Framework&Serivce

比如NSSelectorFromString、isKindOfClass、isMenberOfClass等方法。

  • RuntimeAPI

比如sel_registerName、class_getInstanceSize等底层方法。

Clang编译OC源代码

环境准备

OC源代码

@interface Person : NSObject

- (void) running;
- (void) swimming;

@end

@implementation Person

- (void) running
{
    NSLog(@"running");
}

- (void) swimming
{
    NSLog(@"swimming");
}

@end

@interface Student : Person

@end

@implementation Student

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        [person running];
        objc_msgSend(person, sel_registerName("running"));
        
        Student *student = [Student alloc];
        [student swimming];
        
        struct objc_super yjSuper;
        yjSuper.receiver = student;
        yjSuper.super_class = objc_getClass("Person");
        objc_msgSendSuper(&yjSuper, sel_registerName("swimming"));
    }
    return 0;
}复制代码

Clang转换main.cpp

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("running"));
    }
    return 0;
}复制代码

方法调用分解

使用objc_msgSend调用running方法

objc_msgSend(person, sel_registerName("running"));

使用objc_msgSendSuper调用父类的swimming方法

objc_msgSendSuper(&yjSuper, sel_registerName("swimming"));

我们看一下objc_msgSendSuper的第一个参数是struct objc_super *结构体指针,源码如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};复制代码

其中我们用到的两个参数分别是receiversuper_class,分别是消息接受者,和父类对象,消息接受者就是我们对象本身,父类就是Person类对象,由于我们声明的是一个结构体,参数需要是一个指针所以使用 & 获取结构体地址作为参数传入。

main.cpp分析方法调用过程

  • 通过cpp文件分析我们可以看到alloc方法和running都是通过objc_msgSend方法调用的。

  • objc_getClass()就是runtime的方法,用于获取Person的类对象。

  • sel_registerName()也是runtime的方法,用于获取方法,对应OC的@Selector()、NSSelectorFromString()。

  • 我们通过上述发现,无论是实例方法的调用还是类方法的调用都是通过objc_msgSend方法进行的,只是参数一的消息接收者不同,调用类方法时消息接收者是类对象,调用实例方法时消息接收者时实例对象,参数二就是我们要调用的方法。

  • objc_msgSend会根据参数一在缓存中和方法列表中进行方法查找。

  • 在缓存中查找方法是最快的,所以我们称之为方法快速查找。

Objc_msgSend

介绍

在objc4源码中通过搜索发现objc_msgSend是使用汇编实现的,汇编的主要特征是:

  • 速度快,汇编更容易被机器识别。

  • 方法参数的动态性,汇编调用函数时传入的参数是不确定的,那么消息发送时,直接调用一个函数就可以发送所有消息。

消息查找机制

快速查找:cache中查找(缓存查找)。

慢速查找:methodList中查找(方法列表),和消息转发

Objc_msgSend快速查找分析

objc_msgSend调用

objc_msgSend(person, sel_registerName("running"));

传入两个参数,分别是消息接受者和消息的sel。

objc_msgSend汇编源码

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	
    // p0是我们传入的第一个参数:消息接受者
    // cmp是比较方法,比较p0是否为nil,如果为nil说明没有消息接受者,直接返回
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	// TagPointer类型
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	// 消息接受者为空返回空
	b.eq	LReturnZero
#endif
	// p13 是获取消息接受者的首地址,也就是isa
	ldr	p13, [x0]		// p13 = isa
    // GetClassFromIsa_p16 通过isa获取类对象并赋值给p16
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	// 在cache中查找imp
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached复制代码

流程图

CacheLookup源码

LLookupStart\Function:
	// p1 = SEL, p16 = isa
#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
    // 通过isa内存平移16位,获取cache首地址,cache首地址就是maskAndBuckets
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#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
	// maskAndBuckets是一个共用体,共占8位64字节 高16字节存储着mask值低48字节存储着buckets信息
    // 所以此处将mask|buckets & 0x0000ffffffffffff 获取低48字节的信息,也就是获取buckets并赋值给p10
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    // 此处_cmd & mask就是缓存插入式hash值的计算方式,catch_hash的原理就是 _cmd & mask
    // 所以次数获取的是缓存中的方法hash值,并赋值给p12变量
	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
	
    // 根据hash值索引逻辑计算要获取的bucket
    // 我们知道bucket中存储着sel和imp,所以占16字节
    // 那么p12就是_cmd & mask就是索引,也就是当前要找的第几位
    // (1+PTRSHIFT) == 4
    // (_cmd & mask) << (1+PTRSHIFT) 左移4位相当于,乘以2的4次方 也就是乘以16,整好是每个bucket的大小
    // 逻辑运算后p13就是我们当前从缓存中找到的bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
    // 此处是一个do-while循环
    // 使用p17和p9记录当前bucket的imp和sel
    // 同时bucket--,将bucket向前移动
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    // _cmd就是当前要调用方法的方法编号
    // 比较缓存中获取到的sel与我们传入的_cmd是否一致,如果一致则调用CacheHit命中方法,结束
	cmp	p9, p1				//     if (sel != _cmd) {
    // 如果不一致则调用 3:方法
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
    // 循环结束条件 当获取的bucket地址小于buckets的首地址时,说明已经取超,跳出循环
	cmp	p13, p10			// } while (bucket >= buckets)
    // 跳转 1:方法,比较当前sel与_cmd是否一致
	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
    // 调出循环,则说明当前获取的bucket已经超过了buckets了
    // 此时需要将bucket移动到buckets的最后,在重新从后向前查找一遍
	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
    // 标记已经查找到buckets一次了
	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 &&
    // 当在查找到buckets还为找到时,则结束快速查找,说明缓存中没有要调用的方法
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic复制代码

快速查找完整流程图


作者:少说多笑
链接:https://juejin.cn/post/7021440509453271054


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐