前言
笔者整理了一系列有关OC的底层文章,希望可以帮助到你。这篇文章主要讲解的是方法查找原理分析。
iOS的开发中我们会使用类中的各种方法,在OC中对方法的调用称为消息的发送
。对方法函数的使用每一个iOS开发者都很熟悉的,但是方法函数是怎么在底层中是怎么查找的就是这篇文章主要来介绍的。
1. 方法的本质
为了方便介绍接下来的内容,创建一个macOS的项目,定义了一个TestObject
的类定义了一个testMethod
的方法,在main.m
的文件里面实现如下代码
#import <Foundation/Foundation.h>
#import "TestObject.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestObject *objc = [[TestObject alloc] init];
[objc testMethod];
}
return 0;
}
然后在该项目的目录下用终端命令clang -rewrite-objc main.m
直接编译生成一个main.cpp
文件来查看上面代码的底层实现,最终得到代码如下
TestObject *objc = objc_msgSend(objc_getClass("TestObject"), sel_registerName("alloc")), sel_registerName("init"));
objc_msgSend(objc, sel_registerName("testMethod"));
其中sel_registerName
函数相当于@selector
,在TestObject
类调用alloc
,init
和testMethod
等方法都是在底层通过objc_msgSend
来进行发送消息的,可以看出方法的本质就是通过objc_msgSend
来发送消息的。其中objc_msgSend
有两个参数,id
是消息的接收者,SEL
方法的编号。其中通过之前的objc4-756.2
的源码查找到可以知道,方法的调用在底层会分别被编译成objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
和objc_msgSendSuper_stret
。如果调用父类的方法会编译成带有super
字段的函数,其中objc_msgSend_stret
是调用结构体的方法。
2.方法的快速查找
通过objc4-756.2
的源码找到objc_msgSend
的底层源码是通过汇编的方式来写的,接下来的源码介绍是在arm64
的架构下的。
2.1 objc_msgSend汇编
//objc_msgSend函数的入口
ENTRY _objc_msgSend
//objc_msgSend没有窗口
UNWIND _objc_msgSend, NoFrame
//对比当前的p0第一位是否为空或者是taggedPointer,如果是nil会跑到LReturnZero中,
//如果是taggedPointer会跑到LNilOrTagged
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
//在正常的情况下不是nil不是taggedPointer,会执行到这里
//其中p13为isa,如果消息的接收者是对象通过isa可以找到类,如果是类可以找到元类
ldr p13, [x0] // p13 = isa
//这里就去到GetClassFromIsa_p16的宏方法,将p13为isa作为参数
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
从中可以看到,进入到objc_msgSend
汇编里面会先判断传进来的接收者是否为空和是否是taggedpointer,如果都不是就先找到isa
,通过isa
找到class
,接下来介绍GetClassFromIsa_p16
。
2.2 GetClassFromIsa_p16
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
这部分的源码在arm64
架构下只会走#elif __LP64__
下的,通过传进来的isa
&ISA_MASK
得到class
,并且以p16返回,最终还是返回上面的objc_msgSend
外面的,会继续执行LGetIsaDone
的CacheLookup
。
2.3 CacheLookup
其中CacheLookup
有三种查找的方式CacheLookup NORMAL|GETIMP|LOOKUP
,NORMAL
是正常的流程,GETIMP
查找imp
,LOOKUP
方法的查找。
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x12 = address of cached IMP
#define CACHE (2 * __SIZEOF_POINTER__)
#define CLASS __SIZEOF_POINTER__
//这是在缓存cache_t中查找方法
.macro CacheLookup
//其中x16是找到的class,通过#CACHE得到16个字节,从而class右移16字节得到cache_t
//其中cache_t是一个结构体,占16字节,bucket_t占8个字节,mask和occupied分别占4个字节
//并将cache_t中的buckets赋值给p10,occupied和mask赋值给p11
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
//x12是得到的hash值
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
这部分的内容就是查找到cache_t
,并在cache_t
查找传进来的方法是否在这里,具体的cache_t
的方法缓存可以看iOS的OC的方法缓存的源码分析这篇文章的介绍。在1
部分的内容判断buckect
的sel
与传进来的cmd
是否相等,即是否有缓存过的方法,如果缓存命中CacheHit
直接返回imp
,如果没有缓存的就去到2
部分的内容。执行CheckMiss
。
2.4 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
这部分是根据之前的传进来的$0
参数来判断需要执行那一部分。由上面的内容可知传进来的$0
为NORMAL
。接下来的执行__objc_msgSend_uncached
,至此objc_msgSend
通过cache_t
来快速查找部分就结束了,接下来的部分就是通过慢速的方法查找。
3.方法的慢速查找
objc_msgSend
通过第2部分的cache_t
快速查找,在缓存中找不到有缓存的方法,此时就需要进行没有缓存的慢速查找。
3.1 objc_msgSend_uncached和MethodTableLookup
这部分的内容就是
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
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
通过之前的文章iOS的OC源码分析之类的结构分析可以知道方法的是存类的bits
的ro
和rw
里面的methodList
的,在cache_t
里面找不到方法的时候,此时就需要在methodList
找了,而MethodTableLookup
就是为了这部分内容做的准备。最终会执行到__class_lookupMethodAndLoadCache3
这个函数。
3.2 class_lookupMethodAndLoadCache3
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
传进来的obj
是对象,sel
是方法的编号,cls
是类。然后直接调用lookUpImpOrForward
函数,此时进来的参数中initialize
是YES,cache
是NO,resolver
是YES,因为此时是在cache_t
缓存中找不到方法才执行到这里的。
3.3 lookUpImpOrForward
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
这个lookUpImpOrForward
函数的代码有点多,就分开一点点地分析。其中runtimeLock
是防止线程并发竞争的锁。
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
这里是再次判断如果有缓存的,直接在缓存中找到imp
返回出去。
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
上面这部分的内容是判断类是否是合法的,并且判断类是否是初始化了,如果没有初始化好的话,就需要进入到realizeClass
函数里面进行初始化,这个函数也是对当前的类的父类和元类都做了初始化,这部分的内容就是为了接下来的类在bits
里面查找方法做好准备的。
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
这部分的内容是在类class
里面查找方法。getMethodNoSuper_nolock
函数是在类cls
的data()里面的methodList
列表循环查找sel
。如果找到就返回method_t
。并且执行log_and_fill_cache
函数,到最后还是会执行cache_fill
。此时会将方法再次缓存在cache_t
中。
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
这部分的内容是在类里面查找不到方法了,需要去父类查找方法。因为我们之前的查找都是对当前的类开启objc_msgSend
汇编查找和cls的bits的methodList查找的,父类的方法也是可能有缓存的,所以此时通过父类的循环首先是通过cache_getImp
函数来查找imp
。其中_objc_msgForward_impcache
是实际存储在其中的函数指针方法缓存。如果有直接执行log_and_fill_cache
函数直接done
,如果没有就break
出去。如果没有找到imp
或者找到imp
做转发了此时不缓存,会直接调用getMethodNoSuper_nolock
函数来查找。如果找到还是会对这个方法做缓存的。
4.方法查找失败
上面的介绍都是方法存在的,如果在方法查找的过程中,查找不到的话是会报异常的,例如执行如下的代码
TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];
通过上面的源码可以知道,当方法在慢速查找和快速查找的过程中都找不到,并且也不做消息转发的处理,最终会执行到如下的源码
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
最终会执行_objc_msgForward_impcache
函数,而_objc_msgForward_impcache
是汇编的
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
由此可知,__objc_msgForward_impcache
会执行到__objc_msgForward
,最终会执行到__objc_forward_handler
。通过源码的查找,最后会执行objc_defaultForwardHandler
函数打印出错误的信息。
#if !__OBJC2__
// Default forward handler (nil) goes to forward:: dispatch.
void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;
#else
// Default forward handler halts the process.
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret
objc_defaultForwardStretHandler(id self, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif
为什么objc_msgSend
的底层是用汇编来写的呢?
- 为了更好地性能优化和更加容易被机器识别,因为在项目中方法的调用是非常频繁的也是很消耗性能的,所以用汇编可以更加地优化性能。
- 有时有的方法的调用会有一些未知的参数和未知的类型,如果用c或者c++都是很难实现的,汇编可以很完美地解决这些问题。
5 总结
方法的查找是在objc_msgSend
的函数下进行的,这一个过程有快速查找和慢速查找。
- 快速查找:先通过
objc_msgSend
快速查找,而objc_msgSend
是在汇编的情况下进行的。进入objc_msgSend
先判断第一位的内存值是否为空或者是taggedPointer
,如果是就走相应的流程。如果不是就是正常的流程就需要通过GetClassFromIsa_p16
找到isa
,通过isa
执行CacheLookup
去到类的cache_t
来查找是否缓存方法,如果没有就执行__objc_msgSend_uncached
。此时就相当于快速查找方法是找不到了,需要过度到慢速的查找。 - 慢速查找:通过
__objc_msgSend_uncached
可以执行MethodTableLookup
函数来为接下来需要在类的bits中查找的方法作准备。最终会在汇编中过渡到c++函数,执行class_lookupMethodAndLoadCache3
。通过lookUpImpOrForward
函数来分别遍历类和父类的方法列表中查找,如果找到就缓存在cache_t
中。如果没有找到,并且没有做消息转发
的操作,最终会执行_objc_msgForward_impcache
然后进去__objc_msgForward
的__objc_forward_handler
函数报错。
至此,方法的查找底层原理就介绍完毕了。