《Effective Objective-C 2.0》—(第11-14條)—運行時動態綁定、objc_msgSend、訊息轉寄機制

來源:互聯網
上載者:User

標籤:運行時   訊息轉寄   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對象則構成了類的繼承體系。

● 如果物件類型無法在編譯期確定,那麼就應該使用類型查詢方法來探知

● 盡量使用類型資訊查詢方法來確定物件類型,而不要直接比較類對象,因為某些對象可能實現了訊息轉寄功能。


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.