iOS大師養成之路--方法的旅程

1. 關於對OC方法調用開始

1.1 前期的準備工做

1.1.1 準備用於測試的類和方法

我在工程裏準備了這麼一個類LCHero,有一個對象方法throwSkill, 繼承至LCPerson。 LCPerson裏面有一個對象方法attack, 一個類方法defence,LCPerson 繼承至NSObject. 我在NSObject的一個分類裏準備了一個測試方法test 代碼以下:c++

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCHero *hero = [LCHero new];
        [hero throwSkill];
    }
    return 0;
}
/******************************************/
@interface LCPerson : NSObject

- (void)attack;
+ (void)defence;
- (void)revival;

@end

@implementation LCPerson

- (void)attack {
    NSLog(@"%s --> 開始進攻!",__func__);
}

+ (void)defence {
    NSLog(@"%s ||-- 開始防護! ",__func__);
}

@end
/******************************************/
@interface LCHero : LCPerson
- (void)throwSkill;
@end

@implementation LCHero
- (void)throwSkill {
    NSLog(@"%s --> 釋放終極技能!",__func__);
}

@end

/******************************************/
@interface NSObject (test)
- (void)test;
@end

@implementation NSObject (test)
- (void)test {
    NSLog(@"%s, 測試一下!",__func__);
}
@end
複製代碼

1.1.2 Clang 編譯命令對main.m 編譯一下看看.cpp文件對應的內容

使用的命令以下:程序員

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk main.m
複製代碼

咱們看到的對應的cpp文件中對main文件中咱們寫的內容的c++編譯內容objective-c

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LCHero *hero = ((LCHero *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCHero"), sel_registerName("new"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)hero, sel_registerName("throwSkill"));
    }
    return 0;
}
複製代碼

若是把方法的類型以及類型轉換去掉,就是以下的樣式。咱們發現底層調用的是一個objc_msgSend方法,咱們調用的方法被轉成了SEL。數組

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LCHero *hero = (LCHero *)objc_msgSend(objc_getClass("LCHero"), sel_registerName("new"));
        (void *)objc_msgSend(hero, sel_registerName("throwSkill"));
    }
    return 0;
}
複製代碼

2. 方法旅程的第一站--> objc_msgSend一日遊

2.1 objc_msgSend第一個景點--> objc_msgSend彙編流程

既然咱們知道了底層吊起的是objc_msgSend,那麼咱們在方法調用以前打一個調試斷點,當斷點來了以後咱們按住control鍵 + step into 一步一步點擊看看它會怎麼走。 緩存

按住點擊幾下以後,來到了這裏。竟然是彙編!!

咱們配置好相應的彙編源碼,去找下這個流程。

2.1.1 經過isa獲取類

來到這一步以後,咱們想既然方法這些都在類裏面,而類和對象是經過isa聯繫起來的,是否是要找isa呢?而後經過isa找到對應的類,答案是確定的。bash

2.1.2 查找緩存cache_t

找到class以後開始查找類中的方法緩存。 多線程

找到緩存直接返回,這是最佳的也是最快的方式,爲何要用匯編走這個流程呢?由於執行效率更高。

沒有找到緩存開始進入到下一流程,方法表查詢

到此,快速的彙編查找方法流程告一段落,咱們進入下一景點: 慢速查找

2.2 objc_msgSend第二個景點--> 在c、c++中慢速查找imp

2.2.1 _class_lookupMethodAndLoadCache3

經過上面那個景點咱們看到了_class_lookupMethodAndLoadCache3方法,源碼中以下:發現它直接調起一個下層方法lookUpImpOrForward,_class_lookupMethodAndLoadCache3只是起到中間鏈接做用app

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

2.2.2 lookUpImpOrForward

