標籤:運行時 訊息轉寄 objc_msgsend
第11條:理解objc_msgSend的作用
在對象上調用方法是OC中經常使用的功能。用OC術語來說這叫做:“傳遞訊息”(pass a message)。訊息有“名稱”(name)或者“選擇子”(selector),可以接收參數,而且可能還有返回值。
由於OC是C的超集,所以最好理解C語言的函數調用方式。C語言使用“靜態繫結”,就是說在編譯期就能決定運行時所應調用的函數。以下列代碼為例:
#import <stdio.h>void printHello(){ printf("Hello world\n");}void printGoodBye(){ printf("Goodbye world\n");}void doTheThing(int type){ if(type == 0){ printfHello(); }else{ printGoodbye();} return 0; }
編譯器在編譯代碼的時候就已經知道程式中有printHello與printGoodBye這兩個函數了,於是直接產生調用這些函數的指令。而函數地址實際上是寫入程式碼在指令之中的。若將剛才那段代碼寫成下面這樣,會如何呢?
#import <stdio.h>void printHello(){ printf("Hello world\n");}void printGoodBye(){ printf("Goodbye world\n");}void doTheThing(int type){ void (*fun)(); if(type == 0){ fun = printfHello(); }else{ fun = printGoodbye();} fun(); return 0; }
這時就得使用“動態綁定”(dynamic binding),因為所要用的函數知道運行時才能確定。編譯器在這個環境下產生的指令與剛才哪個例子不同,在第一個例子中if和else語句中都有函數調用指令。而第二個例子中,只有一個函數調用指令,不過待調用的地址無法寫入程式碼在指令之中,而是要運行期讀出來。
在OC中,如果向某對象傳遞訊息,那就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通C語言函數,然而對象收到訊息後,究竟該掉哪個方法則完全於運行期決定,甚至可以在程式運行時改變,這些特性使得OC稱為一門真正的動態語言。
給對象發訊息可以這樣寫:
id returnValue = [someObject messageName:parameter];
本例中,someObject叫做“接收者”,messageName叫做“選擇子”。選擇子與參數合起來稱為“訊息”。編譯器看到此訊息後,將其轉換為一條標準的C語言函數調用,所調用的函數乃是訊息傳遞機制的核心函數,叫做objc_msgSend,其原型如下:
void objc_msgSend(id self ,SEL cmd,...);
這是個參數個數可邊長的函數,經過轉化,剛才的函數被轉化為這樣:
id returnValue = objc_msgSend(someObject,@selector(messageName),parameter);
objc_msgSend函數會依據接收者與選擇子的類型來調用適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods)如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若找不到,那就沿著繼承體系繼續向上尋找,等找到名稱相符的方法之後再跳轉。。如果最終還是找不到相符的方法,那就執行“訊息轉寄”
這麼說來,想調用一個方法似乎需要很多步驟。所幸objc_msgSend會將匹配結果緩衝在“快速映射表”裡面。實際上,訊息派發(message dispatch)並非應用程式的瓶頸所在。
前面講的這部分內容只描述了部分訊息的調用過程,其他“邊界情況”(edge case)則需要交由OC運行環境中的另一些函數來處理:
● objc_msgSend_stret 待發的訊息要返回結構體
● objc_msgSend_fpret 訊息返回的是浮點數
● objc_msgSend_Super 要給超類發訊息。
剛才曾提到,objc_msgSend等函數一旦找到應該調用的方法實現之後,就會“跳轉過去”。之所以能這樣做,是因為OC對象的每隔方法都可以視為簡單的C函數,原型如下:
<return_type> class_selector(id self,SEL _cmd,...)
每隔類裡都有一張表格,其中的指標都會指向這個函數,而選擇子的名稱則是查表時所用的“鍵”。objc_msgSend等函數正是通過這張表格來尋找應該執行的方法並跳至其實現的。請注意,原型的樣子和objc_msgSend很像。這不是巧合,而是利用“尾調用最佳化”(tail-call optimization)技術,令“跳至方法實現”這一操作變得更簡單。
在實際編寫OC時,無須擔心這些問題,開發人員應該瞭解其底層工作原理。代碼究竟是如何執行的,而且能理解為何在調試的時候,棧資訊中總是出現objc_msgSend
【本節要點】
● 訊息由接收者、選擇子及參數構成。給某對象“發送訊息”也就相當於在該對象上“調用方法”
● 發給某對象的去全部訊息都要由“動態訊息派發系統”(dynamic message dispatch system)來處理。該系統會查出對應的方法,並執行其代碼
第12條:理解訊息轉寄機制 第11條講了對象的訊息傳遞機制,並強調了其重要性。本節講另外一個重要的問題,就是對象在接收到無法解讀的訊息之後會發生什麼情況。 若想令類能理解某條訊息,我們必須實現方法才行。如果沒有實現就會啟動“訊息轉寄”(message forwarding)機制。在控制台看到這樣的錯誤,說明啟動了訊息轉寄:
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87*** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'
上面這段異常資訊是由NSObject的“doesNotRecognizedSelector:”方法所拋出的,次異常表明:訊息接收者__NSCFNumber,其無法理解lowercaseString的選擇子。在本利中,訊息轉寄過程以應用程式崩潰而告終,開發人員可於轉寄過程中設定掛鈎,用於執行預訂的邏輯,而不公用程式崩潰。 訊息轉寄分為兩大階段。第一階段先徵詢接收者,所屬的類,看其是否能動態添加方法,已處理當前這個“未知選擇子”,這叫做“動態方法解析”。第二階段涉及“完整的訊息轉寄機制” 動態方法解析 對象在收到無法解讀的訊息後,首先將調用其所屬類的下列類方法: +(BOOL) resolveInstanceMethod:(SEL)selector 該方法的參數就是哪個未知的選擇子,其返回值為Boolean類型,表示這個類是否新增一個執行個體方法處理此選擇子。在繼續往下執行轉寄機制之前,本類有機會新增一個處理此選擇子的方法。假如尚未實現的方法不是執行個體方法而是類方法,那麼運行期系統就會調用另外一個方法,“resolveClassMethod” 使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等待啟動並執行時候動態插在類裡面就可以了。此方案通常用來實現@dynamic屬性,下面代碼展示了如何使用“resolveInstanceMethod:”來實現@dynamic屬性:
id autoDictionaryGetter(id self,self _cmd);void autoDictionarySetter(id self, SEL _cmd,id value);+(BOOL)resolveInstanceMethod:(SEL)selector{ NSString *selectorString = NSStringFromSelector(selector); if(/*selector is from a @dynamic property*/) { if([selectorString hasPrefix:@"set"]){ class_addMethod(self.selector,(IMP)autoDictionarySetter,"[email protected]:@"); }else{ class_addMethod(self.selector,(IMP)autoDictionaryGetter,"[email protected]:@"); } return YES; }return [super resolveInstanceMethod:selector];}
首先將選擇子化為字串,然後檢測其是否表示設定方法。若首碼為set,則表示“設定方法”,否則就是“擷取方法”。備援接收者 當前接收者還有第二次機會處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條訊息轉給其他接收者來註冊。
-(id) forwardingTargetForSelector:(SEL)selector
方法參數代表未知的選擇子,若當前接收者能找到備援對象,則將其返回,若找不到返回nil完整的訊息轉寄
如果轉寄演算法已經來到這一步,那麼唯一能做的就是啟動完整的訊息轉寄機制了。首先建立NSInvacaton對象,把與尚未處理的那條訊息有關的全部細節都封於其中,此對象包含選擇子、目標(target)及參數。在觸發NSInvocation對象時,“訊息派發系統”將親自出馬,把訊息指派給目標對象。
此步驟會調用下列方法轉寄訊息:
-(void) forwardInvocation:(NSInvocation*)invocation
這個方法可以實現得很簡單:只需要改變調用目標,使訊息在新目標上得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人採用這麼簡單的實現方式。比較有用的實現方式為:在觸發訊息前,先以某種方式改變訊息內容,比如追加另外一個參數,或是該換選擇子,等等。
實現此方法時,若發現某叫用作業不應本類處理,則需要調用超類的同名方法。這樣的話,繼承體系中的每隔類都有機會處理此調用請求,直至NSObject。如果最後調用了NSObject類的方法,那麼該方法還會繼而調用“doesNotRecognizeSelector:”以拋出異常,次異常表示選擇子最終未能得到處理。
訊息轉寄全流程
描述了訊息轉寄的各個步驟:
接收者在每一步均有機會處理訊息。步驟越往後,訊息處理的代價就越大。
第13條:用“方法調配技術”測試“黑盒方法” 第11條中解釋過:OC對象收到訊息之後,究竟會調用何種方法需要在運行期才能解析出來。你也許會問:於給定的選擇子名稱對應的方法是不是可以在運行期改變呢?沒錯,就是這樣。若能善用此特性,則可發揮出巨大優勢,因為我們既不需要原始碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣一來,新功能將在本類的所有執行個體中生效,而不是僅限於覆寫了相關方法的那些子類執行個體。次方案經常稱為“方法調配”(method swizzling)。 類的方法列表會把選擇子的名稱映射到相關的方法實現之上,使得“動態訊息派發系統”能夠肇東啊應該調用的方法。這些方法均以函數指標的形式來表示,這種指標叫做IMP,其原型如下:
id(*IMP)(id,SEL,..)
NSString類可以相應lowercaseString、uppercaseString、capitalizedString等選擇子。這張映射表中的每隔選擇字都映射到了不同的IMP之上。如
OC運行期系統提供的幾個方法都能夠用來操作這張表。開發人員可以向其新增選擇子,也可以改變某選擇子對應的方法實現,還可以交換選擇子所映射到的指標。經過幾次操作之後,類的方法表就會標稱這個樣子
在新的映射表中,多了一個名為newSelector的選擇子,capitalizedString的實現也變了,而lowercaseString與uppercaseString的實現則互換了。上述修改均無需編寫子類,只要修改了“方法表”的布局,就會反映到程式中所有的NSString執行個體之上。這下大家見識到此特性的強大之處了吧
下面看一下如何互換兩個方法實現。想交換方法實現,可用下列函數:
void method_exchangeImplementations(Method m1,Method m2)
此函數的兩個參數表示待交換的兩個方法實現,而方法實現則可通過下列函數獲得:
Method class_getInstanceMethod(Class aClass,SEL aSelector)
具體操作如下:
Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString));Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(uppercaseString));method_exchangeImplementations(originalMethod,swappedMethod);
實際應用,可以為那些黑盒方法增加日誌技術功能。比如NSString 的lowercaseString介面,想在lowercaseString中添加log,該如何做呢
@interface NSString (EOCMyAddtions)-(NSString*)eoc_myLowercaseString;@end@implementation NSString(EOCMyAdditions)-(NSString*) eoc_myLowercaseString{ NSString *lowercase = [self eoc_mylowercaseString]; NSLog(@"%@ =>%@",self,lowercase); return lowercase;}@end
這段代碼看上去好像會陷入遞迴調用的死迴圈,不過大家要記住,此方法是準備和lowercaseString方法互換的。所以,在運行期,eoc_myLowercaseString選擇子實際上對應於原有的lowercaseString方法實現。最後,通過下列代碼叫魂這兩個方法實現:
Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString));Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(roc_myLowercaseString));method_exchangeImplementations(originalMethod,swappedMethod);
執行完上訴代碼之後,只要在NSString執行個體上調用lowercaseString方法,就會輸出log了。
【本節要點】
● 在運行期間,可以向類中新增或者替換選擇子所對應的方法實現。
● 使用另一份實現來替換緣由的方法實現,這道工序叫:“方法調配”,開發人員常用此技術向原有實現中添加新功能。
● 一般來說,只有偵錯工具的時候才需要在運行期修改方法實現,這種做法不宜濫用。
第14條:理解“類對象”的用意
OC實際上是一門及其動態語言。第11條講解了運行期系統如何尋找並調用某方法的實現代碼,第12條則講述了訊息轉寄的原理:如果類無法立即響應某個選擇子,那麼就會啟動訊息轉寄流程。然而,訊息的接收者究竟是何物?是對象本身嗎?運行期系統如何知道某個對象的類型呢?物件類型並非在編譯器就綁定好了,而是要在運行期尋找。而且還有個特殊類型id,它能夠指代任意的OC物件類型。
我們先講一些基礎知識,看看OC對象本質是什麼。每個OC對象都是指向某塊記憶體資料的指標。所以在聲明變數時,類型後面都要跟一個星號(*)
NSString *pointerVariable = @"Some thing";
編過C語言程式的人都知道什麼意思。該變數“指向”(point to)NSString執行個體。所有OC對象都是如此,如果想把OC對象聲明在棧上,編譯器會報錯:
Sting stackVariable = @"Some thing";//error: interface type cannot be statically allocated
對於通用id類型,由於其本身已經是指標了,所以我們能夠這樣寫:
id genericTypeString = @"Some thing";
描述OC對象所用的資料結構定義在運行期程式庫的標頭檔裡,id類型本身也在定義這裡:
typedef struct objc_object{ Class isa;} *id;
由此可見,每隔對象結構體的首個成員是Class類的變數。該變數定義了對象所屬的類,通常稱為is a指標。例如,剛才的例子中所有的對象is a NSString,所以其“is a”指標就指向NSString。Class對象頁定義在運行期程式庫的標頭檔中:
typedef struct objc_class *class;struct objc_class{ Class isa; Class super_class; const char* name; long version; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols};
此結構圖存放類的“中繼資料”(metadata),例如類的執行個體實現了幾個方法,具備多少執行個體變數等資訊。此結構體的首個變數也是isa指標,這說明Class本身亦為OC對象。結構體裡還有個變數叫做super_class,它定義了本類的超類。累對象所屬的類型(也就是isa指標所指向的類型)是另外一個類,叫做“元類”(metaclass),用來表叔類對象本身所具備的中繼資料。“類方法”就定義於此處,因為這些方法可以理解成累對象的執行個體方法。每隔類僅有一個”類對象”,而每個“類對象”僅有一個與之相關的“元類”。
假設有個名為SomeClass的子類從NSObject中繼承而來,則其繼承體系如所示
super_class指標確立了繼承關係,而isa指標描述了執行個體所屬的類。通過這張布局關係圖即可執行“類型資訊查詢”。我們可以查出對象是否能響應某個選擇子,是否遵從某項協議,並且能看出此對象位於“類繼承體系”的那一部分。
在類繼承體系中查詢類型資訊
可以用類型資訊查詢方法來檢視類繼承體系。“isMemberOfClass:”能夠判斷出對象是否為某個特定類的執行個體。而“isKindOfClass:”則能夠判斷出對象是否為某類或其派生的執行個體。
NSMutableDictionary *dict = [NSMutableDictionary new];[dict isMemberOfClass:[NSDictionary class]];//no[dict isMemberOfClass:[NSMutableDictionary class]];//yes[dict isKindOfClass:[NSDictionary class]];//yes[dict isKindOfClass:[NSArray class]];//no
由於OC使用“動態類型系統”(dynamic typing),所以用於查詢對象所屬類的類型資訊查詢非常有用。從collecting中擷取對象時,通常是id類型,那就可以使用查詢類型資訊方法了。
【本節要點】
● 每隔執行個體都有一個指向Class對象的指標,用以表明其類型。從這些Class對象則構成了類的繼承體系。
● 如果物件類型無法在編譯期確定,那麼就應該使用類型查詢方法來探知
● 盡量使用類型資訊查詢方法來確定物件類型,而不要直接比較類對象,因為某些對象可能實現了訊息轉寄功能。