NSObject方法調用過程詳細分析

分析OC方法調用過程的博客多如牛毛,爲何我還來炒剩飯,緣由:html

  1. 我本身雖然以前也分析過方法調用,可是沒有成體系作過筆記,此次至關於本身作一個筆記,便於之後查看。
  2. 網上有詳細分析,可是都是基於x86彙編分析的(由於runtime開源的代碼能夠在macOS上運行起來,更方便分析吧),我只對arm64彙編熟悉,我想應該也有部分同窗跟我同樣,因此我基於arm64彙編分析一波~
  3. 我這個是基於最新的runtime源碼版本(版本號objc4-756.2,蘋果官網的源碼),網上分析的大多都是幾年前的版本,雖說整個邏輯基本一致,可是仍是有些許不一樣。

消息發送、轉發流程圖

objc_msgSend

聲明原型

之前是:git

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
複製代碼

Xcode11改爲了:github

void objc_msgSend(void);
複製代碼

修改原型是爲了解決:接收方(被調用者)會從調用方傳遞參數的相同位置和格式中檢索參數。也就是說,被調用者必定知道調用者把參數放在什麼寄存器/內存,這樣就不會取錯參數。避免了調用者把參數放在a寄存器,可是被調用者去b寄存器取參數的錯誤行爲。緩存

例如:bash

- (void)log: (float)x {
    printf("%f\n", x);
}
複製代碼

由於之前是不定參數,因此objc_msgSend(obj, @selector(log:), (float)M_PI);不會報錯,可是 在intel ABI上面,會出錯(函數裏取得的浮點數是錯誤的浮點數)。(由於intel ABI中,float跟double在不一樣的寄存器裏,傳一個double,可是函數參數是float,函數從float取值)。這個就是調用者把參數放在a寄存器,被調用者去b寄存器取參數。數據結構

如何繼續使用objc_msgSend

顯然,蘋果不建議咱們直接使用objc_msgSend,可是咱們依然想使用,能夠用下面兩種方法:多線程

  1. 強制轉換:
((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);
複製代碼

會強制將double轉換成float,而後放入float對應的寄存器,被調用者也是去float對應的寄存器取參數。架構

  1. 聲明函數指針來調用:
void (*PILog)(id, SEL, float) = (void (*)(id, SEL, float))objc_msgSend;
PILog(obj, @selector(log:), M_PI);
複製代碼

雖然上面兩種方法都是強制轉換objc_msgSend,讓咱們能夠直接使用objc_msgSend,可是仍是不建議強制轉換objc_msgSend。對於某些類型的參數,它在運行時仍可能失敗,這就是爲何存在一些變體(爲了適配不一樣cpu架構,好比arm64就不用爲返回值是結構體,而專門有objc_msgSend_stret,可是其它cpu架構須要有),例如objc_msgSend_stret,objc_msgSend_fpret,objc_msgSend_fp2ret…… 只要使用基本類型,就應該沒問題,可是當開始使用結構體時,或使用long double和複雜類型,就得注意了。app

若是咱們使用[obj log:M_PI]來調用,不過什麼平臺的ABI,都不會出錯,Xcode都會幫咱們準確的翻譯好的。因此沒有特殊須要,不要直接使用objc_msgSend。ide

消息發送

arm64源碼分析

arm64彙編作3件事:

1. GetIsa

struct objc_object {
private:
    isa_t isa;
    ... 
}

struct objc_class : objc_object {
    // isa_t isa;
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits; 
    ...
}

union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls (4-36bits,共33bits,存放類地址): 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
    ...
};
複製代碼

由於對象的方法存放在對象對應的類裏,因此須要得到類的地址。類的地址存放在isa的4-36 bits上,因此須要先得到isa;而對象(地址)放在X0上,對象就是objc_object結構體,因此X0裏的地址就是objc_object結構體地址,結構體第一個就是isa,那麼X0地址就能夠看作是isa地址。因此X0&ISA_MASK(0x0000000ffffffff8ULL)就是類地址(由於類的指針要按照字節(8 bits)內存對齊,其指針後三位一定是0,33(33個1與運算)+3(填充3個0),一共36位表示)。

當X0小於0時候,說明X0是tagged pointer,經過類索引來獲取類地址。

CacheLookup

//緩存的數據結構
typedef uint32_t mask_t;
struct cache_t {
    struct bucket_t *_buckets; //哈希表地址
    mask_t _mask;  //哈希表的大小,值爲2^n-1
    mask_t _occupied; //哈希表中元素個數
}

