OC底层探索(八)objc_msgSend 流程之方法快速查找

OC runtime运行时

在探索objc_msgSend时,我们需要先了解OC的runtime机制。

runtime简介

runtime 是 OC底层的一套C/C++的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。

runtime 交互的三种方式

在这里插入图片描述

  • Objective-C Code直接调用
    比如直接调用方法[self say]、#selector()等。

  • Framework&Serivce
    比如NSSelectorFromString、isKindeofClass、isMemberOfClass等方法。

  • RuntimeAPI
    比如sel_registerName、class_getInstanceSize等底层方法。

探索OC方法本质

准备环境

  • 新建一个Person类,并声明一个实例方法。
@interface Person : NSObject
-(void)sayOK;
-(void)sayNB;
    
@end
  • 在main.h文件中,初始化Person,并调用sayOK方法。
int main(int argc, char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        [person sayNB];
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
  • 执行Clang,将main.m文件编译成main.cpp文件,并找到main函数。
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("sayNB"));

    }
    return 0;
}

开始探索

  • 分析main.cpp文件中的main函数

    · 在调用allocsayOK时,都调用了objc_msgSend方法,字面意思是objc消息发送。
    · objc_getClass(“Person”)获取Person类。
    · sel_registerName(“XXX”),调用方法,类似于@selector、NSSelectorFromString()。

  • 使用objc_msgSend方式调用sayOK方法

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        [person sayNB];
        
//        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("sayNB"));

        objc_msgSend(person,sel_registerName("sayNB"));
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

打印结果: objc_msgSend()成功调用sayOK方法。
在这里插入图片描述

  • 调用父类方法

    · 新建Student类,并继承与Person类

#import <Foundation/Foundation.h>
#import "Person.h"

@interface Student : Person

@end

· 在main.m文件中,使用声明并student,并调用sayOK方法;使用objc_msgSendSuper调用sayOK方法。

		Student *student = [Student alloc];
        [student sayOK];
        
        struct objc_super lsuper;
        lsuper.receiver = student;
        lsuper.super_class = [Person class];
        
        objc_msgSendSuper(&lsuper, sel_registerName("sayOK"));

打印结果: 种方式都调用了父类Person的sayOK方法。

在这里插入图片描述

总结

  • 方法的本质发送消息
  • OC调用方法等价runtimeobjc_msgSendobjc_msgSendSuper消息发送。

思考:objc_msgSend是怎样找到对应的方法呢?即 sel 如何找到对应 imp?

objc_msgSend介绍

在objc4源码中我们会发现objc_msgSend是使用汇编实现的,汇编主要的特性是:

  • 速度:汇编更容易被机器识别。
  • 方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息。

消息查找机制

  • 快速查找:cache中查找
  • 慢速查找
    · methodList中查找
    · 消息转发

objc_msgSend分析

快速查找:cache_t中查找

源码分析

objc_msgSend调用
objc_msgSend(person,sel_registerName("sayNB"));

objc_msgSend传入个参数:分别为消息接收者和消息的sel

objc_msgSend 源码
  • 在objc4-781中我们在objc-msg-arm64.s文件找到ENTRY _objc_msgSend部分。
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
	//小对象类型
	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		// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	// 在cache中开始找imp
	CacheLookup NORMAL, _objc_msgSend
  • 具体流程图

在这里插入图片描述

CacheLookup源码 缓存中查找imp
  • 在objc-msg-arm64.s文件中找到.macro CacheLookup
.macro CacheLookup
	
LLookupStart$1:

	// p1 = SEL, p16 = isa
	//part1: isa 平移16字节得到 cache_t,cache首地址是mask_buckets
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	//part2: 获取buckets p11 & 0x0000ffffffffffff 得到后48位 buckets
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	//part3: 获取hash 搜索下标:逻辑右移48位 得到mask;然后p1 & mask给p12 得到hash存储的key 
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#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

	// part4: p12是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	//part6: 0 将p12属性imp 和 sel分别赋值为p17 和 p9
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
	//part6: 1判断当前bucket的sel和传入的sel是否相等
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	//part6: 2如果不相同,则跳入2f
	b.ne	2f			//     scan more
	//part6: 3如果相同直接返回imp
	CacheHit $0			// call or return imp
	
	//part6: 4 没有找到 进入2f
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	//part6: 5 如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
	cmp	p12, p10		// wrap if bucket == buckets
	//part6: 6 如果相等 跳入3f
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	//part6: 7 再将p12的指针指到buckets的最后一个元素
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#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.
//part6: 8 然后在继续查找,直到找到或者再次 bucket 与 buckets再次相等,跳出循环。
	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:	// double wrap
	//part6: 结束循环
	JumpMiss $0

