欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-31 18:38:23
...

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 交互的三种方式

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

  • 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方法。
OC底层探索(八)objc_msgSend 流程之方法快速查找

  • 调用父类方法

    · 新建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底层探索(八)objc_msgSend 流程之方法快速查找

总结

  • 方法的本质发送消息
  • 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
  • 具体流程图

OC底层探索(八)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
    OC底层探索(八)objc_msgSend 流程之方法快速查找

  • 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

源码流程图

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