typedef uintptr_t cache_key_t;
struct bucket_t {
    cache_key_t _key; //SEL
    IMP _imp;  //函數指針
}
複製代碼

先討論一個數學問題: a%b=a-(a/b)*b,這個很明顯吧;那麼當b=2^n - 1,好比b=三、七、15等等,a%b=a&b。好比13%3=1,13&3也是等於1。

講人話,就是當b=2^n - 1,能夠用與運算(a&b)來替代模運算(a%b),可是避免了模操做的昂貴開銷。彙編裏,用sel&mask來代替sel%mask。

sel%mask(哈希表大小)+buckets,結果就是sel函數在緩存裏的地址;若是cache裏爲0,說明沒有緩存,調用__objc_msgSend_uncached;若是發生哈希衝突,那麼從後往前遍歷,若是SEL跟X1匹配上了,則緩存命中;若是遍歷到bucket_t的SEL爲0,則調用__objc_msgSend_uncached。 X12第一次遍歷到buckets(哈希表表頭)時,將X12置爲哈希表尾,從新從後往前遍歷。整個遍歷過程若是遇到SEL爲0,則調用__objc_msgSend_uncached,X12第二次遍歷到buckets時,也調用__objc_msgSend_uncached,遍歷過程若是緩存命中,則調用imp,直接ret。

__objc_msgSend_uncached

__objc_msgSend_uncached就是調用前保存X0-X8/q0-q7寄存器,而後調用__class_lookupMethodAndLoadCache3函數,返回函數imp放在x17,恢復寄存器,而後調用imp。

C/C++源碼分析

_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

_class_lookupMethodAndLoadCache3就是調用了lookUpImpOrForward函數而已。

lookUpImpOrForward

lookUpImpOrForward函數主要幹:1.類沒有註冊,就註冊類;2.類沒有初始化,就初始化類;3.分別從緩存(cache_getImp)和類方法列表(getMethodNoSuper_nolock)裏遍歷,尋找sel函數;4.循環從父類的緩存和方法列表遍歷,直到父類爲nil;5.若是尚未找到,則進行方法解析(resolveMethod);6.若是最後依然沒有找到方法,就把imp賦值爲_objc_msgForward_impcache,返回imp。下面詳細分析這幾個過程:

註冊類

//註冊類
if (!cls->isRealized()) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    // runtimeLock may have been dropped but is now locked again
}
複製代碼

通常狀況下,類在App啓動加載macho文件時候,就已經註冊了。可是也有特例,好比weaklink的類,可能運行到這裏,尚未初始化。爲何有weaklink,就是App最低版本支持iOS9,可是卻使用了iOS11 SDK的新功能,若是沒有weaklink,程序裏確定是不能使用新版本的功能的。更詳細介紹,請見官網

註冊類(realizeClassWithoutSwift) 這個過程會申請class_rw_t空間,遞歸realize父類跟元類,而後設置類的父類跟元類;添加類的方法、屬性、協議;添加分類的方法、屬性、協議。返回這個類的結構體

初始化類

if (initialize && !cls->isInitialized()) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again

    // 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 } 複製代碼

若是類沒有初始化,先遞歸初始化父類,而後給這個類發送objc_msgSend(cls, SEL_initialize)方法。因此initialize不須要顯示調用父類,而且子類沒有實現initialize,會調用父類的initialize方法(這個方法沒啥特別的,也是經過objc_msgSend來調用的)。

cache_getImp

retry:    
runtimeLock.assertLocked();

// Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; 複製代碼

由於多線程,此時cache可能改變了,因此須要從新來次CacheLookup。

getMethodNoSuper_nolock

// Try this class's method lists. { Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } 複製代碼

getMethodNoSuper_nolock內部會調用search_method_list函數,search_method_list函數就是遍歷類的方法列表,只不過當方法列表是排序的,就二分法查找,不然就是依次遍歷。

循環遍歷父類的cache_getImp跟getMethodNoSuper_nolock

