iOS底層探索九(方法的本質中--objc_msgSend慢速及方法轉發初探)

前言

相關文章

       iOS底層探索二(OC 中 alloc 方法 初探)

       iOS底層探索三(內存對齊與calloc分析)  

iOS底層探索四(isa初探-聯合體,位域,內存優化)     

iOS底層探索五(isa與類的關係)  

iOS底層探索六(類的分析上)

iOS底層探索七(類的分析下)

iOS底層探索八(方法本質上)

iOS底層探索十(方法的本質下-消息轉發流程)

  相關代碼:
      objc4_752源碼 

     上篇文章講述了方法的本質實際就是調用objc_msgSend,然後經過彙編代碼進行快速調用查找,或者根據C代碼慢速查找;

彙編代碼快速查找,已經在源碼中做了詳細的註釋,並且在其中加了一個objc_msgSend反彙編僞代碼,可以配合彙編代碼一起查看大家可以自行查看;

方法在類中的查找流程

首先我們來看一下代碼來回顧一下之前的東西:查看Demo

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        #pragma clang diagnostic push
//        // 讓編譯器忽略錯誤,不然調用沒有的方法,編譯器編譯完語法分析會報錯,導致不能運行
        #pragma clang diagnostic ignored "-Wundeclared-selector"

                XZTeacher *teacher = [[XZTeacher alloc] init];
                // 對象方法
                // 自己有 - 返回自己
                [teacher sayHello];
                // 自己沒有 - 老爸 -
                [teacher sayNB];
//                // 自己沒有 - 老爸沒有 - NSObject
                [teacher sayMaster];
                // 自己沒有 - 老爸沒有 - NSObject 沒有
// unrecognized selector sent to instance 0x101806f30 調用下面會直接報錯
//               [teacher performSelector:@selector(saySomething)];
                
                // 類方法
                // 自己有 - 返回自己
                [XZTeacher sayObjc];
//                // 自己沒有 - 老爸 -
                [XZTeacher sayHappay];
//                // 自己沒有 - 老爸沒有 - NSObject
                [XZTeacher sayEasy];
                // 自己沒有 - 老爸沒有 - NSObject 沒有
                // unrecognized selector sent to instance 0x100001368
//                 [XZTeacher performSelector:@selector(sayLove)];
                
                //  sayMaster]這個是NSObject中的實例方法
                 [XZTeacher performSelector:@selector(sayMaster)];

                
        #pragma clang diagnostic pop
    }
    return 0;
}

我們來看一下這個方法的打印結果:

我們可以看到,我們使用[XZTeacher performSelector:@selector(sayMaster)],調用NSObject中的實例方法,也調用成功了;這個實際上,我們在之前的文章中也有提到過什麼原因,這裏再來回顧一下,首先類方法實際就是在元類中以實例方法的方式進行存儲的,而元類的根源類,也是繼承於NSObject的,所以在方法查找的過程中,類方法會最終查找到NSObject類的實例方法,找到並執行了;但是還有一部分方法是在本類,以及父類都找不到的時候就會崩潰,下面我們就進行分析一下

就是根據上圖的查找結果,最終會找到sayMaster並執行

這個我們可以進行總結一下:

對象的實例方法:

  1. 自己有

  2. 自己沒有-->父類有

  3. 自己沒有-->父類沒有-->父類的父類----直到NSObject

  4. 自己沒有-->父類沒有-->直到NSObject也沒有--->崩潰

對象的類方法:

  1. 自己有

  2. 自己沒有-->父類有

  3. 自己沒有-->父類沒有-->父類的父類----直到NSObject

  4. 自己沒有-->父類沒有-->直到NSObject類方法沒有-->NSObject實例方法

  5. 自己沒有-->父類沒有-->直到NSObject類方法沒有-->NSObject實例方法沒有-->崩潰

方法的慢速查找流程

得到上面的結論之後,我們從底層源碼進行分析,首先我們調用方法同過objc_msgSend 查找cachet-->進入彙編快速查找-->最終進入代碼_class_lookupMethodAndLoadCache3 慢速查找,由於都是新方法,所以我們可以直接進入方法最後進行查看

我們直接查看_class_lookupMethodAndLoadCache3方法源碼

