论objc_msgSend消息机制以前传

一.探索前需知

1.1 runtime 的概念api

iOS语言Objective-C是一门动态语言,其在runtime(运行时)会作不少事情,好比:消息的查找、消息的转发、动态属性的添加等等,可是runtime究竟是什么呢?runtime就是由C、C++、 汇编为OC提供运行时功能开发出来的一套api.xcode

1.2 runtime的使用方式 缓存

runtime的使用方式主要有三种 : ObjectIve-C调用 @selector()、NSObject的方法 NSSelectorFromString()、sel_registerName. 函数apisass


二.objc_msgSend 的初步了解
bash

首先咱们在main.m 文件里,建立了个LGPerson的对象并调用了sayNB的方法,又写了个C语言函数run,而且调用.好的那咱们如今看下底层在编译的时候到底作了哪些处理. 打开终端 输入命令:clang -rewrite-objc main.m -o main.cpp(输出个.cpp编译后的文件)打开main.cpp文件截取其中一段以下:app

咱们看到 sayNB方法底层编译经过objc_msgSend 底层查找函数的实现、默认带了两个参数 id和SEL id就是函数的接受者在这里就是person,SEL方法编号. 而run函数底层没有编译成objc_msgSend 那是由于C语言中函数名就是函数指针,它经过这个函数名直接就能找到函数实现并不须要objc_msgSend这一中转这一阶段.可是objc_msgSend到底作了什么呢?咱们先在代码里打个断点、打开工程里的Debug -> Debug Workflow - > Always Show Disassembly函数

就是打开汇编调试、咱们在方法上打个断点:oop

发现会进来下面汇编指令:post

sayCode下有个0x100000c63 <+67>: callq *0x397(%rip) ; (void *)0x00007fff63046000: objc_msgSend. 咱们在这行上打上断点而且按住control+xcode中step into进去发现指令会进入系统的libobjc.A.dylib的库里ui


接下来咱们就要在这个系统的库中继续研究objc_msgSend的流程了.

二.objc_msgSend 的进阶

首先咱们去 www.opensource.apple.com/apsl/ 中下载目前苹果开源的私有库libobjc.A.dylib ,而后配置到咱们的项目里.咱们的objc_msgSend是由汇编写的(由汇编写的好处是:1.能够经过写一个函数保留未知的函数任意的跳转到另外个函数指针 2.汇编语言更接近机器语言 因此使用起来速度会更快)

开始在项目工程全局搜索 objc_msgSend 找到实现objc_msgSend的汇编文件


找到是这个 arm.s 的文件,那咱们进文件里看下究竟吧.

 

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	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
    // person - isa - 类
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
复制代码

代码解析:

cmp	p0, #0 判断是不是 nonepointerisa(是否是纯净的ISA)复制代码
// person - isa - 类
  ldr	p13, [x0]		// p13 = isa
  GetClassFromIsa_p16 p13		// p16 = class复制代码

还记得刚刚objc_msgsend 时会默认 传两个参数吗 .idSELid 就是当前的对象 它的内存地址首地址是ISA,因此 这个对象 [xo]首地址位 就是isa .由于刚刚第一步已经判断出不是纯净的isa,因此本身的isa内部偏移16位获得 shiftClass也就是获取到当前对象所在的类.

CacheLookup NORMAL		// calls imp or objc_msgSend_uncached复制代码

接着在类的缓存里进行查找.

(对cache_t结构不是很熟悉的小伙伴们,建议看下上篇文章juejin.im/post/5e0ca3…)

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	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

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	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

3:	// double wrap
	JumpMiss $0
	
.endmacro
复制代码

 // p1 = SEL, p16 = isa

ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask

由ISA偏移16位获得类的cache,cache里有buckets 和 occupied|mask 分别由p10和p11进行接收。

and	w11, w11, 0xffff	// p11 = mask
#endif
and	w12, w1, w11		// x12 = _cmd & mask复制代码

用低32位的 p11接收mask,而后 &  _cmd (也就是sel) 获得  (mask_t)(key & mask);就是将要在缓存池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复制代码

紧接着就在缓存池里进行查找若是 bucket里的sel和_cmd匹配的话说明在缓存中已经找到,缓存命中.若是没有找到就跳转 2f步骤,找到则返回 , 找不到 JumpMiss .

继续来到 __objc_msgSend_uncached -> MethodTableLookup

最后调用 bl __class_lookupMethodAndLoadCache3, 来到慢速查找流程 .至于慢速查找流程请期待下篇 《论objc_msgSend消息机制正传之消息查找》.

四.总结

当咱们调用一个对象或者类方法的时候,系统会调用objc_msgSend进行汇编层面的查找,会什么先用汇编?由于快!咱们详细的探索了整个过程,汇编层面主要是对已经缓存过的IMP进行搜索。搜索的路径是存放整个方法的类或者元类的cache_t的bucket!若是找到直接返回整个方法的实现,若是查不到,说明这个方法没有进行缓存!那这个方法是否存在呢?请看下一篇文章,objc_msgSend在C函数_class_lookupMethodAndLoadCache3的查找和方法的动态解析!

相关文章
相关标签/搜索