六:底层探索 Runtime(1):方法查找

257 阅读12分钟

  在前面的篇章中我们探索了对象和类,以及属性和方法的存储,接下来我们会对方法进行一个探索,开篇之前我们对Runtime进行一个简单的介绍。

一:前导介绍

1. Runtime介绍

  Objective-C是门动态的语言,那么它需要有编译器,而Objective-C是以Clang作为编译器前端,LLVM作为编译器后端,LLVM 将一些在编译和链接过程中的工作,放到了运行阶段。这就需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
  Runtime就是这个运行时系统,是以 C/C++和汇编编写而成的,这是因为对于编译器来说,C/C++和汇编执行效率更高,可见苹果对动态的效率作出了非常大的努力。
  Runtime更多的内容以及API大家可以去 官方文档 看一下。

2. Runtime版本

  Runtime其实有两个版本: “modern”和“legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

3. 三种交互方式

    1. 直接在 OC 层进行交互:比如 @selector
    1. NSObject 的方法:NSSelectorFromName
    1. Runtime 的函数: sel_registerName

4. Runtime应用

    1. 关联对象
    1. 方法交换
    1. 字典和模型的相互转化
    1. 实现 NSCoding 的自动归档和自动解档

二:方法调用的本质

1.方法调用

@interface WYPerson : NSObject
- (void)eat;
@end
@implementation WYPerson
-(void)eat
{
    NSLog(@"吃饭");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        WYPerson *p = [[WYPerson alloc]init];
        [p eat];
    }
    return 0;
}

我们将main.m编译成main.cpp文件看看,底层到底做了什么,如何编译.cpp文件可以看 这篇文章

int main(int argc, const char * argv[]) {
        { __AtAutoreleasePool __autoreleasepool; 
        WYPerson *p = ((WYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((WYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WYPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
    }
    return 0;
}
[p eat]
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));

其中真正发送消息的是objc_msgSend,这个方法有两个参数,一个是消息的接收者者为 id 类型,其实就是我们创建的对象p,第二个是方法编号 sel。 下面我们在[p eat]打上断点,通过汇编来看看底层会不会走objc_msgSend的方法,选择Debug -- Debug Workflow -- Always show Disassembly

通过汇编我们发现objc_msgSend,是在libobjc.A.dylib中实现的,那么我们就需要用到这篇文章objc源码。

三: objc_msgSend 查找流程

objc_msgSend消息查找分为快速查找和慢速查找,下面会对这两种方式分开解析。

3.1 快速查找

3.1.1 objc_msgSend 汇编入口

使用objc源码,我们快速定位到汇编代码处,文件的标志 s 代表汇编,这里我们主要看arm64下的源码

我们搜索 ENTRY,快速定位到objc_msgSend函数的入口。

注意: 在汇编中ENTRY 是一个伪指令,用于指定汇编程序的入口点(其实我也不懂汇编,其实我也是去度娘结合代码注释去学习)。

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

前面都是一些判断和异常检查,我们直接往后看:
ldr p13, [x0] // p13 = isa,注释写着p13 = isa[x0]其实存的就是isa,这一步就是将x0中的值存储到p13
GetClassFromIsa_p16 p13 // p16 = class,注释写着p16 = class,从函数名也不难看出,意思就是根据p13中的isa获取到对应的class,存入p16CacheLookup 进行消息查找,传递的参数是NORMAL

3.1.2 CacheLookup 消息缓存查找

在文件中搜索CacheLookup,发现注释还比较长。

/********************************************************************
 *
 * 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

这里就涉及到我们上篇讲的方法cache_t了,意思是这个函数会在类的方法缓存中根据 SEL 查找对应的 IMPx1 = selector 就是说 x1 就是需要查找的 SELx16 是当前类,如果查找到了就调用或返回 IMP,否则跳转到 LCacheMiss。最后三个宏定义,其实NORMAL就是上面传递的参数。 下面看下CacheLookup的具体实现,已加上部分注释

.macro CacheLookup
	// p1 = SEL, p16 = isa
	// 这一步实际是通过isa偏移16字节,找到cache_t
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
        // 通过_cmd & mask 获取cache哈希表中的索引
	and	w12, w1, w11		// x12 = _cmd & mask
	// 通过下标获取到对应的bucket
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // 取出bucket中的sel和要查找的方法进行比较
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	// 找到缓存并返回IMP
	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_find一样,查找对应的IMP,只不过这里是通过汇编来实现的。 下面对命中缓存CacheHit,未命中缓存CheckMiss 进行分析。

3.1.3 CacheHit 找到缓存

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	AuthAndResignAsIMP x0, x12	// authenticate imp and re-sign as IMP
	ret				// return IMP
.elseif $0 == LOOKUP
	AuthAndResignAsIMP x17, x12	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

因为这里传递过来的参数是NORMAL,所以只会走这句TailCallCachedImp x17, x12 // authenticate and call imp

3.1.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

因为入参是NORMAL,所以会走__objc_msgSend_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

其中调用了MethodTableLookup 重要的地方已注释

.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
	......省略

.endmacro

其实MethodTableLookup做的事情很简单,就是先对一些参数进行处理,然后调用__class_lookupMethodAndLoadCache3这个C函数进行下一步的方法查找。

3.1.5 慢速查找小结

    1. 首先从objc_msgSend进来,进行一些异常处理。
    1. 通过isa获取到对应的class
    1. 通过指针偏移的方式获取到cache_t
    1. 通过对cache_tkey & mask 获取到下标,查找到对应的bucket,获取到其中的IMP.
    1. 如果没有找到,会调用__objc_msgSend_uncached函数,最终调用__class_lookupMethodAndLoadCache3进入慢速查找流程。

3.2 慢速查找

3.2.1 _class_lookupMethodAndLoadCache3

接下来我么会研究__class_lookupMethodAndLoadCache3这个函数,但是发现全局搜索是搜索不到这个方法的。

这里用到之前的一个小技巧,直接打开汇编

原来是因为方法前面只有一个下换线,这个小细节需要注意。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    /*
     cls:如果是实例方法那就是类,如果是类方法那就是元类
     sel:方法名
     obj:方法调用者
     */
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

注意:

  • 对象方法,查找的是类对象。
  • 类方法,查找的是元类对象。

3.2.2 lookUpImpOrForward

lookUpImpOrForward整个方法将分成两部分就行分析,首先我们得明确一点,当汇编快速查找没有找到的时候才会来到这里。
已加注释

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();
    
    //如果需要从缓存里面查找,那需要先从缓存里面找
    // 第一次进入为 false , 因为汇编快速查找流程没找到进入.
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);

    /** - 这里是查找钱的一些准备条件为查找方法做准备条件,判断类有没有加载好。
        - 如果没有加载好,那就先加载一下类信息,准备好父类、元类
        - 只会加载一次. 
        - 具体实现可以参照realizeClass.
       */
    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:    
   /* retry部分的代码过长,并且是整个方法的核心和分析的重点,下面会进行分析  */
 done:
    runtimeLock.unlock();

    return imp;
}

