—— iOS 運行時中方法的調用流程

1. 消息發送

在iOS運行時系統中,調用方法的本質就是利用objc_msgSend進行消息發送:

// main.m 中的方法調用
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];

// clang 編譯后的底層實現
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

// 用objc_msgSend方法來完成[person sayNB]的功能,其打印結果一致
LGPerson *person = [LGPerson alloc];   
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];

我們可以直接通過調用objc_msgSend方法,來完成[person sayNB]的功能,查看其打印是否是一致:

  1. 直接調用objc_msgSend,需要導入<objc/message.h>;
  2. 工程target --> Build Setting -->enable strict checking of obc_msgSend calls由YES 改為NO,否則objc_msgSend的參數會報warning;也可通過宏定義去掉warning。
實例、類、元類

iOS 中所有的類都是繼承于 NSObject,一個對象所具有的方法分為實例方法和類方法,編譯完成后的對象中,存在一個實例方法鏈表、一個緩存方法鏈表。當實例調用方法經objc_msgSend時:首先,在相應操做的對象中的緩存方法列表中找調用的方法,若找到,轉向相應的實現并執行;若沒找到,在對象的方法列表中查找,若是找到,轉向相應的實現并執行;若是沒找到,則遞歸的去父類指針所指向的類對象方法列表中查找;以此類推,若是一直到根類都沒有找到,轉向攔截調用,走消息轉發機制;若是沒有重寫攔截調用方法,程序報錯;

  • 調用對象方法(給實例對象發消息)
    根據實例對象的isa指針去該對象的類方法中查找,若找到則執行;
    若沒找到,遞歸的去該類的父類類對象中查找,直到根類NSObject;
    如果都沒有找到就報錯(還有三次挽救的機會)

  • 調用類方法(給類對象發送消息)
    根據類對象的isa指針去元對象中查找,若找到則執行;
    若沒找到,遞歸的去父元類對象中查找,直到根類NSOject;
    如果都沒有找到就報錯(也有三次挽救的機會)

消息發送

2. 消息轉發

消息轉發也被稱為攔截調用,就是在找不到調用的方法后,且在程序崩潰以前,有機會經過重寫NSObject的四個方法來補救處理:

// 有機會讓類,實現并添加這個sel
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 讓別的對象去執行這個函數
- (id)forwardingTargetForSelector:(SEL)aSelector;
// (函數執行器)將目標函數以其它形式執行
- (void)forwardInvocation:(NSInvocation *)anInvocation;

若以上都不中,調用 NSObject 的 doesNotRecognizeSelector 方法拋出異常:

- (void)doesNotRecognizeSelector:(SEL)aSelector;
消息轉發

利用以上機制,可以對resolveInstanceMethod 和 resolveClassMethod 兩個方法進行方法交換,攔截可能出現的 iOS 崩潰,然后自定義處理。

3. 實例

消息轉發機制依次的三個過程:1)動態方法解析;2)轉發給其他備用的接收對象;3)消息所有相關內容封裝成一個NSInvocation對象,再做最后的嘗試。

3.1 動態方法解析

第一階段,先征詢接收者所屬的類,是否需要動態的添加方法,用來處理當前未找到的方法。對象在無法解讀消息時會首先調用所屬類的下列類方法,來判斷是否能接收消息:

// 如果是實例方法 (返回值表示這個類能否新增一個實例方法處理此選擇子)
+ (BOOL) resolveInstanceMethod:(SEL)selector;
// 如果是類方法(類方法的添加需要在其“元類”里面。)
+ (BOOL) resolveClassMethod:(SEL)selector;

例:

