—— iOS 運行時中方法的調(diào)用流程

1. 消息發(fā)送

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

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

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

我們可以直接通過調(diào)用objc_msgSend方法,來完成[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ù)會報warning俺亮;也可通過宏定義去掉warning。
實例本讥、類、元類

iOS 中所有的類都是繼承于 NSObject撞芍,一個對象所具有的方法分為實例方法和類方法验毡,編譯完成后的對象中,存在一個實例方法鏈表狮辽、一個緩存方法鏈表。當(dāng)實例調(diào)用方法經(jīng)objc_msgSend時:首先动看,在相應(yīng)操做的對象中的緩存方法列表中找調(diào)用的方法,若找到仇轻,轉(zhuǎn)向相應(yīng)的實現(xiàn)并執(zhí)行;若沒找到疲陕,在對象的方法列表中查找,若是找到诅岩,轉(zhuǎn)向相應(yīng)的實現(xiàn)并執(zhí)行吩谦;若是沒找到逮京,則遞歸的去父類指針?biāo)赶虻念悓ο蠓椒斜碇胁檎依撩蓿灰源祟愅撇哐希羰且恢钡礁惗紱]有找到妻导,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā)機制术浪;若是沒有重寫攔截調(diào)用方法胰苏,程序報錯硕并;

  • 調(diào)用對象方法(給實例對象發(fā)消息)
    根據(jù)實例對象的isa指針去該對象的類方法中查找倔毙,若找到則執(zhí)行陕赃;
    若沒找到么库,遞歸的去該類的父類類對象中查找廊散,直到根類NSObject梧疲;
    如果都沒有找到就報錯(還有三次挽救的機會)

  • 調(diào)用類方法(給類對象發(fā)送消息)
    根據(jù)類對象的isa指針去元對象中查找幌氮,若找到則執(zhí)行该互;
    若沒找到宇智,遞歸的去父元類對象中查找,直到根類NSOject喂分;
    如果都沒有找到就報錯(也有三次挽救的機會)

消息發(fā)送

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

消息轉(zhuǎn)發(fā)也被稱為攔截調(diào)用蒲祈,就是在找不到調(diào)用的方法后梆掸,且在程序崩潰以前酸钦,有機會經(jīng)過重寫NSObject的四個方法來補救處理:

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

// 讓別的對象去執(zhí)行這個函數(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ā)

利用以上機制,可以對resolveInstanceMethod 和 resolveClassMethod 兩個方法進行方法交換风皿,攔截可能出現(xiàn)的 iOS 崩潰桐款,然后自定義處理魔眨。

3. 實例

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

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

第一階段磺芭,先征詢接收者所屬的類钾腺,是否需要動態(tài)的添加方法讥裤,用來處理當(dāng)前未找到的方法己英。對象在無法解讀消息時會首先調(diào)用所屬類的下列類方法损肛,來判斷是否能接收消息:

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

例:

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

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

第二階段捏检,如果動態(tài)方法解析沒有發(fā)現(xiàn)添加的方法贯城,那么嘗試轉(zhuǎn)發(fā)給其他對象來處理這個方法能犯。該步驟調(diào)用的方法是:

// 嘗試轉(zhuǎn)發(fā)給其他對象來處理這個方法
- (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;//如果可以響應(yīng)該方法,則直接轉(zhuǎn)交新對象處理
        }
    }
    return [super forwardingTargetForSelector:aSelector];//如果無合適的備選對象挎峦,則繼續(xù)轉(zhuǎn)發(fā)
}
3.3 完整的消息轉(zhuǎn)發(fā)機制

第三階段,如果沒有可用的備選者透典,那么系統(tǒng)就會把消息所有相關(guān)內(nèi)容封裝成一個NSInvocation對象,再做最后的嘗試税弃,啟動完整的消息轉(zhuǎn)發(fā)则果。先調(diào)用methodSignatureForSelector:獲取方法簽名西壮,然后再調(diào)用forwardInvocation:進行處理,這一步的處理可以直接轉(zhuǎn)發(fā)給其它對象做修,即和第二步的效果等效饰及,但是很少有人這么干燎含,因為消息處理越靠后腿短,就表示處理消息的成本越大,性能的開銷就越大铣除。所以鹦付,在這種方式下敲长,一般會改變消息內(nèi)容祈噪,比如增加參數(shù)辑鲤,改變選擇子等等,具體根據(jù)實際情況而定弛随。

