—— iOS 運(yùn)行時(shí)中方法的調(diào)用流程

1. 消息發(fā)送

在iOS運(yùn)行時(shí)系統(tǒng)中,調(diào)用方法的本質(zhì)就是利用objc_msgSend進(jìn)行消息發(fā)送:

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

// clang 編譯后的底層實(shí)現(xiàn)
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方法來(lái)完成[person sayNB]的功能,其打印結(jié)果一致
LGPerson *person = [LGPerson alloc];   
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];

我們可以直接通過(guò)調(diào)用objc_msgSend方法,來(lái)完成[person sayNB]的功能,查看其打印是否是一致:

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

iOS 中所有的類都是繼承于 NSObject,一個(gè)對(duì)象所具有的方法分為實(shí)例方法和類方法,編譯完成后的對(duì)象中,存在一個(gè)實(shí)例方法鏈表、一個(gè)緩存方法鏈表。當(dāng)實(shí)例調(diào)用方法經(jīng)objc_msgSend時(shí):首先,在相應(yīng)操做的對(duì)象中的緩存方法列表中找調(diào)用的方法,若找到,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行;若沒(méi)找到,在對(duì)象的方法列表中查找,若是找到,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行;若是沒(méi)找到,則遞歸的去父類指針?biāo)赶虻念悓?duì)象方法列表中查找;以此類推,若是一直到根類都沒(méi)有找到,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā)機(jī)制;若是沒(méi)有重寫攔截調(diào)用方法,程序報(bào)錯(cuò);

  • 調(diào)用對(duì)象方法(給實(shí)例對(duì)象發(fā)消息)
    根據(jù)實(shí)例對(duì)象的isa指針去該對(duì)象的類方法中查找,若找到則執(zhí)行;
    若沒(méi)找到,遞歸的去該類的父類類對(duì)象中查找,直到根類NSObject;
    如果都沒(méi)有找到就報(bào)錯(cuò)(還有三次挽救的機(jī)會(huì))

  • 調(diào)用類方法(給類對(duì)象發(fā)送消息)
    根據(jù)類對(duì)象的isa指針去元對(duì)象中查找,若找到則執(zhí)行;
    若沒(méi)找到,遞歸的去父元類對(duì)象中查找,直到根類NSOject;
    如果都沒(méi)有找到就報(bào)錯(cuò)(也有三次挽救的機(jī)會(huì))

消息發(fā)送

2. 消息轉(zhuǎn)發(fā)

消息轉(zhuǎn)發(fā)也被稱為攔截調(diào)用,就是在找不到調(diào)用的方法后,且在程序崩潰以前,有機(jī)會(huì)經(jīng)過(guò)重寫NSObject的四個(gè)方法來(lái)補(bǔ)救處理:

// 有機(jī)會(huì)讓類,實(shí)現(xiàn)并添加這個(gè)sel
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 讓別的對(duì)象去執(zhí)行這個(gè)函數(shù)
- (id)forwardingTargetForSelector:(SEL)aSelector;
// (函數(shù)執(zhí)行器)將目標(biāo)函數(shù)以其它形式執(zhí)行
- (void)forwardInvocation:(NSInvocation *)anInvocation;

若以上都不中,調(diào)用 NSObject 的 doesNotRecognizeSelector 方法拋出異常:

- (void)doesNotRecognizeSelector:(SEL)aSelector;
消息轉(zhuǎn)發(fā)

利用以上機(jī)制,可以對(duì)resolveInstanceMethod 和 resolveClassMethod 兩個(gè)方法進(jìn)行方法交換,攔截可能出現(xiàn)的 iOS 崩潰,然后自定義處理。

3. 實(shí)例

消息轉(zhuǎn)發(fā)機(jī)制依次的三個(gè)過(guò)程:1)動(dòng)態(tài)方法解析;2)轉(zhuǎn)發(fā)給其他備用的接收對(duì)象;3)消息所有相關(guān)內(nèi)容封裝成一個(gè)NSInvocation對(duì)象,再做最后的嘗試。

3.1 動(dòng)態(tài)方法解析

