Runtime底层探究

406 阅读9分钟

Runtime是什么?

Runtime简称运行时,也就是系统在运行时候的一些机制,其中最重要的是消息机制。 Runtime基本是用C、C++和汇编写的,这是为了能够使动态系统更加高效。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的

SEL和IMP又是什么?SEL与IMP有什么关系?

我们先看代码执行:

16307494668900.jpg
之所以是这样的打印结果,那是因为方法编译会编译成函数,函数中默认会有2个参数(id self, SEL _cmd) ,self是消息接受者,_cmd是方法编号。

SEL是方法编号:方法编号的作用就是来找到方法的。
IMP是指向函数具体实现的指针。
对于SEL与IMP的关系,可以举个例子来说,我们有一本书籍,我可以通过书籍目录里的页面,找到想找的内容的开始。这个跟SEL和IMP的关系就是一致的了。 SEL就相当于目录,IMP就相当于内容的开始页码,函数具体实现相当于章节内容。

SEL又是怎么找到IMP的?

首先创建一个Father类,和它的一个子类Son,Father实现一个实例方法instanceMethod

// Father
@interface Father : NSObject
- (void)instanceMethod;
@end
#import "Father.h"

@implementation Father
- (void)instanceMethod {
    NSLog(@"%s", __func__);
}
@end

// Son
@interface Son : Father

@end
#import "Son.h"

@implementation Son

@end

16307504383945.jpg 当我们开启Debug的汇编模式,下看下面代码: 16307504079987.jpg 16307505259311.jpg 调用instanceMethod会有一个objc_msgSend,那么这个objc_msgSend是什么呢?我们可以加一个符号断点来看一下 16307507107758.jpg 首先关闭这个断点,先让程序执行到[a instanceMethod];这一行的断点,然后开启符号断点,继续运行 16307508369231.jpg 这说明objc_msgSend是在objc这个源码中,这个源码可以从opensource下载到,直接搜索objc即可,本次使用的是
16307512885910.jpg
在源码中查找
16307516825839.jpg

方法的快速查找

因为objc_msgSend是用汇编写的,所以我们找arm64下面的ENTRY _objc_msgSend

	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

我们可以看到,在_objc_msgSend下面先走的CacheLookup,通过名字和注释也能大致理解,这里有方法缓存机制,可以通CacheLookup来查找,找不到执行objc_msgSend_uncached
那么我们先看CacheLookup的源码

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	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

在源码中可以看到,查找包含3种情况: 第一种:从找CacheHit中到了,那里直接调用或返回

1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp

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

第二种:没有找到,CheckMiss

2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	
.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

第三种,添加进缓存,方便下次查找

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.

这里,第二种找不到的情况下,应该对应着后续操作,因为我们是从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

从中我们发现了__class_lookupMethodAndLoadCache3,方法名表示这是查找方法添加缓存,但是我们搜索不到了。但是呢,这个源码是用汇编、C、C++混合实现的,我们可以看一下是不是要到C或C++中查找,方法是去掉最前面一个"_",搜索_class_lookupMethodAndLoadCache3,很好,找到了。我们先不去继续探究源码了。 上面这部分内容,其实就是方法查找中的快速查找部分。
当进入_objc_msgSend消息发送后,先使用CacheLookup NORMAL通过汇编了快速查找缓存,因为汇编执行更快,效率更高,所以这部分是快速查找阶段,但CacheHit中,没有发现的时候,就通过MethodTableLookup调用_class_lookupMethodAndLoadCache3进入了慢速查找.

方法的慢速查找

我们继续来看_class_lookupMethodAndLoadCache3部分的源码,这个在objc-runtime-new.mm中,之所以有new这个命名,是因为runtime其实有过2版,我们现在用的都是新版。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

我们可以看到,调用了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;
    }

虽然我们本次传入cache为NO,不需要走缓存查找,但我们能够理解这里的,顺便进去看一下cache_getImp

/********************************************************************
 * IMP cache_getImp(Class cls, SEL sel)
 *
 * On entry:	a1 = class whose cache is to be searched
 *		a2 = selector to search for
 *
 * If found, returns method implementation.
 * If not found, returns NULL.
 ********************************************************************/

	STATIC_ENTRY _cache_getImp

// do lookup
	movq	%a1, %r10		// move class to r10 for CacheLookup
	CacheLookup NORMAL, GETIMP	// returns IMP on success

LCacheMiss:
// cache miss, return nil
	xorl	%eax, %eax
	ret

	END_ENTRY _cache_getImp

