iOS的OC的方法的查找原理

1,419 阅读11分钟

前言

笔者整理了一系列有关OC的底层文章,希望可以帮助到你。这篇文章主要讲解的是方法查找原理分析

1.iOS的OC对象创建的alloc原理

2.iOS的OC对象的内存对齐

3.iOS的OC的isa的底层原理

4.iOS的OC源码分析之类的结构分析

5.iOS的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,inittestMethod等方法都是在底层通过objc_msgSend来进行发送消息的,可以看出方法的本质就是通过objc_msgSend来发送消息的。其中objc_msgSend有两个参数,id是消息的接收者,SEL方法的编号。其中通过之前的objc4-756.2的源码查找到可以知道,方法的调用在底层会分别被编译成objc_msgSend, objc_msgSend_stret, objc_msgSendSuperobjc_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外面的,会继续执行LGetIsaDoneCacheLookup

2.3 CacheLookup

其中CacheLookup有三种查找的方式CacheLookup NORMAL|GETIMP|LOOKUP,NORMAL是正常的流程,GETIMP查找impLOOKUP方法的查找。

/********************************************************************
 *
 * 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部分的内容判断buckectsel与传进来的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参数来判断需要执行那一部分。由上面的内容可知传进来的$0NORMAL。接下来的执行__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源码分析之类的结构分析可以知道方法的是存类的bitsrorw里面的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函数报错。

至此,方法的查找底层原理就介绍完毕了。