探祕Runtime - Runtime消息發送機制

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/014af0de67cdgit


博客配圖


方法調用

在OC中方法調用是經過Runtime實現的,Runtime進行方法調用本質上是發送消息,經過objc_msgSend()函數進行消息發送。github

例以下面的OC代碼會被轉換爲Runtime代碼。數組

原方法:[object testMethod]
轉換後的調用:objc_msgSend(object, @selector(testMethod));
複製代碼

發送消息的第二個參數是一個SEL類型的參數,在項目裏常常會出現,不一樣的類定義了相同的方法,這樣就會有相同的SEL。那麼問題就來了,也是不少人博客裏都問過的一個問題,不一樣類的SEL是同一個嗎?緩存

然而,事實是經過咱們的驗證,建立兩個不一樣的類,並定義兩個相同的方法,經過@selector()獲取SEL並打印。咱們發現SEL都是同一個對象,地址都是相同的。由此證實,不一樣類的相同SEL是同一個對象。多線程

@interface TestObject : NSObject
- (void)testMethod;
@end

@interface TestObject2 : NSObject
- (void)testMethod;
@end

// TestObject2實現文件也同樣
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end

// 結果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
複製代碼

Runtime中維護了一個SEL的表,這個表存儲SEL不按照類來存儲,只要相同的SEL就會被看作一個,並存儲到表中。在項目加載時,會將全部方法都加載到這個表中,而動態生成的方法也會被加載到表中。架構

隱藏參數

咱們在方法內部能夠經過self獲取到當前對象,可是self又是從哪來的呢?函數

方法實現的本質也是C函數,C函數除了方法傳入的參數外,還會有兩個默認參數,這兩個參數在經過objc_msgSend()調用時也會傳入。這兩個參數在Runtime中並無聲明,而是在編譯時自動生成的。源碼分析

objc_msgSend的聲明中能夠看出這兩個隱藏參數的存在。佈局

objc_msgSend(void /* id self, SEL op, ... */ )
複製代碼
  • self,調用當前方法的對象。
  • _cmd,當前被調用方法的SEL

雖然這兩個參數在調用和實現方法中都沒有明確聲明,可是咱們仍然可使用它。響應對象就是self,被調用方法的selector_cmd優化

- (void)method {
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
複製代碼

函數調用

一個對象被建立後,自身的類及其父類一直到NSObject類的部分,都會包含在對象的內存中,例如其父類的實例變量。當經過[super class]的方式調用其父類的方法時,會建立一個結構體。

struct objc_super { id receiver; Class class; };
複製代碼

super的調用會被轉化爲objc_msgSendSuper()的調用,並在其內部調用objc_msgSend()函數。有一點須要注意,儘管是經過[super class]的方式調用的,但傳入的receiver對象仍然是self,返回結果也是selfclass由此可知,當前對象不管調用任何方法,receiver都是當前對象。

objc_msgSend(objc_super->receiver, @selector(class))
複製代碼

objc_msg.s中,存在多個版本的objc_msgSend函數。內部實現邏輯大致一致,都是經過彙編實現的,只是根據不一樣的狀況有不一樣的調用。

objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret
複製代碼

在上面源碼中,帶有super的會在外界傳入一個objc_super的結構體對象。stret表示返回的是struct類型,super2objc_msgSendSuper()的一種實現方式,不對外暴露。

struct objc_super {
	id	receiver;
	Class	class;
};
複製代碼

fp則表示返回一個long double的浮點型,而fp2則返回一個complex long double的複雜浮點型,其餘floatdouble的普通浮點型都用objc_msgSend。除了上面這些狀況外,其餘都經過objc_msgSend()調用。

消息發送流程

當一個對象被建立時,系統會爲其分配內存,並完成默認的初始化工做,例如對實例變量進行初始化。對象第一個變量是指向其類對象的指針-isaisa指針能夠訪問其類對象,而且經過其類對象擁有訪問其全部繼承者鏈中的類。

調用順序

isa指針不是語言的一部分,主要爲Runtime機制提供服務。

當對象接收到一條消息時,消息函數隨着對象isa指針到類的結構體中,在method list中查找方法selector。若是在本類中找不到對應的selector,則objc_msgSend會向其父類的method list中查找selector,若是還不能找到則沿着繼承關係一直向上查找,直到找到NSObject類。

Runtimeselector查找的過程作了優化,爲類的結構體中增長了cache字段,每一個類都有獨立的cache,在一個selector被調用後就會加入到cache中。在每次搜索方法列表以前,都會先檢查cache中有沒有,若是沒有才調用方法列表,這樣會提升方法的查找效率。

若是經過OC代碼的調用都會走消息發送的階段,若是不想要消息發送的過程,能夠獲取到方法的函數指針直接調用。經過NSObjectmethodForSelector:方法能夠獲取到函數指針,獲取到指針後須要對指針進行類型轉換,轉換爲和調用函數相符的函數指針,而後發起調用便可。

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);
複製代碼