可见就是快速查找,那块流程。好了,继续看方法查找源码

retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
    // 查找当前类缓存
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

这里为什么又要从缓存里去获取呢?这是因为现在是慢速查找,可能在查找过程中,有地方可能通过runtime的addMethod等方式动态添加了方法,如果不查缓存,本次就找不到了,所以先从缓存查找一遍。然后会去查找类的方法列表。

从类的方法列表查找

 // Try this class's method lists.
    {
        // 从类的方法列表获取方法,如果没找到,返回0x0,即nil
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
        // 如果找到方法,先加入当前类的缓存,这里是为了下次能快速查找,然后返回
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

我们看一下getMethodNoSuper_nolock

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;
}

可见是从类的data()中的methodlist查找的。 这里先打断一下,有个基础知识是必须知道的,那就是示例方法是存在类对象中的,类方法是存在元类对象中的 16307552115287.jpg

从父类的方法列表查找

回去继续看getMethodNoSuper_nolock的代码,如果当前类中没找方法,那么会尝试从父类中去查找

// Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        // curClass先为当前类的父类,然后通过for循环一直找父类,直到找到NSObject
        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;
            }
        }
    }

那么如果一直找的NSObject都没找到方法呢?那么就会进入动态解析流程

动态方法解析

 // 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;
    }

我们进去_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

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 判断是否重写重写了  + (BOOL)resolveInstanceMethod:(SEL)name  方法
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
    // 没重写,返回
        // Resolver not implemented.
        return;
    }

    // 发送消息,即调用[cls resolveInstanceMethod]方法.这又会进行方法查找了
    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));
        }
    }
}

元类执行_class_resolveClassMethod

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // 判断是否重写了  + (BOOL)resolveClassMethod:(SEL)name   方法
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        // 没重写,返回
        return;
    }

    // 发送消息,即调用[cls resolveClassMethod]方法.这又会进行方法查找了
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() 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 resolveClassMethod:%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));
        }
    }
}

动态解析部分源码分析已经结束了,下面先看一下动态解析如何使用吧。

动态解析使用简单示例

我们可以基于runtime来动态的给类添加原本未实现的方法,以实例方法missMethod为例

#include <objc/runtime.h>  
  
void dynamicMethodIMP(id self, SEL _cmd) {  
    NSLog(@" >> dynamicMethodIMP");  
} 
+ (BOOL)resolveInstanceMethod:(SEL)name {     
    NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));  
    if (name == @selector(missMethod)) {  
    / 如果是missMethod给该SEL绑定对应的IMP:dynamicMethodIMP
        class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");  
        return YES;  
    }  
      
    return [super resolveInstanceMethod:name];  
}  

关于"v@:":这个其实是返回值和参数的标记。第一个字符表示返回值,"v"表示无返回值,后续每个字符表示一个参数,因为每个函数默认2个参数,"@"表示对象,":"表示SEL。更多字符含意见Type Encodings

消息转发

如果没有添加动态方法解析呢,那么就会进入消息转发

/ 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

再往下进行的代码没找到,先这样吧。消息转发分为消息快速转发和消息慢速转发。
下面讲一下消息转发的简单示例。

消息快速转发

forwardingTargetForSelector可以把方法传递给一个能处理该方法的类的实例来处理方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(missMethod)) {
        // 让能处理missMethod方法的实例来处理
        return [Other new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果没有实现forwardingTargetForSelector,或者没有转发方法到其他实例会进入消息慢速转发。

消息慢速转发

分为2步,先要实现methodSignatureForSelector方法,返回一个方法签名

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(missMethod1)) {
        NSMethodSignature *ms = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return ms;
    }else if (aSelector == @selector(missMethod2)) {
        Method method = class_getInstanceMethod([self class], @selector(xxxFunc));
        const char *type - method_getTypeEncoding(method);
        return [NSMethodSignature signatureWithObjCTypes:type];
    }
    return [super methodSignatureForSelector:aSelector];
}

还要实现forwardInvocation方法

+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s-%@",__func__, NSStringFromSelector(anInvocation.selector));
    NSString *sto = @"参数";
    // 能够处理方法的类
    anInvocation.target = [OtherClass class];
    // 参数要从index=2添加,01被id和SEL占用了
    [anInvocation setArgument:&sto atIndex:2];
    NSLog(@"%@",anInvocation.methodSignature);
    // 调用哪个方法
    anInvocation.selector = @selector(run:);
    [anInvocation invoke];
}

附一张别人整理的消息发送流程图:

16307632558478.jpg