[iOS]深入淺出ObjC之訊息

來源:互聯網
上載者:User

深入淺出ObjC之訊息  

羅朝輝(http://blog.csdn.net/kesalin

轉載請註明出處

在入門層級的ObjC 教程中,我們常對從C++或Java 或其他物件導向語言轉過來的程式員說,ObjC 中的方法調用(ObjC中的術語為訊息)跟其他語言中的方法調用差不多,只是形式有些不同而已。

譬如C++ 中的:

Bird * aBird = new Bird();

aBird->fly();

在ObjC 中則如下:

Bird * aBird = [[Bird alloc] init];

[aBird fly];

初看起來,好像只是書寫形式不同而已,實則差異大矣。C++中的方法調用可能是動態,也可能是靜態;而ObjC中的訊息都為動態。下文將詳細介紹為什麼是動態,以及編譯器在這背後做了些什麼事情。

要說清楚訊息這個話題,我們必須先來瞭解三個概念Class, SEL, IMP,它們在objc/objc.h 中定義:

typedef struct objc_class *Class;

typedef struct objc_object {

    Class isa;

} *id;

typedef struct objc_selector   *SEL;  

typedef id (*IMP)(id, SEL, ...);

Class 的含義

Class 被定義為一個指向 objc_class的結構體指標,這個結構體表示每一個類的類結構。而 objc_class 在objc/objc_class.h中定義如下:

struct objc_class {

    struct objc_class super_class;  /*父類*/

    const char *name;                 /*類名字*/

    long version;                   /*版本資訊*/

    long info;                        /*類資訊*/

    long instance_size;               /*執行個體大小*/

    struct objc_ivar_list *ivars;     /*執行個體參數鏈表*/

    struct objc_method_list **methodLists; /*方法鏈表*/

    struct objc_cache *cache;               /*方法緩衝*/

    struct objc_protocol_list *protocols;   /*協議鏈表*/

};

由此可見,Class 是指向類結構體的指標,該類結構體含有一個指向其父類類結構的指標,該類方法的鏈表,該類方法的緩衝以及其他必要資訊。

NSObject 的class 方法就返回這樣一個指向其類結構的指標。每一個類執行個體對象的第一個執行個體變數是一個指向該對象的類結構的指標,叫做isa。通過該指標,對象可以訪問它對應的類以及相應的父類。一所示:

一所示,圓形所代表的執行個體對象的第一個執行個體變數為 isa,它指向該類的類結構 The object’s class。而該類結構有一個指向其父類類結構的指標superclass, 以及自身訊息名稱(selector)/實現地址(address)的方法鏈表。

方法的含義:

注意這裡所說的方法鏈表裡面儲存的是Method 類型的。圖一中selector 就是指 Method的 SEL,  address就是指Method的 IMP。 Method 在標頭檔 objc_class.h中定義如下:

typedef struct objc_method *Method;

typedef struct objc_ method {

    SEL method_name;

    char *method_types;

    IMP method_imp;

};

一個方法 Method,其包含一個方法選標 SEL – 表示該方法的名稱,一個types – 表示該方法的參數,一個 IMP  - 指向該方法的具體實現的函數指標。

SEL 的含義:

在前面我們看到方法選標 SEL 的定義為:

typedef struct objc_selector   *SEL;  

它是一個指向 objc_selector 指標,表示方法的名字。如下所示,列印出 selector。

-(NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b

{

    return (a > b) ? a : b;

}

NSLog(@"SEL=%s", @selector(maxIn:theOther:));

輸出:SEL=maxIn:theOther:

不 同的類可以擁有相同的 selector,這個沒有問題,因為不同類的執行個體對象performSelector相同的 selector 時,會在各自的訊息選標(selector)/實現地址(address) 方法鏈表中根據 selector 去尋找具體的方法實現IMP, 然後用這個方法實現去執行具體的實現代碼。這是一個動態綁定的過程,在編譯的時候,我們不知道最終會執行哪一些代碼,只有在執行的時候,通過selector去查詢,我們才能確定具體的執行代碼。

IMP 的含義:

在前面我們也看到 IMP 的定義為:

typedef id (*IMP)(id, SEL, ...);

根據前面id 的定義,我們知道 id是一個指向 objc_object 結構體的指標,該結構體只有一個成員isa,所以任何繼承自 NSObject 的類對象都可以用id 來指代,因為 NSObject 的第一個成員執行個體就是isa。

至 此,我們就很清楚地知道 IMP  的含義:IMP 是一個函數指標,這個被指向的函數包含一個接收訊息的對象id(self  指標), 調用方法的選標 SEL (方法名),以及不定個數的方法參數,並返回一個id。也就是說 IMP 是訊息最終調用的執行代碼,是方法真正的實現代碼  。我們可以像在C語言裡面一樣使用這個函數指標。

NSObject 類中的methodForSelector:方法就是這樣一個擷取指向方法實現IMP 的指標,methodForSelector:返回的指標和賦值的變數類型必須完全一致,包括方法的參數類型和傳回值類型。

下面的例子展示了怎麼使用指標來調用setFilled:的方法實現:

void (*setter)(id, SEL, BOOL);

int i;

setter = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];

for (i = 0; i < 1000; i++)

    setter(targetList[i], @selector(setFilled:), YES);

使用methodForSelector:來避免動態綁定將減少大部分訊息的開銷,但是這隻有在指定的訊息被重複發送很多次時才有意義,例如上面的for迴圈。

注意,methodForSelector:是Cocoa運行時系統的提供的功能,而不是Objective-C語言本身的功能。

訊息調用過程:

至此我們對ObjC 中的訊息應該有個大致思路了:樣本

Bird * aBird = [[Bird alloc] init];

[aBird fly];

中對 fly 的調用,編譯器通過插入一些代碼,將之轉換為對方法具體實現IMP的調用,這個 IMP是通過在 Bird 的類結構中的方法鏈表中尋找名稱為fly 的 選標SEL 對應的具體方法實現找到的。

上面的思路還有一些沒有提及的話題,比如說編譯器插入了什麼代碼,如果在方法鏈表中沒有找到對應的 IMP又會如何,這些話題在下面展開。

訊息函數 obj_msgSend:

編譯器會將訊息轉換為對訊息函數 objc_msgSend的調用,該函數有兩個主要的參數:訊息接收者id 和訊息對應的方法選標 SEL, 同時接收訊息中的任意參數:

id objc_msgSend(id theReceiver, SELtheSelector, ...)

如上面的訊息 [aBird fly]會被轉換為如下形式的函數調用:

objc_msgSend(aBird, @selector(fly));

該訊息函數做了動態綁定所需要的一切工作:
1,它首先找到 SEL 對應的方法實現 IMP。因為不同的類對同一方法可能會有不同的實現,所以找到的方法實現依賴於訊息接收者的類型。
2, 然後將訊息接收者對象(指向訊息接收者對象的指標)以及方法中指定的參數傳遞給方法實現 IMP。
3, 最後,將方法實現的傳回值作為該函數的傳回值返回。

編 譯器會自動插入調用該訊息函數objc_msgSend的代碼,我們無須在代碼中顯示調用該訊息函數。當objc_msgSend找到方法對應的實現時, 它將直接調用該方法實現,並將訊息中所有的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:訊息的接收者以及方法名稱 SEL。這些參數協助方法實現獲得了訊息運算式的資訊。它們被認為是”隱藏“的是因為它們並沒有在定義方法的原始碼中聲明,而是在代碼編譯時間是插入方法的 實現中的。

儘管這些參數沒有被顯示聲明,但在原始碼中仍然可以引用它們(就象可以引用訊息接收者對象的執行個體變數一樣)。在方法中可以通過self來引用訊息接收者對象,通過選標_cmd來引用方法本身。在下面的例子中,_cmd 指的是strange方法,self指的收到strange訊息的對象。

- strange

{

    id target = getTheReceiver();

    SEL method = getTheMethod();

    if (target == self || mothod == _cmd)

        return nil;

    return [target performSelector:method];

}

在這兩個參數中,self更有用一些。實際上,它是在方法實現中訪問訊息接收者對象的執行個體變數的途徑。

尋找 IMP 的過程:

前 面說了,objc_msgSend 會根據方法選標 SEL 在類結構的方法列表中尋找方法實現IMP。這裡頭有一些文章,我們在前面的類結構中也看到有一個叫objc_cache *cache 的成員,這個緩衝為提高效率而存在的。每個類都有一個獨立的緩衝,同時包括繼承的方法和在該類中定義的方法。。

尋找IMP 時:

1,首先去該類的方法 cache 中尋找,如果找到了就返回它;

2, 如果沒有找到,就去該類的方法列表中尋找。如果在該類的方法列表中找到了,則將 IMP 返回,並將它加入cache中緩衝起來。根據最近使用原則,這個方法再次調用的可能性很大,緩衝起來可以節省下次調用再次尋找的開銷。3,3,如果在該類 的方法列表中沒找到對應的 IMP,在通過該類結構中的 super_class指標在其父類結構的方法列表中去尋找,直到在某個父類的方法列表中找到對應的IMP,返回它,並加入cache中。

4,如果在自身以及所有父類的方法列表中都沒有找到對應的 IMP,則進入下文中要講的訊息轉寄流程。

便利函數:

我們可以通過NSObject的一些方法擷取運行時資訊或動態執行一些訊息:

class   返回對象的類;

isKindOfClass 和 isMemberOfClass檢查對象是否在指定的類繼承體系中;

respondsToSelector 檢查對象能否相應指定的訊息;

conformsToProtocol 檢查對象是否實現了指定協議類的方法;

methodForSelector  返回指定方法實現的地址。

performSelector:withObject 執行SEL 所指代的方法。

訊息轉寄:

通 常,給一個對象發送它不能處理的訊息會得到出錯提示,然而,Objective-C運行時系統在拋出錯誤之前,會給訊息接收對象發送一條特別的訊息forwardInvocation 來通知該對象,該訊息的唯一參數是個NSInvocation類型的對象——該對象封裝了原始的訊息和訊息的參數。

我們可以實現forwardInvocation:方法來對不能處理的訊息做一些預設的處理,也可以將訊息轉寄給其他對象來處理,而不拋出錯誤。

關於訊息轉寄的作用,可以考慮如下情景:假設,我們需要設計一個能夠響應negotiate訊息的對象,並且能夠包括其它類型的對象對訊息的響應。 通過在negotiate方法的實現中將negotiate訊息轉寄給其它的對象來很容易的達到這一目的。

更進一步,假設我們希望我們的對象和另外一個類的對象對negotiate的訊息的響應完全一致。一種可能的方式就是讓我們的類繼承其它類的方法實現。 然後,有時候這種方式不可行,因為我們的類和其它類可能需要在不同的繼承體系中響應negotiate訊息。

雖然我們的類無法繼承其它類的negotiate方法,但我們仍然可以提供一個方法實現,這個方法實現只是簡單的將negotiate訊息轉寄給其他類的對象,就好像從其它類那兒“借”來的現一樣。如下所示:

- negotiate

{

    if ([someOtherObject respondsToSelector:@selector(negotiate)])

        return [someOtherObject negotiate];

    return self;

}

這 種方式顯得有欠靈活,特別是有很多訊息都希望傳遞給其它對象時,我們就必須為每一種訊息提供方法實現。此外,這種方式不能處理未知的訊息。當我們寫下代碼 時,所有我們需要轉寄的訊息的集合都必須確定。然而,實際上,這個集合會隨著運行時事件的發生,新方法或者新類的定義而變化。

forwardInvocation:訊息給這個問題提供了一個更特別的,動態解決方案:當一個對象由於沒有相應的方法實現而無法響應某訊息時,運行時系統將通過forwardInvocation:訊息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然 而,NSObject中的方法實現只是簡單地調用了doesNotRecognizeSelector:。通過實現我們自己的forwardInvocation:方法,我們可以在該方法實現中將訊息轉寄給其它對象。

要轉寄訊息給其它對象,forwardInvocation:方法所必須做的有:
1,決定將訊息轉寄給誰,並且
2,將訊息和原來的參數一塊轉寄出去。

訊息可以通過invokeWithTarget:方法來轉寄:

- (void) forwardInvocation:(NSInvocation *)anInvocation

{

    if ([someOtherObject respondsToSelector:[anInvocation selector]])

        [anInvocation invokeWithTarget:someOtherObject];

    else

        [super forwardInvocation:anInvocation];

}

轉寄訊息後的傳回值將返回給原來的訊息寄件者。您可以將返回任何類型的傳回值,包括: id,結構體,浮點數等。

forwardInvocation:方法就像一個不能識別的訊息的分發中心,將這些訊息轉寄給不同接收對象。或者它也可以象一個運輸站將所有的訊息都發送給同一個接收對象。它可以將一個訊息 翻譯成另外一個訊息,或者簡單的"吃掉“某些訊息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的訊息提供同樣的響 應,這一切都取決於方法的具體實現。該方法所提供是將不同的對象連結到訊息鏈的能力。

注意: forwardInvocation:方法只有在訊息接收對象中無法正常響應訊息時才會被調用。  所以,如果我們希望一個對象將negotiate訊息轉寄給其它對象,則這個對象不能有negotiate方法。否 則,forwardInvocation:將不可能會被調用。

參考資料:

Objective-CRuntime Reference:

http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html

Objective-C Runtime Programming Guide:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.