// 獲取方法簽名
 - (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]) {
        /* 手動創(chuàng)建簽名
         寫法例子一  v@:@
         字符說明:(1)v:返回值類型void;(2)@:id類型,執(zhí)行sel的對象;(3): SEL;(4)@:參數(shù)
         
         寫法例子二  @@:
         字符說明:(1)@:返回值類型id;(2)@:id類型,執(zhí)行sel的對象;(3):SEL
        
         */
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return signature;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //*-----------  處理方式一:不改變sel -------------*/
    // 拿到這個消息
    SEL selector = [anInvocation selector];
    // 轉(zhuǎn)發(fā)消息
    AnotherObject *otherObject = [[AnotherObject alloc] init];
    if ([otherObject respondsToSelector:selector]) {
        // 調(diào)用這個對象,進行轉(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ù)";
    // 消息的第一個參數(shù)是self,第二個參數(shù)是選擇子厉斟,所以"參數(shù)"是第三個參數(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]) {//交給另外的對象來處理
            [anInvocation invokeWithTarget:otherObject];
            return;
        }
    }
    [super forwardInvocation:anInvocation];
    //*---------------------------------------------*/
}

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

4. 應(yīng)用場景

  1. 為@dynamic等實現(xiàn)方法
    使用 @synthesize 可以為 @property 自動生成 getter 和 setter 方法(現(xiàn) Xcode 版本中,會自動生成)究飞,而 @dynamic 則是告訴編譯器,不用生成 getter 和 setter 方法媒峡。當(dāng)使用 @dynamic 時谅阿,我們可以使用消息轉(zhuǎn)發(fā)機制签餐,來動態(tài)添加 getter 和 setter 方法盯串。當(dāng)然你也可用其他的方法來實現(xiàn)体捏。

  2. 間接實現(xiàn)多繼承
    Objective-C本身不支持多繼承糯崎,這是因為消息機制名稱查找發(fā)生在運行時而非編譯時,很難解決多個基類可能導(dǎo)致的二義性問題乔询,但是可以通過消息轉(zhuǎn)發(fā)機制在內(nèi)部創(chuàng)建多個功能的對象竿刁,把不能實現(xiàn)的功能給轉(zhuǎn)發(fā)到其他對象上去食拜,這樣就做出來一種多繼承的假象负甸。轉(zhuǎn)發(fā)和繼承相似痹届,可用于為OC編程添加一些多繼承的效果队腐,一個對象把消息轉(zhuǎn)發(fā)出去柴淘,就好像他把另一個對象中放法接過來或者“繼承”一樣为严。消息轉(zhuǎn)發(fā)彌補了objc不支持多繼承的性質(zhì)第股,也避免了因為多繼承導(dǎo)致單個類變得臃腫復(fù)雜炸茧。

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

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

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

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

5. 總結(jié)

由于OC的動態(tài)特性,在編譯過程向類發(fā)送了其無法理解的消息并不會報錯焚刺,因為在運行時敛摘,我們可以改變對象調(diào)用的方法、向類中添加方法乳愉。只有當(dāng)程序運行起來之后兄淫,才知道要真正執(zhí)行哪個函數(shù)(動態(tài)綁定)屯远。

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

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

簡單理解:

  1. 首先科平,若對象無法響應(yīng)某個方法調(diào)用,則進入消息轉(zhuǎn)發(fā)流程。
  2. 開始第一步,通過運行時的動態(tài)方法解析楣责,可以將需要的某個方法及汉,加入到類中温眉。
  3. 上一步失敗砂心,開始第二步,將消息轉(zhuǎn)發(fā)給其他對象處理衰抑。
  4. 上述兩步失敗赃春,啟動完整的消息轉(zhuǎn)發(fā)機制,通過封裝NSInvocation,明確指出方法的響應(yīng)者(甚至改變SEL)摘悴。
  5. 上述都失敗挫以,拋出異常。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市翁潘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌出刷,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枷邪,居然都是意外死亡芥吟,警方通過查閱死者的電腦和手機种樱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門取董,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嚼沿,你說我怎么就攤上這事辨图《幻海” “怎么了调窍?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耸弄。 經(jīng)常有香客問我计呈,道長总寒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任摩桶,我火速辦了婚禮,結(jié)果婚禮上裙盾,老公的妹妹穿的比我還像新娘。我一直安慰自己染乌,他們只是感情好山孔,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荷憋,像睡著了一般台颠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勒庄,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天串前,我揣著相機與錄音,去河邊找鬼锅铅。 笑死酪呻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盐须。 我是一名探鬼主播玩荠,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贼邓!你這毒婦竟也來了阶冈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤塑径,失蹤者是張志新(化名)和其女友劉穎女坑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體统舀,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡匆骗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年劳景,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碉就。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡盟广,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瓮钥,到底是詐尸還是另有隱情筋量,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布碉熄,位于F島的核電站桨武,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锈津。R本人自食惡果不足惜呀酸,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望一姿。 院中可真熱鬧七咧,春花似錦、人聲如沸叮叹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛉顽。三九已至蝗砾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間携冤,已是汗流浹背悼粮。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留曾棕,地道東北人扣猫。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像翘地,于是被迫代替她去往敵國和親申尤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容