// Try superclass caches and method lists.
{
    unsigned attempts = unreasonableClassCount();
    從上圖能夠看出,不論是類仍是元類,都是一直遍歷到RootClass(NSObject)。
    整個過程,不過是cache中,仍是methodlist中找到sel的imp,都調用log_and_fill_cache,將sel和imp放入cache中
    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.");
        }
        
        // 父類cache尋找
        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 {
                // 若是imp爲_objc_msgForward_impcache,說明這個sel以前尋找過,沒有找到。因此退出循環
                // 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. Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } } 複製代碼

resolveMethod(方法解析)

// 若是上面都沒有找到sel的imp,就不會執行goto done;進而走到這裏來,這裏會調用方法解析,方法解析後,而後goto retry,
又回到上面的cache_getImp--> getMethodNoSuper_nolock -->
循環遍歷父類的cache_getImp跟getMethodNoSuper_nolock -->
再次到此處,可是再次到此處時候,不會進入if裏面了,由於triedResolver已經設置爲YES了。
if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    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;
}


static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    //若是類不是元類,調用resolveInstanceMethod,
    //resolveInstanceMethod函數會調用objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        //若是類是元類,就調用resolveClassMethod
        //resolveClassMethod函數會調用objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

//只給出resolveInstanceMethod函數,resolveClassMethod相似。
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    //從這裏能夠看出執行完SEL_resolveInstanceMethod,返回的bool值,跟會不會進行消息轉發無關,僅僅跟打印系統日誌有關。
    // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveInstanceMethod adds to self a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

複製代碼

須要注意是:平時咱們寫的消息解析resolveInstanceMethod函數跟resolveClassMethod函數,通常用來add method,他們返回的bool值,跟是否會進入消息轉發無關,網上文章絕大部分都說返回YES就表示消息解析已經處理了這個消息,不會進行消息轉發,而返回NO,就進入消息轉發。實際上是錯誤的,讀者能夠本身寫demo驗證。

根據上面的流程圖,咱們能夠清楚知道,消息解析後,會從新進行類cache_getImp--> 類getMethodNoSuper_nolock --> 循環遍歷父類的cache_getImp跟getMethodNoSuper_nolock,若是找到了,填充cache,而後到done,ret。若是沒有找到,imp賦值爲_objc_msgForward_impcache,而執行_objc_msgForward_impcache纔會進入消息轉發,跟resolveInstanceMethod返回的bool值確實沒有關係。

_objc_msgForward_impcache

調用_objc_msgForward_impcache:(接口宏,定義在arm64裏) 在arm64彙編裏,最後調用了_objc_forward_handler函數。 _objc_msgForward-->_objc_forward_handler。

// Default forward handler halts the process.
__attribute__((noreturn)) void 
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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret 
objc_defaultForwardStretHandler(id self, SEL sel)
{
    objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

// Define SUPPORT_STRET on architectures that need separate struct-return ABI.
#if defined(__arm64__)
# define SUPPORT_STRET 0
#else
# define SUPPORT_STRET 1
#endif

由於arm64中(不用爲返回值是結構體,而須要支持objc_msgSend_stret(這也是爲啥其它文章裏面有許多objc_msgSend變體,而本文沒有)等。),SUPPORT_STRET爲0。
上面代碼在arm64中,能夠簡潔爲:

// Default forward handler halts the process.
__attribute__((noreturn)) void 
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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
}

能夠看到_objc_forward_handler的默認實現是objc_defaultForwardHandler(打印系統日誌,殺掉進程),
可是App在啓動時候,會調用objc_setForwardHandler,從新給_objc_forward_handler賦值新的函數指針。賦值成什麼函數呢?在Core Foundation
中。

複製代碼

消息轉發階段

Core Foundation 裏面沒找到objc_setForwardHandler的調用,可是打符號斷點,發現App啓動時候,經過_CFInitialize調用了objc_setForwardHandler函數,說明_objc_forward_handler被從新賦值了。

經過消息轉發調用堆棧,發現_objc_forward_handler被替換成了_CF_forwarding_prep_0函數,_CF_forwarding_prep_0調用___forwarding___函數。

forwarding 函數(打符號斷點看到有336行彙編) 大概作了:

  1. 若是類實現了forwardingTargetForSelector,調用,返回對象target跟self不一樣,從新調用objc_msgSend(target,sel...) 而後ret。
  2. 若是實現了methodSignatureForSelector,調用,返回sig,則調用forwardInvocation,而後返回結果;不然調用doesNotRecognizeSelector
// Replaced by CF (throws an NSException)這裏說了,
也是被Core Foundation替換,其實也是打日誌,拋異常。
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

複製代碼

參考

  1. www.mikeash.com/pyblog/objc…
  2. yulingtianxia.com/blog/2016/0…
相關文章
相關標籤/搜索