iOS探索 方法的本質和訊息查詢流程

我是好寶寶發表於2020-02-04

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

書接上文說到cache_t快取的是方法,那麼方法又是什麼呢?這一切都要從Runtime開始說起

一、Runtime

1.什麼是Runtime?

Runtime是一套API,由c、c++、彙編一起寫成的,為OC提供了執行時

  • 執行時:程式碼跑起來,將可執行檔案裝載到記憶體
  • 編譯時:正在編譯的時間——翻譯原始碼將高階語言(OC、Swift)翻譯成機器語言(彙編等),最後變成二進位制

2.Runtime版本

Runtime有兩個版本——LegacyModern蘋果開發者文件都寫得清清楚楚

原始碼中-old__OBJC__代表Legacy版本,-new__OBJC2__代表Modern版本,以此做相容

3.Runtime的作用及呼叫

Runtime底層經過編譯會提供一套API和供FrameWorkService使用

iOS探索 方法的本質和訊息查詢流程

Runtime呼叫方式:

  • Runtime API,如 sel_registerName()
  • NSObject API,如 isKindOf()
  • OC上層方式,如 @selector()

原來平常在用的這麼多方法都是Runtime啊,那麼方法究竟是什麼呢?

二、方法的本質

1.研究方法

通過clang編譯成cpp檔案可以看到底層程式碼,得到方法的本質

  • 相容編譯(程式碼少):clang -rewrite-objc main.m -o main.cpp
  • 完整編譯(不報錯):xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

2.程式碼轉換