實現原理

Runtime中,objc_msgSend函數也是開源的,但其是經過彙編代碼實現的,arm64架構代碼能夠在objc-msg-arm64.s中找到。在Runtime中,不少執行頻率比較高的函數,都是用匯編寫的。

objc_msgSend並非徹底開源的,在_class_lookupMethodAndLoadCache3函數中已經獲取到Class參數了。因此在下面中有一個確定是對象中獲取isa_t的過程,從方法命名和註釋來看,應該是GetIsaFast彙編命令。若是這樣的話,就能夠從消息發送到調用流程銜接起來了。

ENTRY	_objc_msgSend
	MESSENGER_START

	NilTest	NORMAL

	GetIsaFast NORMAL		// r11 = self->isa
	CacheLookup NORMAL		// calls IMP on success

	NilTestSupport	NORMAL

	GetIsaSupport	   NORMAL

// cache miss: go search the method lists
LCacheMiss:
	// isa still in r11
	MethodTableLookup %a1, %a2	// r11 = IMP
	cmp	%r11, %r11		// set eq (nonstret) for forwarding
	jmp	*%r11			// goto *imp

	END_ENTRY	_objc_msgSend
複製代碼
  • MESSENGER_START:消息開始執行。
  • NilTest:判斷接收消息的對象是否爲nil,若是爲nil則直接返回,這就是對nil發送消息無效的緣由。
  • GetIsaFast:快速獲取到isa指向的對象,是一個類對象或元類對象。
  • CacheLookup:從ache list中獲取緩存selector,若是查到則調用其對應的IMP
  • LCacheMiss:緩存沒有命中,則執行此條彙編下面的方法。
  • MethodTableLookup:若是緩存中沒有找到,則從method list中查找。

cache_t

若是每次進行方法調用時,都按照對象模型來進行方法列表的查找,這樣是很消耗時間的。Runtime爲了優化調用時間,在objc_class中添加了一個cache_t類型的cache字段,經過緩存來優化調用時間。

在執行objc_msgSend函數的消息發送過程當中,同一個方法第一次調用是沒有緩存的,但調用以後就會存在緩存,以後的調用就直接調用緩存。因此方法的調用,能夠分爲有緩存和無緩存兩種,這兩種狀況下的調用堆棧是不一樣的。

首先是從緩存中查找IMP,可是因爲cache3調用lookUpImpOrForward函數時,已經查找過cache了,因此傳入的是NO,不進入查找cahce的代碼塊中。

struct cache_t {
    // 存儲被緩存方法的哈希表
    struct bucket_t *_buckets;
    // 佔用的總大小
    mask_t _mask;
    // 已使用大小
    mask_t _occupied;
}

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};
複製代碼

當給一個對象發送消息時,Runtime會沿着isa找到對應的類對象,但並不會馬上查找method_list,而是先查找cache_list,若是有緩存的話優先查找緩存,沒有再查找方法列表。

這是Runtime對查找method的優化,理論上來講在cache中的method被訪問的頻率會更高。cache_listcache_t定義,內部有一個bucket_t的數組,數組中保存IMPkey,經過key找到對應的IMP並調用。具體源碼能夠查看objc-cache.mm

若是類對象沒有被初始化,而且lookUpImpOrForward函數的initialize參數爲YES,則表示須要對該類進行建立。函數內部主要是一些基礎的初始化操做,並且會遞歸檢查父類,若是父類未初始化,則先初始化其父類對象。

STATIC_ENTRY _cache_getImp

	mov	r9, r0
	CacheLookup NORMAL
	// cache hit, IMP in r12
	mov	r0, r12
	bx	lr			// return imp
	
	CacheLookup2 GETIMP
	// cache miss, return nil
	mov	r0, #0
	bx	lr

	END_ENTRY _cache_getImp