//這裏的extern  標示外部也可以調用這個方法
extern IMP _class_lookupMethodAndLoadCache3(id, SEL, Class);

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
/**
 obj 當前對象
 sel 方法名稱
 cls 實例方法爲類    類方法爲元類
 NO   cache 沒有緩存纔會進入這裏,有的話,從緩存哪裏就返回出去了
 */
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

我們可以看出,這個方法中,會調用lookUpImpOrForward,並傳入幾個參數cls(當前類,如果是類方法這裏就是元類)sel 方法編號,obj (當前對象)以及initialize(YES)cache(NO),resilver(NO),繼續查看lookUpImpOrForward方法:

 

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
//    查找方法進入的時候這裏是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.
//加鎖,防止多線程訪問
    runtimeLock.lock();
//   內部對class 的判斷,是否被編譯
    checkIsKnownClass(cls);

    // 這裏是拿出類中的bits中的 class_rw_t 中的 data中的Method查看是否有
    //爲查找方法做準備條件,判斷類有沒有加載好,如果沒有加載好,那就先加載一下類信息,準備好父類、元類
    if (!cls->isRealized()) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    //確定類已經加載完成,
    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
    }


 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
  //判斷該方法從緩存裏面查找一遍,如果是方法調用,會先走cache,一般這裏imp 是取不到的,別的方法可能會調用這裏
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
//    這裏的括號是爲了行程局部作用域,避免局部變量命名重
    {
//        在MethodList 中進行查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
//            找到方法後,填充到緩存中去,內部調用cache_fill_nolock
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
      //直接跳到done這個點,C語言調用方式goto
            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. //從父類的緩存中查找一下
            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 {
//如果是_objc_msgForward_impcache方法,就退出,不需要遍歷了,因爲_objc_msgForward_impcache這個方法就是未實現時候轉發用的
                    // 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;
            }
        }
    }

    // No implementation found. Try method resolver once.
    //如果方法仍然沒找到,就開始做方法消息動態解析了
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
    
//     實例方法解析:resolveInstanceMethod
//     類方法解析:resolveClassMethod
        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.
        //設置爲NO,防止因爲消息動態解析導致死循環
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    //如果第一次轉發還是沒用的話,就取出_objc_msgForward_impcache這個方法指針
    imp = (IMP)_objc_msgForward_impcache;
    //緩存起來將指針返回回去執行
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

這個方法就有幾個重點要觀察的:

  1. runtimeLock.lock()爲了防止多線程操作

  2. realizeClassMaybeSwiftAndLeaveLocked 爲查找方法做準備條件,如果類沒有初始化時,初始化類和父類、元類、分類等

  3. imp = cache_getImp(cls, sel)爲了容錯從緩存中再找一遍,如果找到直接goto done(8)

  4. 局部作用域中查找本類getMethodNoSuper_nolock中,如果找到進行log_and_fill_cache 保存緩存直接goto done(8)

  5. 局部作用與中在父類中進行查找首先父類緩存中查找,若有直接log_and_fill_cachegoto done;沒有再去父類的方法列表中查找方法,若有直接log_and_fill_cachegoto done(8)

  6. 如果還沒找到就動態方法解析_class_resolveMethod,標記爲triedResolver = YES(已自我拯救過),動態方法解析結束後跳轉慢速流程第三步進行再一次查找

  7. 如果動態方法解析之後再找一遍仍然沒找到imp,就拋出錯誤_objc_msgForward_impcache得到impcache_fill

  8. done:多線程解鎖,返回imp

這個方法cache_getImp 後續進行詳細解釋

getMethodNoSuper_nolock 方法

這裏對其中部分代碼進行解釋第四步getMethodNoSuper_nolock 方法遍歷調用search_method_list方法

static method_t *
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過程之後,通過findMethodInSortedMethodList(sel, mlist)進行二分查找,保證能夠快速尋找到目標方法

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);
        
        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;
        }
        // 如果keyValue > probeValue 則折半向後查詢
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

log_and_fill_cache -->cache_fill--->cache_fill_nolock連續調用這個方法可以看類的分析下

static void
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);
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

_class_resolveMethod動態方法解析:在找不到imp時的自我拯救操作 ,下面會進行分析

 

_objc_msgForward_impcache方法

這個方法中就是如果方法在本類,父類都找不到方法後執行到這裏_objc_msgForward_impcache 方法