下面我们来看下方法调用的情况。

上面分析过,要走到这个方法,那再快速查找阶段一定是没有找到缓存的,所以这里的cache一定是NO。

3.2.3 retry:分析

1. 查找本类
// 查找本类缓存
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;
        }
    }
  • 在本类的方法列表中查找,找到就进行缓存填充(log_and_fill_cache),并调用done,否则继续下面的操作。
  • 其实这里还有一个有意思的地方,log_and_fill_cache,最终这个方法会走到 上篇文章 所讲到的内容,cache_fill_nolock,缓存的入口,有兴趣的可以回过头看一看,串一串。
2.循环查找父类
{
        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.
                    // 找到IMP,把方法缓存
                    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;
            }
        }
    }

这段代码就是从当前类的父类开始沿着继承链循环查找,直到superClass是nil,先查找父类的cache缓存,再查找父类的 ro 中的方法列表。需要注意是,当在父类中找了IMP,会把IMP存到当前类的缓存中,而不是父类。

3.动态方法解析

如果上面的流程走完,还没有找到,就会到下面这部分

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;
    }
  • 这里我们发现,执行了_class_resolveMethod(cls, sel, inst)之后,又是goto retry,重新进行了一次方法查找。>

我们先看下_class_resolveMethod(cls, sel, inst)做了什么

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
  • 先判断类是不是元类,如果类是元类,那方法就是类方法 调用 _class_resolveInstanceMethod。
  • 如果不是元类,那么方法就是实例方法,调用 _class_resolveClassMethod。
  • 调用 lookUpImpOrNil 如果没找到 , 调用 _class_resolveInstanceMethod
class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
// 这个方法NSObject已经实现,返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

  • 先判断SEL_resolveInstanceMethod有没有实现,必然是已经实现的,因为这个方法NSObject已经实现,返回NO。
  • 然后向本类发送了SEL_resolveInstanceMethod消息,能响应吗? 肯定是能的。
  • 然后完成之后执行goto retry;操作。
消息转发入口

如果经过上面在本类,和父类循环查找,动态解析,都没有处理的情况下,就来到这里,进行消息转发,也是苹果给我们处理崩溃预留的最后一条途径。

// No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

3.2.4 慢速查找流程总结

  • 调用_class_lookupMethodAndLoadCache3,继而调用lookUpImpOrForward方法
  • 先从当前类的方法列表中查找,找到了返回
  • 找不到交给父类,先从父类的缓存中查找,如果找到返回,如果没有,查找方法列表,找到了返回,找不到进行动态方法解析。
  • 根据当前是类还是元类来进行对象方法动态解析和类方法动态解析。
  • 如果解析成功,则返回,如果失败,进入消息转发流程。

四:全文总结

  • Objective-C调用一个实例方法[p eat]
  • 实际会调用函数objc_msgSend(p, sel_registerName("eat"))
  • objc_msgSend()是一个汇编函数入口
  • objc_msgSend()会先从Person类的cache中查找eat对应的IMP
  • 如果没有找到缓存,会进入慢速查找流程
  • 调用_class_lookupMethodAndLoadCache3,继而调用lookUpImpOrForward方法
  • 先从当前类的方法列表中查找,找到了返回
  • 找不到交给父类,先从父类的缓存中查找,如果找到返回,如果没有,查找方法列表,找到了返回,找不到进行动态方法解析。
  • 根据当前是类还是元类来进行对象方法动态解析和类方法动态解析。
  • 如果解析成功,则返回,如果失败,进入消息转发流程。

本篇我们研究了消息查找的底层实现和流程,下一章将会沿着本章的路线研究消息转发的流程。敬请期待~