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

來源:Effective 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 錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章