FXPerson *p = [FXPerson alloc];
[p fly];
複製程式碼
FXPerson *p = ((FXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FXPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("fly"));
複製程式碼
  • ((FXPerson *(*)(id, SEL))(void *)是型別強轉
  • (id)objc_getClass("FXPerson")獲取FXPerson類物件
  • sel_registerName("alloc")等同於@selector()

那麼可以理解為((型別強轉)objc_msgSend)(物件, 方法呼叫)

3.方法的本質

方法的本質是通過objc_msgSend傳送訊息,id是訊息接收者,SEL是方法編號

如果外部定義了C函式並呼叫如void fly() {},在clang編譯之後還是fly()而不是通過objc_msgSend去呼叫。因為傳送訊息就是找函式實現的過程,而C函式可以通過函式名——指標就可以找到

4.向不同物件傳送訊息

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface FXFather: NSObject
- (void)walk;
+ (void)run;
@end

@implementation FXFather
- (void)walk { NSLog(@"%s",__func__); }
+ (void)run { NSLog(@"%s",__func__); }
@end

@interface FXSon: FXFather
- (void)jump;
+ (void)swim;
@end
複製程式碼

子類FXSon有例項方法jump、類方法swim

父類FXFather有例項方法walk、類方法run

①傳送例項方法

訊息接收者——例項物件

FXSon *s = [FXSon new];
objc_msgSend(s, sel_registerName("jump"));
複製程式碼

②傳送類方法

訊息接收者——類物件

objc_msgSend(objc_getClass("FXSon"), sel_registerName("swim"));
複製程式碼

objc_msgSend不能向父類傳送訊息,需要使用objc_msgSendSuper,並給objc_super結構體賦值(在objc2中只需要賦值receiver、super_class)

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif
複製程式碼

③向父類傳送例項方法

receiver——例項物件;super_class——父類類物件

struct objc_super superInstanceMethod;
superInstanceMethod.receiver = s;
superInstanceMethod.super_class = objc_getClass("FXFather");
objc_msgSendSuper(&superInstanceMethod, sel_registerName("walk"));
複製程式碼

④向父類傳送類方法

receiver——類物件;super_class——父類元類物件

struct objc_super superClassMethod;
superClassMethod.receiver = [s class];
superClassMethod.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&superClassMethod, sel_registerName("run"));
複製程式碼

如果出現Too many arguments to function call, expected 0, have 2問題,來到BuildSetting把配置修改成如下圖

iOS探索 方法的本質和訊息查詢流程

三、訊息查詢流程

訊息查詢流程其實是通過上層的方法編號sel傳送訊息objc_msgSend找到具體實現imp的過程

objc_msgSend是用匯編寫成的,至於為什麼不用C而是用匯編寫,是因為:

  • C語言不能通過寫一個函式,保留未知的引數,跳轉到任意的指標,而彙編有暫存器
  • 對於一些呼叫頻率太高的函式或操作,使用匯編來實現能夠提高效率和效能,容易被機器來識別

1.開始查詢

開啟objc原始碼,由於主要研究arm64結構的彙編實現,來到objc-msg-arm64.s

iOS探索 方法的本質和訊息查詢流程
iOS探索 方法的本質和訊息查詢流程
p0表示0暫存器的指標,x0表示它的值,w0表示低32位的值(不用過多在意)

①開始objc_msgSend

②判斷訊息接收者是否為空,為空直接返回

③判斷tagged_pointers(之後會講到)

④取得物件中的isa存一份到p13中(暫存器指令在逆向篇中會講到)

⑤根據isa進行mask地址偏移得到對應的上級物件(類、元類)

iOS探索 方法的本質和訊息查詢流程
檢視GetClassFromIsa_p16定義,主要就是進行isa & mask得到class操作

(其定義方式與iOS探索 isa初始化&指向分析一文中提到的shiftcls異曲同工)

⑥開始在快取中查詢imp——開始了快速流程

2.快速流程

CacheLookup開始了快速查詢流程(此時x0是sel,x16是class

iOS探索 方法的本質和訊息查詢流程

iOS探索 方法的本質和訊息查詢流程
#CACHE是個巨集定義表示16個位元組,[x16, #CACHE]表示類物件記憶體地址偏移16位元組得到cachecache一分為二——8位元組的buckets存放在p10,兩個4位元組的occupiedmask存放在p11

#define CLASS            __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__)
複製程式碼

②x1是sel即cmd,取出p11中的低32位(w11)——mask,兩者進行與運算得到hash下標 存放在x12

③p12先左移動(1+PTRSHIFT),再與p10buckets相加得到新的p12——bucket

④拿出p12bucket地址所在的值,放在p17imp和p9sel中,這點可以從bucket_t的結構中看出(sel強轉成key)用bucket中的sel與x1cmd作對比,如果相同則快取命中CacheHit得到其中的imp;如果不等就跳轉⑤

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
    ...
}
複製程式碼

⑤如果bucket->sel == 0CheckMiss;比較p12bucket和p10buckets,如果不相等就將x12bucket的值進行自減操作(查詢上一個bucket),跳轉回④重新迴圈,直到bucket == buckets遍歷結束跳轉⑥

.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
複製程式碼

⑥平移雜湊使得p12 = first bucket,再重複進行一下類似④⑤⑥的操作—— 防止不斷迴圈的過程中多執行緒併發,正好快取更新了。如果bucket->sel == 0CheckMiss,如果bucket == bucketsJumpMiss,本質是一樣的

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
    b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製程式碼

NORMAL時,CheckMissJumpMiss都走__objc_msgSend_uncached

__objc_msgSend_uncached呼叫MethodTableLookup

iOS探索 方法的本質和訊息查詢流程

⑧儲存引數呼叫c++方法進入慢速流程(準備好裝備和藥水打BOSS)

iOS探索 方法的本質和訊息查詢流程

總結:訊息查詢的快速流程可以和cache_t::find方法對比加深理解

iOS探索 方法的本質和訊息查詢流程

3.慢速流程

彙編__class_lookupMethodAndLoadCache3與c++中_class_lookupMethodAndLoadCache3相對應

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製程式碼
//  initialize = YES , cache = NO , resolver = YES
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 快取查詢,cache為NO直接跳過
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    // lock是為了防止多執行緒操作; 類是否被編譯
    runtimeLock.lock();
    checkIsKnownClass(cls);

    // 為查詢方法做準備條件,如果類沒有初始化時,初始化類和父類、元類等
    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
    // 從快取裡面查詢一遍,若有直接goto done
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    // 形成區域性作用域,避免區域性變數命名重複
    {
        // 在類的方法列表中查詢方法,若有直接cache_fill
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        // 遍歷父類進行查詢
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            // 在父類快取中查詢,若有直接cache_fill
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            // 在父類的方法列表中查詢方法,若有直接cache_fill
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.
    // 如果方法仍然沒找到,就開始做動態方法解析
    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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    // 開始訊息轉發
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}
複製程式碼

慢速流程主要分為幾個步驟:

_class_lookupMethodAndLoadCache3呼叫lookUpImpOrForward,此時引數initialize=YES cache=NO resolver=YES

runtimeLock.lock()為了防止多執行緒操作

realizeClass(cls)為查詢方法做準備條件,如果類沒有初始化時,初始化類和父類、元類等

imp = cache_getImp(cls, sel)為了容錯從快取中再找一遍,若有goto done⑨

// Try this class's method lists區域性作用域中,在類的方法列表中查詢方法,若有直接log_and_fill_cachegoto done⑨

// Try superclass caches and method lists區域性作用域中,遍歷父類:先在父類快取中查詢,若有直接log_and_fill_cachegoto done;沒有再去父類的方法列表中查詢方法,若有直接log_and_fill_cachegoto done⑨

⑦如果還沒找到就動態方法解析_class_resolveMethod,標記為triedResolver = YES(已自我拯救過),動態方法解析結束後跳轉慢速流程④

⑧如果動態方法解析之後再找一遍仍然沒找到imp,就丟擲錯誤_objc_msgForward_impcache得到impcache_fill

done:多執行緒解鎖,返回imp


接下來拆解步驟進行說明:

  • cache_getImp這個方法後續會解釋
  • getMethodNoSuper_nolock遍歷呼叫search_method_list
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
複製程式碼
  • search_method_list利用二分查詢尋找方法
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;
    // >>1 表示將變數n的各個二進位制位順序右移1位,最高位補二進位制0
    // count >>= 1 如果count為偶數則值變為(count / 2);如果count為奇數則值變為(count-1) / 2
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        // 取出中間method_t的name,也就是SEL
        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--;
            }
            // 取出 probe
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 則折半向後查詢
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
複製程式碼
  • log_and_fill_cache->cache_fill->cache_fill_nolock進行快取
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
複製程式碼
  • _class_resolveMethod動態方法解析——在找不到imp時的自我拯救操作
    • cls是元類的話說明呼叫類方法,走_class_resolveInstanceMethod;非元類的話呼叫了例項方法,走_class_resolveInstanceMethod
    • 兩者邏輯大同小異,主要邏輯是是objc_msgSend函式傳送SEL_resolveInstanceMethod訊息,系統呼叫resolveInstanceMethod
    • 傳送訊息後,系統會再查詢一下sel方法
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);
        }
    }
}
複製程式碼
  • _objc_msgForward_impcache在彙編中呼叫了_objc_msgForward,然後又進入_objc_forward_handler,它在c++呼叫了objc_defaultForwardHandler
    iOS探索 方法的本質和訊息查詢流程
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
複製程式碼

原來unrecognized selector sent to instance xxx是這麼來的啊...

4.訊息查詢流程圖

iOS探索 方法的本質和訊息查詢流程
iOS探索 方法的本質和訊息查詢流程

寫在後面

OC的訊息機制分為三個階段:

  • 訊息查詢階段:從類及父類的方法快取列表及方法列表查詢方法
  • 動態解析階段:如果訊息傳送階段沒有找到方法,則會進入動態解析階段,負責動態的新增方法實現
  • 訊息轉發階段:如果沒有實現動態解析方法,則會進行訊息轉發階段,將訊息轉發給可以處理訊息的接受者來處理

本文主要講了訊息查詢流程,順帶提了幾句動態方法解析,下一篇文章將通過案例來詳細解讀動態方法解析並著重介紹訊息轉發機制

最後準備了一份動態方法決議的Demo,有興趣的小夥伴們可以自己下斷點看看方法查詢流程和研究下動態方法決議

相關文章