這個方法裏面的內容有點多同時也很重要,我就把判斷、斷言、賦值的代碼都刪掉了,保留一些關鍵的方法和註釋。你們先過個眼隱。咱們再一一分析下。oop

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{

    if (!cls->isRealized()) {  //先判斷類有沒有加載到內存,若是沒有,先加載類
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {//判斷是否實現了initialize,若是有實現,先調用initialize
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
    
 retry:    

    // Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // Try this class's method lists.
        //從類的方法表裏查詢,若是有就返回imp 順便存一份到類的cache裏
    
    // Try superclass caches and method lists.
         // Found the method in a superclass. Cache it in this class.
    
    // No implementation found. Try method resolver once.
    
    // 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; } 複製代碼
2.2.2.1 方法查找前的準備-->繼承鏈上的類以及分類都得準備
  • cls->isRealized()判斷類是否加載了,realizeClass(cls)這是一個遞歸操做,全部繼承鏈上的類都會被加載。父類-->根類-->根元類,直到cls爲空才退出遞歸。
  • supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA()));加載父類元類一直遞歸加載,爲的就是方便方法查找。
  • 鏈接關聯的子類以及父類
  • methodizeClass(cls)分類方法加載咱們來看看他們作了什麼,主線流程我在代碼塊中介紹了
先看註釋,我英文不太好可是大概意思明白了,把方法、協議、屬性安排好,而後把外面的分類也添加進來 --> 感受看到了美景 ><
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list. * Attaches any outstanding categories. * Locking: runtimeLock must be held by the caller **********************************************************************/ 咱們把這個方法的主要內容拆開說明一下 1. 把方法、屬性、協議從類的ro 拷貝到 rw中來,爲啥有個1?-->是由於把數組的首地址傳進去組成了一個二維數組 method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 2. 判斷是不是根元類,若是是根元類須要把方法加載一下,確保在分類替換以前就已經加載好了 --> ? 下面的註釋也有, 就是說若是根類調用了一個它本身沒有的方法,它會往根元類中找。 個人根元類要是有相關方法我要把他添加到個人類的方法表裏面,它才能找的獲得,並且要早於分類方法添加以前。 爲何是在分類替換以前呢?我在這裏只能大膽猜測一下,方法表設計的多是一個相似棧的表, 若是有分類在後面添加以後那麼我就找imp的時候就先找到分類的imp就返回了,就出現了替換了類的方法的這麼個現象 // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }
    
3. 添加分類方法
 // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/); 複製代碼
2.2.2.2 再次嘗試cache_getImp(cls, sel)

咱們前面已經找過緩存了,爲何還要找緩存呢?緣由有2:組件化

  • 多是多線程找,萬一有線程有返回了呢是否是能夠直接調用。
  • 提高效率,有就立馬調用有助於提高性能-->畢竟是蘋果爸爸寫的代碼
2.2.2.3 沒有緩存就往類、遞歸父類的方法表裏找
Method meth = getMethodNoSuper_nolock(cls, sel);//找類的方法表
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就緩存一份
    imp = meth->imp;
    goto done
    
    for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
    imp = cache_getImp(curClass, sel); //遞歸找父類的方法表
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就緩存一份
    imp = meth->imp;
    goto done
複製代碼
2.2.2.4 遍歷了類、遞歸了父類以後尚未找到

若是咱們找了類的方法表,同時遞歸找了父類都沒有找到,因爲咱們傳遞的resolver默認是YES同時triedResolver也沒有進行重新賦值仍是NO,咱們會走下到下一站方法決議_class_resolveMethod,具體源碼在下面,

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

3. 方法旅程的第二站--> method resolver 動態方法決議

在經歷了類->父類->元類->根元類->...->NSObject ,分類等一系列的查找以後沒有找到,那而後怎麼辦?咱們的旅程還要繼續啊!蘋果爸爸仍是很心疼咱們的,給你個機會處理一下吧,我不直接讓它崩潰。因而咱們看到了下一站的風景_class_resolveMethod(cls, sel, inst)

3.1 _class_resolveMethod

咱們來看下這個方法裏有啥東西,具體代碼在下面代碼塊。步驟在下面分析

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

3.1.1 若是當前傳入的類不是元類--> 對象方法

  • 爲何要判斷?--> 對象方法在類裏面,類方法在元類裏面

  • 註釋中寫try [cls resolveInstanceMethod:sel] ?--> 咱們的類裏面沒有怎麼try? 是否是系統幫咱們實現了這個方法,不知道,繼續往下看

  • 先判斷是否沒有實現這個方法,若是沒有就直接返回。我本身寫的類裏面沒有這個方法,若是咱們本身沒有實現的話是否是系統幫咱們實現?

  • 在objc的源碼中找到了這個方法,默認返回的是NO.

  • 如今咱們思考一下,若是我能讓這個方法執行下去勢必要找到一個imp返回回去。若是咱們重寫這個方法而後添加一個imp到類裏面是否是就解決這個問題了呢?咱們去文檔中查一下這個方法,果真驗證了咱們的想法。

  • 無論咱們有沒有處理lookUpImpOrNil都會調起,而後再回到lookUpImpOrForward,由於已經retry過了這個結果已經保存了,若是找到imp直接到done流程,若是仍是沒找到就會來到imp = (IMP)_objc_msgForward_impcache.

  • 咱們搜一下,結果這個傢伙在彙編裏面調用的是__objc_msgForward

  • 咱們再看看__objc_msgForward,它裏面調用的有兩個很像的方法,再搜一下就發如今objc的源碼中有實現,這裏的打印內容咱們好熟悉哦,咱們來試下在咱們開始準備的main裏面調用LCHero沒有實現的對象方法- (void)revival看看錯誤輸出 --> 沒錯就是沒找到方法的報錯輸出

3.1.2 若是當前傳入的類是元類--> 類方法

類方法和對象方法處理有點不太同樣調用的是resolveClassMethod,添加imp是往元類裏面添加,只是和對象方法相似的處理流程只不過調用的方法不同,只是調用完類方法決議以後竟然還走了對象方法的決議。咱們就猜測爲何還要走這步。

  • 類方法在元類裏面,咱們程序員通常不能直接操做元類,是頗有可能找不到類方法。
  • 無論找不找獲得,一旦查找了類方法的動態決議以後就會lookUpImpOrNil再次查找一下若是仍是沒有就會走一次對象方法決議_class_resolveInstanceMethod
  • 也就意味着若是調用的類方法沒有會一直往父類的繼承鏈中找直到NSObject,NSObject類咱們不能直接改,可是能夠寫一個分類拓展方法,若是咱們再NSObject分類中進行相應類方法的動態決議就能截獲這類崩潰。
_class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }

