阅读 127

iOS-Runtime之方法查找

一、Runtime简介

一套以c、c++以及汇编写成的,可以为Object-C提供运行时功能的api。源码参考

Runtime其实有两个版本: “modern” 和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。

这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

二、消息发送的本质

1、objc_msgSend

熟悉Object-C的同学都知道,对象的方法调用在底层其实是一个消息发送的过程,接下来我们验证一下。

首先定义一个对象Son,它包含一个实例方法。然后我们在main.m中调用,再通过clang -rewrite-objc main.m,生成一个.cpp文件,对比如下:

--------------main.m---------------
Son *son = [Son new];
[son son_instanceSelector];

--------------main.cpp---------------
Son *son = ((Son *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Son"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)son, sel_registerName("son_instanceSelector"));
复制代码

可以分析得出,对象的方法调用在底层都被编译成了objc_msgSend(id _Nullable self, SEL _Nonnull op, …)

2、objc_msgSendSuper

再来看他的父类方法调用,声明两个类,Father、Son(继承Father),然后在Son的实例方法与类方法中分别实现父类对应的实例方法与类方法,然后在通过clang编译,比较:

-----------------------------Son.m-----------------------------
-(void)son_instanceSelector{
    [super father_instanceSelector];
}

+(void)son_classSelector{
    [super father_classSelector];
}

-----------------------------Son.cpp---------------------------
static void _I_Son_son_instanceSelector(Son * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("father_instanceSelector"));
}

static void _C_Son_son_classSelector(Class self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(&(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getMetaClass("Son"))}, sel_registerName("father_classSelector"));
}
复制代码

可以得出,super的方法调用在底层会转化为objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),就是去父类的方法列表中查找方法,然后调用。

注意:方法调用的主体还是子类对象。

3、方法调用汇总

所以,我们平时的方法调用使用Runtime底层函数来实现是什么样子呢?如下:

// 类对象实例方法调用
objc_msgSend(son, sel_registerName("son_instanceSelector"));

// 类方法调用
objc_msgSend(objc_getClass("Son"), sel_registerName("son_classSelector"));

// 向父类发消息(实例方法)
struct objc_super kmSuper;
kmSuper.receiver = son;
kmSuper.super_class = [Father class];
objc_msgSendSuper(&kmSuper, @selector(father_instanceSelector));

//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [son class];
myClassSuper.super_class = class_getSuperclass(objc_getMetaClass("Son"));
objc_msgSendSuper(&myClassSuper, NSSelectorFromString(@"father_classSelector"));
复制代码

三、方法查找流程

1、快速查找流程

objc_msgSend的快速查找流程是用汇编实现的,主要原因有

  • c语言不可能通过写一个函数来保留未知的参数并且跳转到一个人任意的函数指针。c语言没有满足做这件事情的必要特性。
  • 性能更高,汇编是更接近系统底层的语言。

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
    // person - isa - 类
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
复制代码
  • 1、正常判空处理
  • 2、TAGGED_POINTERS判断(后面文章再一起探究)
  • 3、通过isa指针拿到他的class(class中存储它的方法以及方法缓存)
  • 4、CacheLookup 查看方法缓存

接着查看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
复制代码
  • 1、通过isa偏移16位,拿到类的方法缓存中的buckets、以及occupied和mask(类的方法缓存

  • 2、查看是否缓存命中,有则retun imp

    通过汇编查找方法缓存,缓存命中,就是方法查找的快速流程,未命中则开始走方法查找的慢速流程。

  • 3、缓存未命中,调用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
复制代码

因为我们之前调用的CacheLookup 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
	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
复制代码

做了一些内存上的准备工作,然后调用函数**_class_lookupMethodAndLoadCache3:**

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

从这里开始,便从汇编进入到了C/C++。也就是真正的方法慢速查找流程。

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();

    // 判断是否需要从缓存查找,是则先去方法缓存查找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // 加锁,防止多线程并发引起的返回错误
    runtimeLock.lock();
    checkIsKnownClass(cls);
    
    // 方法查找之前的准备工作
    // 类、元类、以及父类、父类的元类、直到它的根类、根元类
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    
    // 确保类已经初始化完成
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    
 retry:    
    runtimeLock.assertLocked();

    // 先去类的方法缓存中查找一次,多线程并发调用时可能已经存在之前的调用缓存
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 在当前的类方法列表中查找
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        // 如果找到方法,先缓存,然后goto done(return imp)
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 查找父类的方法缓存和方法列表
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // 递归报错
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 先去父类的方法缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父类缓存中找到方法,将方法缓存在子类中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // 在父类的方法列表中查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 在父类的方法列表中找到方法,将方法缓存在子类中
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 没有找到方法实现,调用一次方法动态解析
    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;
    }

    // 未能找到方法实现,且方法的动态解析也没有用的时候
    // 就会走到消息转发流程(下篇文章会讲)
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

复制代码

方法的慢速查找遵循着一个规律,即先找类本身的方法,找不到则找父类方法,一直找到NSObject。

_class_resolveMethod:

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);
        }
    }
}
复制代码

如果我们在类中实现了resolveInstanceMethod或者resolveClassMethod方法并且正确处理了sel,可以避免程序报错。他会返回一个方法实现imp,并让程序去再次查找。 例:

我们在主线程中调用person对象一个并未实现的实例方法

Person *per = [Person alloc];
[per performSelector:@selector(run)];
复制代码

然后,在Person.m中加入动态解析函数:

----------------------Person.m----------------------
#import "Person.h"
#import <objc/runtime.h>

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        class_addMethod(self, sel, (IMP)methodImp, "v@:");
        return YES;
    }
    return NO;
}

void methodImp(id self,SEL _cmd){
    NSLog(@"来了老弟...");
}

@end
复制代码

打印结果:

来了老弟...

resolveClassMethod方法同理。

四、总结

方法查找的流程图如下:

(消息转发流程下篇分析)

关注下面的标签,发现更多相似文章
评论