天天看點

Objective-C 消息機制學習總結

    message 機制可以說是 objc 最重要的機制。零零散散看了 method cache、method search 以及 message forward,今天把三者串起來總結一下。 message 機制帶來很大的靈活性,調用和實作完全解耦,實作上也力求高效,非常值得學習。流程概括來說可以分為三步

  1. 查 cache ,命中則傳回 imp,miss 進入下一步
  2. 搜尋 method list,found 則傳回 imp,not found 進入下一步
  3. resolve method or forward message ,otherwise cann’t respond to selector

一、查 cache

    查 cache 是用彙編實作的。了解算法(流程)之前我們先了解一下資料結構(記憶體模型)。我們知道對象記憶體模型裡面有個 isa 指針,而 isa_t 結構體裡面主要是一個類指針,另外還有一些位标志資料(has_assoc、weakly_referenced、extra_rc 等)。類對象模型裡面,首先是 superClass,接下來就是我們的主角 cache。cache 是個哈希數組,使用線性探測再散列的方式解決沖突。

struct objc_object {
private:
    isa_t isa;
...

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
...

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
...

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
...

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
...
           

    然後到了流程這塊兒,參考這篇文章,結合源碼來說說。這部分源碼就在 runtime 源碼中,objc-msg-arm64.s,彙編代碼。

.macro CacheLookup
	// x1 = SEL, x9 = isa
	ldp	x10, x11, [x9, #CACHE]	// x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4	// x12 = buckets + ((_cmd & mask)<<4)

	ldp	x16, x17, [x12]		// {x16, x17} = *bucket
1:	cmp	x16, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->cls == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x16, x17, [x12, #-16]!	// {x16, x17} = *--bucket
	b	1b			// loop

3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4	// x12 = buckets+(mask<<4)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	x16, x17, [x12]		// {x16, x17} = *bucket
1:	cmp	x16, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->cls == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x16, x17, [x12, #-16]!	// {x16, x17} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
           

    參考文章解析的非常詳細了。我說一下我的了解。首先程式運作至此,argument 已經塞到了寄存器中,如注釋,x1 裡面是 sel,x9 裡面是 isa,isa 的類型是 objc_class,它的結構是這樣的,superclass 指針之後就是 cache 指針。

    

ldp x10, x11, [x9, #CACHE]

其中 CACHE 是宏,值是 16,因為結構體裡還有一個 superclass。如注釋,這時 x10 裡是 buckets,x11 裡面是 mask 和 occupied,它們是 32 位的,可以一起放到 x11 中。

and w12, w1, w11

如注釋,sel 和 mask 相與,放到 w12 中,是最簡單的 hash,确定 target bucket index。

add x12, x10, x12, LSL #4

如注釋,計算 target bucket 的位址。

ldp x16, x17, [x12]

讀取 bucket 的内容,分别是 sel 和 imp,放在 x16, x17 當中。

二、在 method list 裡查找 imp

    整個流程可以看 lookUpImpOrForward 方法。中間還涉及類的 realize、查父類方法清單等細節。這裡先單拎 getMethodNoSuper_nolock 說說,調用連結下來是周遊類的方法清單的清單,調用 search_method_list 搜尋每一個方法清單,

/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
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 {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
           

    在這個方法裡面,根據方法清單是否排過序,分别調用 findMethodInSortedMethodList 進行二分查找或循環查找。

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
           

三、resolve & forward

Objective-C 消息機制學習總結

    resolve 如果 cls 是 meta class,代表 object 是 class,調用 _class_resolveClassMethod,否則調用 _class_resolveInstanceMethod。這個時候是我們可以進行 hook 的第一個機會,我們可以在這個時候把實作添加到 method list 中。在這之後,lookUpImpOrForward 會 goto retry,上面的流程都會再走一遍(try this class’s cache,try this class’s method lists,try superclass caches and method lists,try resolve method)當然會有一個是否已經嘗試了 resolve 的辨別,防止循環的 go to retry。如果 resolve 也沒有提供 imp,runtime 會進行 forward。

    forward 是我們可以進行 hook 的第二個機會。runtime 會把上面的流程都沒法識别的 sel 對應的的 imp 設為 _objc_msgForward_impcache。用彙編實作。一個是效率考慮。一個是函數參數的傳遞考慮。(這個彙編注釋比較少,看不懂,下次再琢磨。看它怎麼再回調 c 的代碼轉移控制流的。這裡沒弄明白控制流是怎樣的。)根據文檔,runtime 會先後調用 forwardingTargetForSelector 和 forwardInvocation 這個兩個方法繼續嘗試進行消息的響應。forwardingTargetForSelector 是為了你快速的把消息轉發給别的對象而進行的便捷設計。forwardInvocation 則是給你最大的自由度去修改這個 invocation。如果你不實作這個方法,就會調用 NSObject 的預設實作,預設實作調用了 doesNotRecognizeSelector 方法,産生 unrecognized selector sent to instance 這個我們熟悉的 runtime 錯誤。