複製代碼

可是就這樣結束了麼?咱們再來看看奔潰時候的堆棧是否是還有咱們沒了解過的方法,明顯還有。

可是咱們實在跟不進去流程了怎麼辦?咱們記得在父類以及元類遞歸找方法的時候有個 log_and_fill_cache方法,除了fill_cache 還有log,不妨一看究竟。發現調用了 logMessageSend,再往裏面看看發現了一個相似log開關控制的參數

這個開關參數是怎麼控制的呢?在👇

  • 這裏咱們不妨大膽地玩一下,由於根據咱們在log_and_fill_cache的流程中發現它有個打印log的方法,還會寫到一個文件裏,這個文件里根據它的註釋說會給咱們一些方法有關的線索。咱們把這個開關在咱們準備的工程中拓展一下使用範圍在奔潰先後都調用了啥方法?

  • 再運行一下,咱們在/tmp/msgSends 找有沒有相似的文檔記錄log

  • 打開一看,OMG, 啥!! 都打印出來的確有兩個咱們沒跟出來的流程一個是forwardingTargetForSelector,另外一個是methodSignatureForSelector。突然發現咱們還想逛的還要不少,咱們都想看看後面兩站都是什麼風景!!

4. 方法旅程的第三站--> forwardingTargetForSelector

表面意思是傳遞一個對象,什麼意思?原來我方法調用時傳入的對象不要了麼? 咱們來看看源碼是否是NSObject也實現了只是跟上一個站點同樣沒有作處理額?

  • 我本身沒有,父類沒有,元類也沒有。-->是否是別人有也能夠呢若是交個有這個方法的對象處理原則上也是OK的嘛。

臥槽,果真實現了只是直接返回了一個nil,這裏就引起了個人另外一個猜測,若是返回一個實現了這個方法的對象呢是否是就解決問題了。咱們試一試哈

  • 咱們新建一個類,LCEnemy,繼承之LCPerson 而後實現- (void)revival
@implementation LCEnemy
- (void)revival {
    NSLog(@"%s --> 哈哈哈 本魔王又活了!",__func__);
}
@end
複製代碼
  • 而後LCHero重寫forwardingTargetForSelector,返回一個LCEnemy對象。
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return [NSClassFromString(@"LCEnemy") alloc];
}