.endmacro
part1: 获取mask_buckets
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
......
}
  • 我们已知P16isa,那么平移16字节我们就可以获取到cache属性,并赋值给P11
part2:获取buckets
struct cache_t {
#if 1 // Mac 
    ...
#elif 1 // 真机
    uintptr_t _maskAndBuckets;
    mask_t _mask_unused;
    
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    ...
   }
  • 在part1中获取到了P11cache,那么首地址_maskAndBuckets

  • 其中在objc4-781源码中_maskAndBuckets的定义:

{
    uintptr_t buckets = (uintptr_t)newBuckets;
    uintptr_t mask = (uintptr_t)newMask;
    
    ASSERT(buckets <= bucketsMask);
    ASSERT(mask <= maxMask);
    //maskShift 是 48 
    //将mask左移48位只留下16位,剩余的补0,
    _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
    _occupied = 0;
}

· 将mask48位只留下16位,放入_maskAndBuckets的高16
· 并且 运算 buckets放在低48位(或运算:0|0=0; 0|1=1; 1|0=1; 1|1=1;)
· 高16位 | 低48位 = mask | buckets

  • p11 & 0x0000ffffffffffff 获取到低48位,即buckets
and	p10, p11, #0x0000ffffffffffff
part3: 获取hash 搜索下标
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}
  • 先将p11(_maskAndBuckets) 逻辑48位,得到mask
  • 在使用p1(传入的第二个参数:sel) 运算 mask获取到开始下标
and	p12, p1, p11, LSR #48	
part4:根据下标找到对应的bucket
  • PTRSHIFT 是一个宏定义,固定值为3
    在这里插入图片描述

  • buckets是一个数组,如果想得到数组中的元素 我们可以根据首地址进行指针平移获取到对应下标的

  • 将第三步获取的P12开始下标 逻辑4位 或者 可以理解为 bucket是有selimp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16

  • buckets指针平移上一步得到的值,然后将平移后的bucket存到p12中。

part6:根据bucket中的sel查找
  • 将bucket中的属性属性impsel分别赋值为p17p9
ldp	p17, p9, [x12]		// {imp, sel} = *bucket
  • 判断当前bucket的sel传入sel是否相等:如果相等返回对应imp=>p17;不相等进入2f
	cmp	p9, p1			// if (bucket->sel != _cmd)
	//part6: 2如果不相同,则跳入2f
	b.ne	2f			//     scan more
	//part6: 3如果相同直接返回imp
	CacheHit $0			// call or return imp
  • 此时是不相等2f部分,这是一个循环。由于汇编中的查找是向上查找,所以p12-1获取到上一个bucket指针。如果当前p12 bucket与buckets首地址(第一个元素)相等,那么就直接跳入3f部分。
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	//part6: 5 如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
	cmp	p12, p10		// wrap if bucket == buckets
	//part6: 6 如果相等 跳入3f
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

  • 此时是p12 bucket与buckets的首地址(第一个元素)相等,3f部分。

    · maskbuckets数组的个数减一,将mask4位,

    · 将buckets首地址地址平移上一步的结果,就到了buckets最后一位,再将buckets最后一位的指针地址赋值给p12

    · 然后在继续进行比较sel,如果有相等就返回相应的imp,如果没有相等则就继续向上查询。

    · 如果p12又一次指到的首地址,那么说明整个buckets不存在方法sel,则退出循环,并返回。

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	//part6: 7 再将p12的指针指到buckets的最后一个元素
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#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
//part6: 8 然后在继续查找,直到找到或者再次 bucket 与 buckets再次相等,跳出循环。
	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:	// double wrap
	//part6: 结束循环
	JumpMiss $0

.endmacro

源码流程图

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值