iOS探索 方法的本質和方法查找流程

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

寫在前面

書接上文說到cache_t緩存的是方法,那麼方法又是什麼呢?c++

1、Runtime

1.什麼是Runtime?

Runtime是一套API,由c、c++、彙編一塊兒寫成的,爲OC提供了運行時git

  • 運行時:代碼跑起來,將可執行文件裝載到內存
  • 編譯時:正在編譯的時間——翻譯源代碼將高級語言(OC、Swift)翻譯成機器語言(彙編等),最後變成二進制

2.Runtime版本

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

源碼中-old__OBJC__表明Legacy版本,-new__OBJC2__表明Modern版本,以此作兼容算法

3.Runtime的做用及調用

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

Runtime調用方式:bash

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

2、方法的本質

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是方法編號app

若是外部定義了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把配置修改爲以下圖

3、方法查找流程

objc_msgSend是用匯編寫成的,至於爲何不用C而是用匯編寫,是由於:

  • C語言不能經過寫一個函數,保留未知的參數,跳轉到任意的指針,而彙編有寄存器
  • 對於一些調用頻率過高的函數或操做,使用匯編來實現可以提升效率和性能,容易被機器來識別

1.開始查找

打開objc源碼,因爲主要研究arm64結構的彙編實現,來到objc-msg-arm64.s

p0表示0寄存器的指針,x0表示它的值,w0表示低32位的值(不用過多在乎)

①開始objc_msgSend

②判斷消息接收者是否爲空,爲空直接返回

③判斷tagged_pointers(以後會講到)

④取得對象中的isa存一份到p13中(寄存器指令在逆向篇中會講到)

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

查看 GetClassFromIsa_p16定義,主要就是進行 isa & mask獲得 class操做

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

⑥開始在緩存中查找imp——開始了快速流程

2.快速流程

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

#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

⑧保存參數調用c++方法進入慢速流程(準備好裝備和藥水打BOSS)

總結:方法查找的快速流程能夠和cache_t::find方法對比加深理解

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
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.方法查找流程圖

寫在後面

OC的消息機制分爲三個階段:

  • 方法查找階段:從類及父類的方法緩存列表及方法列表查找方法
  • 動態解析階段:若是消息發送階段沒有找到方法,則會進入動態解析階段,負責動態的添加方法實現
  • 消息轉發階段:若是沒有實現動態解析方法,則會進行消息轉發階段,將消息轉發給能夠處理消息的接受者來處理

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

最後準備了一份動態方法決議的Demo,有興趣的小夥伴們能夠本身下斷點看看方法查找流程和研究下動態方法決議

相關文章
相關標籤/搜索