在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.m中main函数中的代码被编译成了如下形式。
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的慢速查找流程。