iOS objc_msgSend分析

172 阅读3分钟

在OC中,方法本质上又是什么?我们调用一个方法的时候究竟发生了什么?

方法的本质

我们新建一个项目,在main.m中实现入下代码。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JKPerson *person = [[JKPerson alloc] init];
        [person saySomething];
    }
    return 0;
}

接下来我们通过clang来编译这个main.m文件。

clang -rewrite-objc main.m

执行完这条命令后我们会发现,在当前mian.m所在的文件目录下生成了一个新的main.cpp文件。 在main.cpp文件的最底部,我们发现我们main.mmain函数中的代码被编译成了如下形式。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        JKPerson *person = ((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JKPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
    }
    return 0;
}

这段对象调用方法的代码

[person saySomething];

被编译成了如下形式

((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));

也就是说我们的OC方法其本质上就是通过调用objc_msgSend函数来发送消息。 接下来我们来看看在objc_msgSend中究竟做了什么事情。

objc_msgSend

我们通过给objc_msgSend下符号断点得知objc_msgSend函数在我们的libobjc.A.dylib中。 接下来我们在libobjc.A.dylib中来查看我们的objc_msgSend源码。

我们以objc-msg-arm64.s为研究对象。

我们发现objc_msgSend使用汇编来实现的,为什么要用汇编来实现呢?有以下几点原因

  • 汇编更加容易被机器识别,效率更高。
  • C语言中不可以通过一个函数来保留未知的参数并且跳转到任意的函数指针。C语言没有满足这些事情的必要特性。

objc_msgSend中摘取其中关键代码如下

	ENTRY _objc_msgSend
	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
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL 
    END_ENTRY _objc_msgSend

我们可以看到在获取到Isa之后我们开启了方法的缓存查找流程

LGetIsaDone:
	CacheLookup NORMAL 

查找缓存

我们摘取CacheLookup关键代码如下。

.macro CacheLookup
	
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

我们可以看出其中有两个比较重要的,一个是CacheHit命中缓存,这个时候缓存命中了之后直接返回imp 另一个是CheckMiss,我们来看看CheckMiss做了什么。

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我们跟随__objc_msgSend_uncached__objc_msgLookup_uncached流程继续往下看

STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
	UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
。
	MethodTableLookup
	ret

	END_ENTRY __objc_msgLookup_uncached

我们可以看到他们都调用了一个叫MethodTableLookup的东西。 摘取其中关键代码如下。

.macro MethodTableLookup
	
	bl	__class_lookupMethodAndLoadCache3

.endmacro

至此我们关于objc_msgSend的汇编部分结束了,接下来将进入C/C++的查找流程。我们将在下篇文章中介绍objc_msgSend的慢速查找流程。