iOS底层探索(八) objc_msgSend方法☞快速查找

Objective-C runtime

在了解objc_msgSend时。我们需要先了解Objective-C的runtime机制

runtime简介

在了解runtime之前,先区分个概念编译时运行时

  • 编译时:顾名思义,就是正在编译的时候,编译器帮你把源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)。在这个过程中,编译器不关心你的具体流程,只关心你是否有语义语句上的错误。
  • 运行时:就是代码跑起来了,已经被装载到内存中调用了。(在应用没有运行时,你的代码或应用是保存在磁盘上的,是个死的。只有跑到内存中才会被激活)。而运行时类型检查就是与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码。而是在内存中做些操作,做些判断

runtime版本

Runtime有两个版本:一个Legacy版本(早期版本),一个Modern版本(现行版本)

  • 早期版本对应的编程接口:Objective-C 1.0

  • 现行版本对应的编程接口:Objective-C 2.0

  • 早期版本用于Objective-C 1.0、32位的Mac OS X的平台上

  • 香型版本用于iOS和Mac OS X v10.5及以后的系统中的64位程序

Objective-C Runtime Programming Guide

runtime 交互的三种方式

在这里插入图片描述

  • Objective-C Code 直接调用
    比如直接调用方法[self addSubview:]@selector()
  • Framework&Service
    比如NSSelectorFromStringisKindOfClassisMemberOfClass等方法
  • Runtime API
    比如sel_registerNameclass_getInstanceSize等底层方法。

Objective-C方法本质

  • 新建一个LGPerson类,并声明一个实例方法。
@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end
  • main.h文件中,实例化LGPerson,并调用sayHello方法。
LGPerson *person = [LGPerson alloc];
[person sayHello];
  • 执行Clang,将main.m文件编译成main.cpp文件,并找到main函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_93c270_mi_1);
    }
    return 0;
}

解析

  • ** 分析main.cpp文件中的main函数**
    • 在调用allocsayHello时,都调用了objc_msgSend方法,
    • objc_getClass(“LGPerson”)获取LGPerson
    • sel_registerName(“sayHello”),获取调用方法,类似于==@selector==、NSSelectorFromString()
  • **修改工程中的Apple Clang - Preprocessing配置
    在这里插入图片描述
  • **使用objc_msgSend方式调用sayHello方法
LGPerson *person = [LGPerson alloc];
[person sayHello];
objc_msgSend(person, sel_registerName("sayHello"));

打印结果objc_msgSend成功调用了sayHello方法。
在这里插入图片描述

  • 调用父类方法
  1. 新建LGTeacher类,并继承于LGPerson
@interface LGTeacher : LGPerson
- (void)sayNB;
@end
  1. main.m文件中,实例化LGTeacher,并调用sayHello方法;使用objc_msgSendSuper调用sayHello方法。
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];

struct objc_super lgsuper;
lgsuper.receiver = teacher;
lgsuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));

** 打印结果**:两种方式都调用了父类LGPersonsayHello方法
在这里插入图片描述

总结

  • 调用方法的本质发送消息
  • Objective-C调用方法等价runtime中的objc_msgSendobjc_msgSendSuper消息发送。

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

objc_msgSend介绍

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

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

消息查找机制

  • 快速查找:cache中查找
  • 慢速查找
    1. methodList中查找
    2. 消息转发

objc_msgSend分析

快速查找:cache_t中查找
源码分析

objc_msgSend调用

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

objc_msgSend传入两个参数:分别为消息接收者sel
objc_msgSend源码

  • 在objc4-781中,在objc-msg-arm64.s文件找到ENTRY _objc_msgSend部分,注:在汇编中的方法会比在OC的方法多一个_.
	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	
	// p0 是传入的第一个参数,即消息的接收者
	cmp	p0, #0			// nil check and tagged pointer check,判断p0是否为空
#if SUPPORT_TAGGED_POINTERS
	// 小对象类型
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	// p0位空时,执行LReturnZero,即返回空
	b.eq	LReturnZero
#endif
	// p13 获取消息接收者的isa
	ldr	p13, [x0]    	// p13 = isa
	// p16 根据isa 获取到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
	// isa 平移16字节得到 cache_t,cache首地址是mas_bukets
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	// 获取buckets p11 & 0x0000ffffffffffff 得到后48位 buckets
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	// 获取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
	// p12是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

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

	// 然后继续查找,直到找到或者再次 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
	// 结束循环
	JumpMiss $0

.endmacro

源码流程图

在这里插入图片描述

objc-msg-arm64.s源码文件

objc-msg-arm64.s

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值