複製代碼
  • 運行再看結果。 >< 驚不驚喜意不意外 --> 竟然調用了LCEnemy方法。
    但是還有個方法我還沒看到啊,下一站咱們很期待啊,沒毛病 --> 趕忙上車去往下一站

4. 方法旅程的第三站--> methodSignatureForSelector

到這個流程以後也就意味着:

  • 我本身沒有對應的方法處理
  • 繼承鏈也沒有誰能處理
  • 也沒有動態方法決議處理
  • 別人也沒有這個能力處理

那這個時候我是否是要在網上發給求助帖-->看看哪一個好心人能處理。 可是總得知道你這個方法是什麼格式吧,否則別人怎麼知道能不能處理?-->方法調用必須簽名和SEL 相匹配纔會被調用。 咱們來看看官方文檔怎麼解釋的

這個方法要想有效地防止崩潰的話有個使用前提就是要實現另一個方法 - (void)forwardInvocation:(NSInvocation *)anInvocation; 那麼咱們來試一下:

  • 咱們仍是使用以前準備的工程實現這兩個方法,只是把上一步註釋掉。
// 方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(revival)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息轉發 -- 開始祈禱誰來處理一下
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"%@",anInvocation);
}
複製代碼
  • 調用查看一下結果,走了forwardInvocation方法,可是沒有崩潰直接走完了。--> 意味着若是我發出的方法簽名沒有人處理這個方法調用流程就結束了,至關於一個無效的方法,沒有任何響應。

5. 方法旅程的第四站--> doesNotRecognizeSelector

若是咱們調用沒有實現的方法,既沒有動態決議、也沒有轉發給其餘對象處理同時也沒有寫求助帖🙏好心人來處理此時蘋果爸爸也幫不了你,只好結束你的方法旅程,給你一張紅色的回程票doesNotRecognizeSelector --> 熟悉的崩潰

6. 寫在最後的回顧和思考

6.1方法的流程回顧

objc_msgSend彙編流程 -->從類緩存中快速查找imp
    _class_lookupMethodAndLoadCache3 --> 開始進入c、c++慢速查找
        lookUpImpOrForward --> 繼承鏈上的類的方法表裏遍歷查找,找到了緩存一份而後返回imp
            _class_resolveMethod --> 開始查看是否有動態決議,若是有給到imp,從新lookUpImpOrForward
                forwardingTargetForSelector --> 本身沒有處理,是否有交給別人代理處理。
                    methodSignatureForSelector + forwardInvocation --> 若是也沒有代理者,請按照規範寫求助信
                        doesNotRecognizeSelector --> 若是什麼都不作,你太懶了 蘋果爸爸表示上帝也救不了你。
複製代碼

6.2方法旅程的思考

通過這段方法探索的旅程我領悟了一些東西

  • 崩潰是能夠有效預防的,甚至是能夠本身蒐集的。--> 原來那些崩潰統計的SDK也是從這個思路去作的
  • 以前以爲很神奇的組件化、路由啥的,通過此次旅程已經被我揭掉了神祕面紗。--> 調用方法並不必定須要引入相應的類的頭文件
  • 蘋果對方法調用防崩潰的策略仍是很豐富的。--> 給了開發人員3次挽救處理的機會
  • 方法在下層調用匹配是操做的是SEL,是一個數值而不是字符串比較。並且方法是有簽名的,只有SEL和簽名匹配成功纔會認爲是能夠調用的方法。
  • 其實在每次方法調用的時候都傳了一個id(調用方法的對象),這也是爲何咱們能在任何方法裏面能輕易地拿到self
  • 一些控制打印的東西能很好地幫助咱們進行Bug分析,如void instrumentObjcMessageSends(BOOL flag),在合適的時候拓展做用域就能跟蹤一些方法調用的線索。

感謝你們的閱讀,若是你以爲寫得還能夠請動動大家的小手給我點個贊。我會更有動力給你們分享一下好東西。下一次計劃更新關於類的加載的文章。有興趣交流學習的能夠加我QQ:578200388

相關文章
相關標籤/搜索