複製代碼

下面會進入cache_getImp的代碼中,然而這個函數不是開源的,可是有一部分源碼能夠看到,是經過彙編寫的。其內部調用了CacheLookupCacheLookup2兩個函數,這兩個函數也都是彙編寫的。

通過第一次調用後,就會存在緩存。進入objc_msgSend後會調用CacheLookup命令,若是找到緩存則直接調用。可是Runtime並非徹底開源的,內部不少實現咱們依然看不到,CacheLookup命令內部也同樣,只能看到調用完命令後就開始執行咱們的方法了。

CacheLookup NORMAL, CALL
複製代碼

源碼分析

在上面objc_msgSend彙編實現中,存在一個MethodTableLookup的彙編調用。在這條彙編調用中,調用了查找方法列表的C函數。下面是精簡版代碼。

.macro MethodTableLookup
	
  // 調用MethodTableLookup並在內部執行cache3函數(C函數)
	blx	__class_lookupMethodAndLoadCache3
	mov	r12, r0			// r12 = IMP

.endmacro
複製代碼

MethodTableLookup中經過調用_class_lookupMethodAndLoadCache3函數,來查找方法列表。函數內部是經過lookUpImpOrForward函數實現的,在調用時cache字段傳入NO,表示不須要查找緩存了,由於在cache3函數上面已經經過彙編查找過了。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    // 經過cache3內部調用lookUpImpOrForward函數
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

lookUpImpOrForward函數是支持多線程的,因此內部會有不少鎖操做。其內部有一個rwlock_t類型的runtimeLock變量,有runtimeLock控制讀寫鎖。其內部有不少邏輯代碼,這裏把函數內部實現作了精簡,把核心代碼貼到下面。

經過類對象的isRealized函數,判斷當前類是否被實現,若是沒有被實現,則經過realizeClass函數實現該類。在realizeClass函數中,會設置versionrwsuperClass等一些信息。

