Object-C的消息傳遞機制和method swizzling方法混淆

objc_msgSend
在Object-C中,我們經常調用一個對象的方法,通常我們將這個過程成爲 消息傳遞。不同於 C 語言對對象方法的靜態調用,Object-C 是通過 Dynamic Binding (動態綁定) 機制來實現消息傳遞的,對象對於詳細的響應和處理都是在 runtime 運行時才能決定。
通常,一個消息傳遞是這個樣子的
id result = [object messageName:prameter];  
而在程序編譯時,編譯器會自動將其轉爲C函數
id objc_msgSend(id self,SEL cmd, ...) ;
這是一個可變參數的函數,它接受了兩個或以上的參數,包括消息的目標對象,消息的seletor,還有依照順序傳入的消息參數。
objc_msgSend 函數負責的是處理一個髮往對象的消息,讓對象調用正確地方法,其機制如下
首先,它會在這個Object所在的class中尋找與seletor相應的方法,若找到,就跳至這個方法的實現執行
若找不到相應方法,就會按着繼承的層次不斷在該對象的所屬class的父類尋找相應方法
若找不到,則進行 消息的前向處理
相對於C中對象方法的靜態調用,objc_msgSend 做了更多事情,但這並不是整個程序性能的瓶頸所在,並且,objc_msgSend 還存在一個全局的cache map 進行消息調用結果的緩存 ,同一 class 的 同一方法 多次調用將會十分快速。
當然,在coding過程中,我們並不需要關心消息傳遞的實現細節,但這些可以幫助我們更好理解程序背後的運行機制,也能更好地理解後面講的東東~
消息的前向處理
一個類只能理解已經在其內部定義好的消息,在objc的機制中,我們可以在runtime運行時在一個類中加入新的函數,因此我們的編譯器無法完全阻止你向一個對象調用一個它並沒定義的函數。比如說,我經常遇到的bug就是不小心把一個NSNumber當成NSString時,會發生以下悲劇
NSString *str = @(1);  
NSLog(@"%@",[str lowercaseString]);
程序會crash,並輸出
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x8d71510' 
當這件不幸的事情發生時,這個對象就會調用其消息的前向機制,決定這個消息的去向。而以上的輸出,是當對象的所有前向機制處理方法都沒有處理該消息時,由基類 NSobject 的 doesNotRecognizeSeletor: 方法來輸出控制檯的這些信息,來告訴你沒法處理的對象與消息。
處理未定義的消息,一個對象通常有兩種途徑:
1. 首先,這個對象會調用+ (BOOL)resolveInstanceMethod:(SEL)sel 來決定是否要通過往這個類加入新的方法來處理這個消息,若返回YES,則告訴編譯器已經加入了相應的方法實現( 通過 class_addMethod(self,sel,(IMP),@"v@:@") 方法 ),能夠再次調用該方法,否則,就進入前向處理的下一步。
2. 如果上一種方法返回NO,那對象將對於這個消息進行第二次處理,調用-(id)forwardingTargetForSelector:(SEL)aSelector這個函數返回了另一對象,告訴編譯器發往本對象的消息可以扔給另一對象處理。這個方法有很多應用途徑,一般可以返回自己所擁有的對象,這樣在外部看起來就像自己能處理這個消息一樣,高貴大氣有內涵~
當然,如果該函數返回nil時,就證明第二部也失敗了,我們就進入了最終處理方法。
3. 到了這個地步,對象將調用最終的處理函數- (void):(NSInvocation *)anInvocation 可以看到,我們把這個消息包裝成了NSInvocation 對象,裏面包含了消息的所有信息,這個函數完成了對於消息的最後分配。
一般來說,在這個消息裏,可以對消息進行重新分配,發往其他對象,不過這就和第二步的效果一樣了,或者,可以修改這個消息的參數,或者消息名等等任意處理當我們已經沒法處理這個消息時,需要調用父類的[super forwardInvocation:anInvocation];來處理,而層層處理後消息仍未被處理,則 NSObject 對於這個方法的實現將被調用,就會調用doesNotRecognizeSeletor: 這個方法來拋出異常啦~
函數混淆 (Method Swizzling)
從以上我們可以得知,需要在運行時runtime ,編譯器才能決定如何處理傳給一個對象的消息,那麼,我們也能夠在運行時改變一個對象對消息的響應,這就是函數混淆。
一個類在編譯器中擁有一張映射表,讓動態消息傳遞系統能夠通過方法名來找到響應的實現,這些實現是用一些叫做 IMPs 的函數指針進行儲存的,就像這樣id (*IMP)(id SEL,...) 而這個表在程序運行時能被修改,讓這些方法名指向其他函數實現IMP,甚至指向自己實現的方法。主要通過兩個方法:
method_exchangeImplementations(Method m1, Method m2);  
這個方法能夠將兩個方法名指向的函數指針交換;
class_getInstanceMethod(Class cls, SEL name);  
這個方法能夠通過方法名和類名獲取相應的函數指針,當然也有獲取類方法的函數
class_getClassMethod(Class cls, SEL name);  
通過這兩個方法,我們就能自由地交換方法名對應的函數實現,來實現函數混淆的效果。
Method yesMethod = class_getInstanceMethod([self class], @selector(sayYES));   Method noMethod = class_getInstanceMethod([self class], @selector(sayNO));   method_exchangeImplementations(yesMethod, noMethod);  
這樣子 sayYES 與 sayNO 的方法實現就會交換。
函數混淆在調試程序時十分有效,它能夠修改一些隱藏了實現的函數的指向,對其實現進行一些添加,並在全局造成影響。當然,對其的使用要慎重,畢竟影響了可讀性,並且隨意修改了原來的函數實現,很容易造成一系列的bug~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章