//消息轉發機制的第一步 :動態方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selName = NSStringFromSelector(sel);
    if ([selName hasPrefix:@"doSomeThing"]) {//判斷特定無法響應的方法
        class_addMethod(self, sel, (IMP)otherOneDoSomeThing, "v@:");//動態添加響應方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//動態將實現轉到這個函數(或者就是單純的添加doSomeThing方法)
void otherOneDoSomeThing(id self ,SEL _cmd){
    NSLog(@"class:%@, sel:%s",self,sel_getName(_cmd));
    NSLog(@"原對象無法響應該消息,在動態方法解析時添加了一個方法來處理該消息");
}
3.2 備用的接收者

第二階段,如果動態方法解析沒有發現添加的方法,那么嘗試轉發給其他對象來處理這個方法。該步驟調用的方法是:

// 嘗試轉發給其他對象來處理這個方法
- (id) forwardingTargetForSelector:(SEL)selector;

例:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSString * selString = NSStringFromSelector(aSelector);
    if([@"doSomeThing" isEqualToString:selString]){
        OtherObject *someone = [[OtherObject alloc] init];//備選對象
        if ([someone respondsToSelector:aSelector]) {
            return someone;//如果可以響應該方法,則直接轉交新對象處理
        }
    }
    return [super forwardingTargetForSelector:aSelector];//如果無合適的備選對象,則繼續轉發
}
3.3 完整的消息轉發機制

第三階段,如果沒有可用的備選者,那么系統就會把消息所有相關內容封裝成一個NSInvocation對象,再做最后的嘗試,啟動完整的消息轉發。先調用methodSignatureForSelector:獲取方法簽名,然后再調用forwardInvocation:進行處理,這一步的處理可以直接轉發給其它對象,即和第二步的效果等效,但是很少有人這么干,因為消息處理越靠后,就表示處理消息的成本越大,性能的開銷就越大。所以,在這種方式下,一般會改變消息內容,比如增加參數,改變選擇子等等,具體根據實際情況而定。

// 獲取方法簽名
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// 完整的消息轉發(消息的受主、消息體、消息參數均封裝在內)
 - (void)forwardInvocation:(NSInvocation *)anInvocation

例:

//獲取方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *method = NSStringFromSelector(aSelector);
    if ([@"doSomeThing" isEqualToString:method]) {
        /* 手動創建簽名
         寫法例子一  v@:@
         字符說明:(1)v:返回值類型void;(2)@:id類型,執行sel的對象;(3): SEL;(4)@:參數
         
         寫法例子二  @@:
         字符說明:(1)@:返回值類型id;(2)@:id類型,執行sel的對象;(3):SEL
        
         */
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return signature;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //*-----------  處理方式一:不改變sel -------------*/
    // 拿到這個消息
    SEL selector = [anInvocation selector];
    // 轉發消息
    AnotherObject *otherObject = [[AnotherObject alloc] init];
    if ([otherObject respondsToSelector:selector]) {
        // 調用這個對象,進行轉發
        [anInvocation invokeWithTarget:otherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
    //*---------------------------------------------*/

    //*-----------  處理方式二:改變sel -------------*/
    SEL selector = @selector(myAnotherMethod:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:@selector(myAnotherMethod:)];
    NSString *param = @"參數";
    // 消息的第一個參數是self,第二個參數是選擇子,所以"參數"是第三個參數
    [anInvocation setArgument:&param atIndex:2];
    
    if ([self respondsToSelector:selector]) {//如果自己響應,就自己處理
        [anInvocation invokeWithTarget:self];
        return;
    } else {
        AnotherObject * otherObject = [[AnotherObject alloc] init];
        if ([otherObject respondsToSelector:selector]) {//交給另外的對象來處理
            [anInvocation invokeWithTarget:otherObject];
            return;
        }
    }
    [super forwardInvocation:anInvocation];
    //*---------------------------------------------*/
}

//類中的另一個方法,來處理消息
- (void)myAnotherMethod:(NSString*)para
{
    NSLog(@"交給我自己的另一個方法來處理:%@", para);
}
消息轉發機制簡圖

4. 應用場景

  1. 為@dynamic等實現方法
    使用 @synthesize 可以為 @property 自動生成 getter 和 setter 方法(現 Xcode 版本中,會自動生成),而 @dynamic 則是告訴編譯器,不用生成 getter 和 setter 方法。當使用 @dynamic 時,我們可以使用消息轉發機制,來動態添加 getter 和 setter 方法。當然你也可用其他的方法來實現。

  2. 間接實現多繼承
    Objective-C本身不支持多繼承,這是因為消息機制名稱查找發生在運行時而非編譯時,很難解決多個基類可能導致的二義性問題,但是可以通過消息轉發機制在內部創建多個功能的對象,把不能實現的功能給轉發到其他對象上去,這樣就做出來一種多繼承的假象。轉發和繼承相似,可用于為OC編程添加一些多繼承的效果,一個對象把消息轉發出去,就好像他把另一個對象中放法接過來或者“繼承”一樣。消息轉發彌補了objc不支持多繼承的性質,也避免了因為多繼承導致單個類變得臃腫復雜。

  3. 實現多重代理
    利用消息轉發機制可以無代碼侵入的實現多重代理,讓不同對象可以同時代理同個回調,然后在各自負責的區域進行相應的處理,降低了代碼的耦合程度。

這里就是利用了消息轉發機制的第三個階段,將NSIvocation分發給多個代理去響應。
https://blog.csdn.net/kingjxust/article/details/49559091

  1. iOS動態化更新(JSPatch、ReactiveCocoa等)
  • JSPatch,通過消息轉發機制來進行JS和OC的交互,從而實現iOS的熱更新。
  • 雖然蘋果大力整改熱更新讓JSPatch的審核通過率在有一段時間里面無法過審,但是后面bang神對源碼進行代碼混淆之后,基本上是可以過審了。
  • 下面截圖只摘出來用到消息轉發的部分:關鍵點就是在第三階段,通過invocation拿到方法參數,然后傳給JS,調用JS的實現函數。

http://blog.cnbang.net/tech/2808/
http://blog.cnbang.net/tech/2855/

5. 總結

由于OC的動態特性,在編譯過程向類發送了其無法理解的消息并不會報錯,因為在運行時,我們可以改變對象調用的方法、向類中添加方法。只有當程序運行起來之后,才知道要真正執行哪個函數(動態綁定)。

OC消息發送原理、方法查找過程:

  1. 調用一個方法(包括respondsToSelector),編譯器將OC代碼,轉換成C函數,給對象發送消息 : void objc_msgSend(id self, SEL cmd,...) ,第一個參數是接收者,第二個參數是方法(名),后面是消息的參數。
  2. objc_msgSend查找方法,實例對象根據其isa指針,找到其所屬的class,然后遍歷其methodLists,如果找到則根據IMP函數指針去調用,并且緩存(objc_cache);如果沒有找到,那么根據這個類的super_class找到其父類,再看其父類是否能相應這個方法就可以了,直到super_class為nil時,就無法響應這個方法了,此時就觸發消息轉發機制。
    當使用類名調用類方法(+方法)時,只需要根據class的isa指針,找到其meta-class,然后通過meta-class的methodLists找到相應的方法既可(“類”是“元類”的對象)。
  3. 如果對象接收到無法解讀的消息后(未查詢到該方法),就會啟動“消息轉發”機制,我們可在此過程告訴對象應該如何處理未知消息。如果我們不做任何處理,或處理無效,則會調用doesNotRecognizeSelector:,造成異常崩潰:unrecognized selector sent to instance 0xxx

簡單理解:

  1. 首先,若對象無法響應某個方法調用,則進入消息轉發流程。
  2. 開始第一步,通過運行時的動態方法解析,可以將需要的某個方法,加入到類中。
  3. 上一步失敗,開始第二步,將消息轉發給其他對象處理。
  4. 上述兩步失敗,啟動完整的消息轉發機制,通過封裝NSInvocation,明確指出方法的響應者(甚至改變SEL)。
  5. 上述都失敗,拋出異常。

OC、運行時初始化時機:
http://www.lxweimin.com/p/4b93b40977b5
https://blog.csdn.net/weixin_30920513/article/details/100093380

參考文章:
http://www.lxweimin.com/p/7e132cda35cd
https://www.cnblogs.com/feng9exe/p/10397102.html
https://www.shangmayuan.com/a/02d9b8b219b24d888ef93b97.html
https://blog.csdn.net/lin1109221208/article/details/108724965

iOS之使用NSInvocation調用方法
http://www.lxweimin.com/p/e24b3420f1b4

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容