// 執行查找imp和轉發的代碼
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 若是cache是YES,則從緩存中查找IMP。若是是從cache3函數進來,則不會執行cache_getImp()函數
    if (cache) {
        // 經過cache_getImp函數查找IMP,查找到則返回IMP並結束調用
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();

    // 判斷類是否已經被建立,若是沒有被建立,則將類實例化
    if (!cls->isRealized()) {
        // 對類進行實例化操做
        realizeClass(cls);
    }

    // 第一次調用當前類的話,執行initialize的代碼
    if (initialize  &&  !cls->isInitialized()) {
        // 對類進行初始化,並開闢內存空間
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
    
 retry:    
    runtimeLock.assertReading();

    // 嘗試獲取這個類的緩存
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    
    {
        // 若是沒有從cache中查找到,則從方法列表中獲取Method
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 若是獲取到對應的Method,則加入緩存並從Method獲取IMP
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    {
        unsigned attempts = unreasonableClassCount();
        // 循環獲取這個類的緩存IMP 或 方法列表的IMP
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 獲取父類緩存的IMP
            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_t對象。若是找到則緩存查找到的IMP
            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.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // 若是沒有IMP被發現,而且動態方法解析也沒有處理,則進入消息轉發階段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}
複製代碼

在方法第一次調用時,能夠經過cache_getImp函數查找到緩存的IMP。但若是是第一次調用,就查不到緩存的IMP,就會進入到getMethodNoSuper_nolock函數中執行。下面是getMethod函數的關鍵代碼。

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // 根據for循環,從methodList列表中,從頭開始遍歷,每次遍歷後向後移動一位地址。
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        // 對sel參數和method_t作匹配,若是匹配上則返回。
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
複製代碼

當調用一個對象的方法時,查找對象的方法,本質上就是遍歷對象isa所指向類的方法列表,並用調用方法的SEL和遍歷的method_t結構體的name字段作對比,若是相等則將IMP函數指針返回。

// 根據傳入的SEL,查找對應的method_t結構體
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        for (auto& meth : *mlist) {
            // SEL本質上就是字符串,查找的過程就是進行字符串對比
            if (meth.name == sel) return &meth;
        }
    }

    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }

    return nil;
}
複製代碼

getMethod函數中,主要是對Classmethods方法列表進行查找和匹配。類的方法列表都在Classclass_data_bits_t中,經過data()函數從bits中獲取到class_rw_t的結構體,而後獲取到方法列表methods,並遍歷方法列表。

若是從當前類中獲取不到對應的IMP,則進入循環中。循環是從當前類出發,沿着繼承者鏈的關係,一直向根類查找,直到找到對應的IMP實現。

查找步驟和上面也同樣,先經過cache_getImp函數查找父類的緩存,若是找到則調用對應的實現。若是沒找到緩存,表示第一次調用父類的方法,則調用getMethodNoSuper_nolock函數從方法列表中獲取實現。

for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
{
    imp = cache_getImp(curClass, sel);
    if (imp) {
        if (imp != (IMP)_objc_msgForward_impcache) {
            log_and_fill_cache(cls, imp, sel, inst, curClass);
            goto done;
        }
    }
    
    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參數是否爲YES,而且會判斷是否已經有過動態決議,由於下面是goto retry,因此這段代碼可能會執行屢次。

if (resolver  &&  !triedResolver) {
    _class_resolveMethod(cls, sel, inst);
    triedResolver = YES;
    goto retry;
}
複製代碼

若是知足條件而且是第一次進行動態方法決議,則進入if語句中調用_class_resolveMethod函數。動態方法決議有兩種,_class_resolveClassMethod類方法決議和_class_resolveInstanceMethod實例方法決議。

BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
複製代碼

在這兩個動態方法決議的函數實現中,本質上都是經過objc_msgSend函數,調用NSObject中定義的resolveInstanceMethod:resolveClassMethod:兩個方法。

能夠在這兩個方法中動態添加方法,添加方法實現後,會在下面執行goto retry,而後再次進入方法查找的過程當中。從triedResolver參數能夠看出,動態方法決議的機會只有一次,若是此次再沒有找到,則進入消息轉發流程。

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼

若是通過上面這些步驟,仍是沒有找到方法實現的話,則進入動態消息轉發中。在動態消息轉發中,還能夠對沒有實現的方法作一些彌補措施。

下面是經過objc_msgSend函數發送一條消息後,所通過的調用堆棧,調用順序是從上到下的。

CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
複製代碼

調用總結

在調用objc_msgSend函數後,會有一系列複雜的判斷邏輯,總結以下。

  1. 判斷當前調用的SEL是否須要忽略,例如Mac OS中的垃圾處理機制啓動的話,則忽略retainrelease等方法,並返回一個_objc_ignored_methodIMP,用來標記忽略。
  2. 判斷接收消息的對象是否爲nil,由於在OC中對nil發消息是無效的,這是由於在調用時就經過判斷條件過濾掉了。
  3. 從方法的緩存列表中查找,經過cache_getImp函數進行查找,若是找到緩存則直接返回IMP
  4. 查找當前類的method list,查找是否有對應的SEL,若是有則獲取到Method對象,並從Method對象中獲取IMP,並返回IMP(這步查找結果是Method對象)。
  5. 若是在當前類中沒有找到SEL,則去父類中查找。首先查找cache list,若是緩存中沒有則查找method list,並以此類推直到查找到NSObject爲止。
  6. 若是在類的繼承體系中,始終沒有查找到對應的SEL,則進入動態方法解析中。能夠在resolveInstanceMethodresolveClassMethod兩個方法中動態添加實現。
  7. 動態消息解析若是沒有作出響應,則進入動態消息轉發階段。此時能夠在動態消息轉發階段作一些處理,不然就會Crash

總體分析

整體能夠被分爲三部分:

  1. 剛調用objc_msgSend函數後,內部的一些處理邏輯。
  2. 複雜的查找IMP的過程,會涉及到cache listmethod list等。
  3. 進入消息轉發階段。

cache list中找不到方法的狀況下,會經過MethodTableLookup宏定義從類的方法列表中,查找對應的方法。在MethodTableLookup中本質上也是調用_class_lookupMethodAndLoadCache3函數,只是在傳參時cache字段傳NO,表示不從cache list中查找。

cache3函數中,是直接調用的lookUpImpOrForward函數,這個函數內部實現很複雜,能夠看一下Runtime Analyze。在這個裏面直接搜lookUpImpOrForward函數名便可,能夠詳細看一下內部實現邏輯。


簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github上,下載Runtime PDF合集。把全部Runtime文章總計九篇,都寫在這個PDF中,並且左側有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!😁