extern void _objc_msgForward_impcache(void);//這個方法會進入彙編


	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF] //調用_objc_forward_handler 方法
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
	
	

_objc_msgForward_impcache這個方法調用到彙編之後,彙編方法又會調用會C方法_objc_forward_handler

void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;

#else

// 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;

這裏我們就能看到熟悉的unrecognized selector sent to instance 崩潰信息了

消息轉發機制初探

我們已經看到了如果沒有實現方法,然後調用,系統會直接崩潰,那麼如何避免這些問題呢,蘋果大大給我們提供了另一個解決思路,就是消息轉發機制,這篇我們先進行一個初步的查看

首先我們現在main函數中調用一個沒有實現的方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        #pragma clang diagnostic push
//        // 讓編譯器忽略錯誤,不然調用沒有的方法,編譯器編譯完語法分析會報錯,導致不能運行
        #pragma clang diagnostic ignored "-Wundeclared-selector"

                XZTeacher *teacher = [[XZTeacher alloc] init];
//        消息轉發流程這個方法.h中之聲明,不實現
        [teacher saySomthing];
        
        #pragma clang diagnostic pop
    }
    return 0;
}

根據上面文章的分析,我們很快可以定位到,如果都沒有找到這個方法,那麼就會走到動態解析方法_class_resolveMethod

static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {//不是元類
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // 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方法

static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
//查找方法中是否有實現resolveInstanceMethod這個方法
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //發送SEL_resolveInstanceMethod消息,
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //發送消息SEL_resolveInstanceMethod 傳參爲我們的方法編號
    //崩潰的方法不是這個方法,說明再NSObject方法中有實現這個方法
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
//再次查找類中是否sel方法,因爲resolveInstanceMethod方法執行後可能動態進行添加了,resolver是不要進行消息轉發了
    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));
        }
    }
}

首先判斷類有沒有實現SEL_resolveInstanceMethod方法,其實也就是+ (BOOL)resolveInstanceMethod:(SEL)sel方法,我們通過在源碼搜索可以看到在NSObject類中已經都實現了,並且都是一個空方法,並且返回NO,

//在NSObject.mm 文件中有看到這個方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

在lookUpImpOrNil 方法中實際就是有調用了一次方法查找流程這裏的參數是NO( initialize)  YES (Cache ) NO (resolver)不會造成遞歸

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

這裏我們看到系統會自動調用+resolveInstanceMethod方法,這說明我們就有一些可操作性,那麼我們在類中實現以下這個方法

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"XZTeacher 走了resolveInstanceMethod %p",sel);
    return [super resolveInstanceMethod:sel];
}

運行程序看到輸出結果,我們可以看到執行了我們輸出日誌,但是崩潰了,這裏我們就考慮我們是可以做一些操作


        這裏我們可以考慮到這裏實際上的一些列操作就是爲了找到函數的IMP ,那我們是不是可以直接在這裏將函數IMP實現使得下面lookUpImpOrNil方法的時候可以找到IMP,程序就不會崩潰了;

具體實現方法,引入頭文件#import <objc/message.h> 如下所示:

#import <objc/message.h>

- (void)sayHello{
    NSLog(@"%s",__func__);

}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(saySomthing)) {
        NSLog(@"進入方法了");
        IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayType = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, saySomeIMP, sayType);
    }
    return [super resolveInstanceMethod:sel];
}

我們來看一下輸出結果:

可以看出我們在調用saySomthing 方法的時候進入了我們寫的代碼,並且還調用了我們想讓他調用的sayHello方法,而且沒有崩潰;

 

總結

    這篇文章主要是根據上篇文章職工objc_msgSend方法快速查找流程彙編過程找到_class_lookupMethodAndLoadCache3 方法,並進行分析了這個代碼中消息查找的流程,並對消息轉發防止崩潰進行了初步的瞭解,如果有錯誤的地方還請指正,大家一起討論,(文章中有錯誤後,我發現會第一時間對博文進行修改),還望大家體諒!

歡迎大家點贊,關注我的CSDN,我會定期做一些技術分享!

寫給自己

時光流逝,討厭的人會帶着討人厭的話離開,喜歡的人會帶着美好的事到來。把目光放在遠處,灑脫還給自己。忘掉路人,放過自己,未完待續...

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