iOS-Runtime之方法查找

1、Runtime簡介

一套以c、c++以及彙編寫成的,能夠爲Object-C提供運行時功能的api。源碼參考c++

Runtime其實有兩個版本: 「modern」 和 「legacy」。咱們如今用的 Objective-C 2.0 採用的是現行 (Modern) 版的 Runtime 系統,只能運行在 iOS 和 macOS 10.5 以後的 64 位程序中。而 maxOS 較老的32位程序仍採用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系統。api

這兩個版本最大的區別在於當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。緩存

2、消息發送的本質

一、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, …)app

二、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, ...),就是去父類的方法列表中查找方法,而後調用。oop

注意:方法調用的主體仍是子類對象。佈局

三、方法調用匯總

因此,咱們平時的方法調用使用Runtime底層函數來實現是什麼樣子呢?以下:post

// 類對象實例方法調用
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"));
複製代碼

3、方法查找流程

一、快速查找流程

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
複製代碼
  • 一、正常判空處理
  • 二、TAGGED_POINTERS判斷(後面文章再一塊兒探究)
  • 三、經過isa指針拿到他的class(class中存儲它的方法以及方法緩存)
  • 四、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
複製代碼
  • 一、經過isa偏移16位,拿到類的方法緩存中的buckets、以及occupied和mask(類的方法緩存

  • 二、查看是否緩存命中,有則retun imp

    經過彙編查找方法緩存,緩存命中,就是方法查找的快速流程,未命中則開始走方法查找的慢速流程。

  • 三、緩存未命中,調用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++。也就是真正的方法慢速查找流程。

二、慢速查找流程

在快速查找流程中,方法緩存未命中。也就是說,快速查找行不通的時候,底層就會走到慢速查找流程,並一路從彙編走到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方法同理。

4、總結

方法查找的流程圖以下:

(消息轉發流程下篇分析)
相關文章
相關標籤/搜索