第一階段,先征詢接收者所屬的類,是否需要?jiǎng)討B(tài)的添加方法,用來(lái)處理當(dāng)前未找到的方法。對(duì)象在無(wú)法解讀消息時(shí)會(huì)首先調(diào)用所屬類的下列類方法,來(lái)判斷是否能接收消息:

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

例:

//消息轉(zhuǎn)發(fā)機(jī)制的第一步 :動(dòng)態(tài)方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selName = NSStringFromSelector(sel);
    if ([selName hasPrefix:@"doSomeThing"]) {//判斷特定無(wú)法響應(yīng)的方法
        class_addMethod(self, sel, (IMP)otherOneDoSomeThing, "v@:");//動(dòng)態(tài)添加響應(yīng)方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//動(dòng)態(tài)將實(shí)現(xiàn)轉(zhuǎn)到這個(gè)函數(shù)(或者就是單純的添加doSomeThing方法)
void otherOneDoSomeThing(id self ,SEL _cmd){
    NSLog(@"class:%@, sel:%s",self,sel_getName(_cmd));
    NSLog(@"原對(duì)象無(wú)法響應(yīng)該消息,在動(dòng)態(tài)方法解析時(shí)添加了一個(gè)方法來(lái)處理該消息");
}
3.2 備用的接收者

第二階段,如果動(dòng)態(tài)方法解析沒(méi)有發(fā)現(xiàn)添加的方法,那么嘗試轉(zhuǎn)發(fā)給其他對(duì)象來(lái)處理這個(gè)方法。該步驟調(diào)用的方法是:

// 嘗試轉(zhuǎn)發(fā)給其他對(duì)象來(lái)處理這個(gè)方法
- (id) forwardingTargetForSelector:(SEL)selector;

例:

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

第三階段,如果沒(méi)有可用的備選者,那么系統(tǒng)就會(huì)把消息所有相關(guān)內(nèi)容封裝成一個(gè)NSInvocation對(duì)象,再做最后的嘗試,啟動(dòng)完整的消息轉(zhuǎn)發(fā)。先調(diào)用methodSignatureForSelector:獲取方法簽名,然后再調(diào)用forwardInvocation:進(jìn)行處理,這一步的處理可以直接轉(zhuǎn)發(fā)給其它對(duì)象,即和第二步的效果等效,但是很少有人這么干,因?yàn)橄⑻幚碓娇亢螅捅硎咎幚硐⒌某杀驹酱?,性能的開(kāi)銷就越大。所以,在這種方式下,一般會(huì)改變消息內(nèi)容,比如增加參數(shù),改變選擇子等等,具體根據(jù)實(shí)際情況而定。

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

例:

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

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //*-----------  處理方式一:不改變sel -------------*/
    // 拿到這個(gè)消息
    SEL selector = [anInvocation selector];
    // 轉(zhuǎn)發(fā)消息
    AnotherObject *otherObject = [[AnotherObject alloc] init];
    if ([otherObject respondsToSelector:selector]) {
        // 調(diào)用這個(gè)對(duì)象,進(jìn)行轉(zhuǎn)發(fā)
        [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 = @"參數(shù)";
    // 消息的第一個(gè)參數(shù)是self,第二個(gè)參數(shù)是選擇子,所以"參數(shù)"是第三個(gè)參數(shù)
    [anInvocation setArgument:&param atIndex:2];
    
    if ([self respondsToSelector:selector]) {//如果自己響應(yīng),就自己處理
        [anInvocation invokeWithTarget:self];
        return;
    } else {
        AnotherObject * otherObject = [[AnotherObject alloc] init];
        if ([otherObject respondsToSelector:selector]) {//交給另外的對(duì)象來(lái)處理
            [anInvocation invokeWithTarget:otherObject];
            return;
        }
    }
    [super forwardInvocation:anInvocation];
    //*---------------------------------------------*/
}

//類中的另一個(gè)方法,來(lái)處理消息
- (void)myAnotherMethod:(NSString*)para
{
    NSLog(@"交給我自己的另一個(gè)方法來(lái)處理:%@", para);
}
消息轉(zhuǎn)發(fā)機(jī)制簡(jiǎn)圖

4. 應(yīng)用場(chǎng)景

  1. 為@dynamic等實(shí)現(xiàn)方法
    使用 @synthesize 可以為 @property 自動(dòng)生成 getter 和 setter 方法(現(xiàn) Xcode 版本中,會(huì)自動(dòng)生成),而 @dynamic 則是告訴編譯器,不用生成 getter 和 setter 方法。當(dāng)使用 @dynamic 時(shí),我們可以使用消息轉(zhuǎn)發(fā)機(jī)制,來(lái)動(dòng)態(tài)添加 getter 和 setter 方法。當(dāng)然你也可用其他的方法來(lái)實(shí)現(xiàn)。

  2. 間接實(shí)現(xiàn)多繼承
    Objective-C本身不支持多繼承,這是因?yàn)橄C(jī)制名稱查找發(fā)生在運(yùn)行時(shí)而非編譯時(shí),很難解決多個(gè)基類可能導(dǎo)致的二義性問(wèn)題,但是可以通過(guò)消息轉(zhuǎn)發(fā)機(jī)制在內(nèi)部創(chuàng)建多個(gè)功能的對(duì)象,把不能實(shí)現(xiàn)的功能給轉(zhuǎn)發(fā)到其他對(duì)象上去,這樣就做出來(lái)一種多繼承的假象。轉(zhuǎn)發(fā)和繼承相似,可用于為OC編程添加一些多繼承的效果,一個(gè)對(duì)象把消息轉(zhuǎn)發(fā)出去,就好像他把另一個(gè)對(duì)象中放法接過(guò)來(lái)或者“繼承”一樣。消息轉(zhuǎn)發(fā)彌補(bǔ)了objc不支持多繼承的性質(zhì),也避免了因?yàn)槎嗬^承導(dǎo)致單個(gè)類變得臃腫復(fù)雜。

  3. 實(shí)現(xiàn)多重代理
    利用消息轉(zhuǎn)發(fā)機(jī)制可以無(wú)代碼侵入的實(shí)現(xiàn)多重代理,讓不同對(duì)象可以同時(shí)代理同個(gè)回調(diào),然后在各自負(fù)責(zé)的區(qū)域進(jìn)行相應(yīng)的處理,降低了代碼的耦合程度。

這里就是利用了消息轉(zhuǎn)發(fā)機(jī)制的第三個(gè)階段,將NSIvocation分發(fā)給多個(gè)代理去響應(yīng)。
https://blog.csdn.net/kingjxust/article/details/49559091

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

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

5. 總結(jié)

由于OC的動(dòng)態(tài)特性,在編譯過(guò)程向類發(fā)送了其無(wú)法理解的消息并不會(huì)報(bào)錯(cuò),因?yàn)樵谶\(yùn)行時(shí),我們可以改變對(duì)象調(diào)用的方法、向類中添加方法。只有當(dāng)程序運(yùn)行起來(lái)之后,才知道要真正執(zhí)行哪個(gè)函數(shù)(動(dòng)態(tài)綁定)。

OC消息發(fā)送原理、方法查找過(guò)程:

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

簡(jiǎn)單理解:

  1. 首先,若對(duì)象無(wú)法響應(yīng)某個(gè)方法調(diào)用,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
  2. 開(kāi)始第一步,通過(guò)運(yùn)行時(shí)的動(dòng)態(tài)方法解析,可以將需要的某個(gè)方法,加入到類中。
  3. 上一步失敗,開(kāi)始第二步,將消息轉(zhuǎn)發(fā)給其他對(duì)象處理。
  4. 上述兩步失敗,啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制,通過(guò)封裝NSInvocation,明確指出方法的響應(yīng)者(甚至改變SEL)。
  5. 上述都失敗,拋出異常。

OC、運(yùn)行時(shí)初始化時(shí)機(jī):
http://m.itdecent.cn/p/4b93b40977b5
https://blog.csdn.net/weixin_30920513/article/details/100093380

參考文章:
http://m.itdecent.cn/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調(diào)用方法
http://m.itdecent.cn/p/e24b3420f1b4

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容