Objective-C-- id、nil、Nil、SEL、IMP、Method、Class

在Objective-C中,有一些我們之前並不熟悉但是經常見到的數據類型,比如id、nil、Nil、SEL等等。在很多文章裏,我們都見過這些數據類型的介紹,但是都沒有說的太清楚。


這篇文章從最底層的定義開始,介紹一下這些類型到底是怎麼定義的,這會幫助我們更加深入地瞭解Objective-C。

參考:
http://unixjunkie.blogspot.com/2006/02/nil-and-nil.html

http://blog.csdn.net/itudou_2010/article/details/5501840 

Objective-C中有一些很有趣的數據類型經常會被錯誤地理解。他們中的大多數都可以在/usr/include/objc/objc.h或者這個目錄中的其他頭文件中找到。下面是從objc.h中摘錄的一段,定義了一些數據類型:

  1. // objc.h  
  2.   
  3. typedef struct objc_class *Class;  
  4.   
  5. typedef struct objc_object {  
  6.     Class isa;  
  7. } *id;  
  8.   
  9. typedef struct objc_selector  *SEL;  
  10. typedef id      (*IMP)(id, SEL, …);  
  11. typedef signed char   BOOL;  
  12.   
  13. #define YES             (BOOL)1  
  14. #define NO              (BOOL)0  
  15.   
  16. #ifndef Nil  
  17. #define Nil 0   /* id of Nil class */  
  18. #endif  
  19.   
  20. #ifndef nil  
  21. #define nil 0   /* id of Nil instance */  
  22. #endif  

 

我們在這裏解釋一下它們的細節:


id
id和void *並非完全一樣。在上面的代碼中,id是指向struct objc_object的一個指針,這個意思基本上是說,id是一個指向任何一個繼承了Object(或者NSObject)類的對象。需要注意的是id 是一個指針,所以你在使用id的時候不需要加星號。比如id foo=nil定義了一個nil指針,這個指針指向NSObject的一個任意子類。而id *foo=nil則定義了一個指針,這個指針指向另一個指針,被指向的這個指針指向NSObject的一個子類。

nil 
nil和C語言的NULL相同,在objc/objc.h中定義。nil表示一個Objctive-C對象,這個對象的指針指向空(沒有東西就是空)。

Nil 
首字母大寫的Nil和nil有一點不一樣,Nil定義一個指向空的類(是Class,而不是對象)。

SEL 
這個很有趣。SEL是“selector”的一個類型,表示一個方法的名字。比如以下方法:

-[Foo count] 和 -[Bar count] 使用同一個selector,它們的selector叫做count。

在上面的頭文件裏我們看到,SEL是指向 struct objc_selector的指針,但是objc_selector是什麼呢?那麼實際上,你使用GNU Objective-C的運行時間庫和NeXT Objective-C的運行運行時間庫(Mac OS X使用NeXT的運行時間庫)時,它們的定義是不一樣的。實際上Mac OSX僅僅將SEL映射爲C字符串。比如,我們定義一個Foo的類,這個類帶有一個- (int) blah方法,那麼以下代碼:

  1. NSLog (@"SEL=%s", @selector(blah));  

 

會輸出爲 SEL=blah。

說白了SEL就是返回方法名。

 

這樣的機制大大的增加了我們的程序的靈活性,我們可以通過給一個方法傳遞SEL參數,讓這個方法動態的執行某一個方法;我們也可以通過配置文件指定需要執行的方法,程序讀取配置文件之後把方法的字符串翻譯成爲SEL變量然後給相應的對象發送這個消息。

 

在 Objective-C 運行時庫中,selector 是作爲數組來管理的。這都是從效率的角度出發:函數調用的時候,不是通過方法名字比較而是指針值的比較來查找方法,由於整數的查找和匹配比字符串要快得多,所以這樣可以在某種程度上提高執行的效率。

 

這樣就必須保證所有類中的 selector 須指向同一實體(數組)。一旦有新的類被定義,其中的 selector 也需要映射到這個數組中。

 

實際情況下,總共有兩種 selector 的數組:預先定義好的內置selector數組 和用於動態追加的selector數組 。

  • 內置selector
簡單地說,內置的selector就是一個大的字符串數組。定義在objc-sel-table.h文件中: 
  1. #define NUM_BUILTIN_SELS 16371  
  2. /* base-2 log of greatest power of 2 < NUM_BUILTIN_SELS */  
  3. #define LG_NUM_BUILTIN_SELS 13  
  4.   
  5. static const char * const _objc_builtin_selectors[NUM_BUILTIN_SELS] = {  
  6.     ".cxx_construct",  
  7.     ".cxx_destruct",  
  8.     "CGColorSpace",  
  9.     "CGCompositeOperationInContext:",  
  10.     "CIContext",  
  11.     "CI_affineTransform",  
  12.     "CI_arrayWithAffineTransform:",  
  13.     "CI_copyWithZone:map:",  
  14.     "CI_initWithAffineTransform:",  
  15.     "CI_initWithRect:",  
  16.     "CI_rect",  
  17.     "CTM",  
  18.     "DOMDocument",  
  19.     "DTD",  
  20.     ...  
  21.   
  22. };  
可以看到,數組的大小NUM_BUILTIN_SELS定義爲16371。字符串按照字母順序排序,簡單的都是爲了運行時檢索的速度(二分法查找)。
從定義好的 selector 名稱我們可以看到一些新的方法名稱,比如 CIConetext,CI開頭的方法是由Tiger開始導入的程序庫。
每次系統更新的時候,這個數組也是需要更新的。
  • 動態追加selector

另一個用於動態追加的 selector,其定義在 objc-sel.m 和 objc-sel-set.m  文件中 新的 selector 都被追加到 _buckets 成員中,其中追加和搜索使用 Hash 算法。

  1. static struct __objc_sel_set *_objc_selectors = NULL;  
  2.   
  3. struct __objc_sel_set {  
  4.     uint32_t _count;  
  5.     uint32_t _capacity;  
  6.     uint32_t _bucketsNum;  
  7.     SEL *_buckets;  
  8. };  

 

IMP 
從上面的頭文件中我們可以看到,IMP定義爲

  1. id (*IMP) (id, SEL, …)  

 

這樣說來, IMP是一個指向函數的指針,這個被指向的函數包括id(“self”指針),調用的SEL(方法名),再加上一些其他參數。

說白了IMP就是實現方法。

 

我們取得了函數指針之後,也就意味着我們取得了執行的時候的這段方法的代碼的入口,這樣我們就可以像普通的 C語言函數調用一樣使用這個函數指針。當然我們可以把函數指針作爲參數傳遞到其他的方法,或者實例變量裏面,從而獲得極大的動態性。我們獲得了動態性,但 是付出的代價就是編譯器不知道我們要執行哪一個方法所以在編譯的時候不會替我們找出錯誤,我們只有執行的時候才知道,我們寫的函數指針是否是正確的。所 以,在使用函數指針的時候要非常準確地把握能夠出現的所有可能,並且做出預防。尤其是當你在寫一個供他人調用的接口API的時候,這一點非常重要。

 

Method 
在objc/objc-class.h中定義了叫做Method的類型,是這樣定義的:

  1. typedef struct objc_method *Method;  
  2. struct objc_method {  
  3.     SEL method_name;  
  4.     char *method_types;  
  5.     IMP method_imp;  
  6. };  

 

這個定義看上去包括了我們上面說過的其他類型。也就是說,Method(我們常說的方法)表示一種類型,這種類型與selector和實現(implementation)相關。

 

最初的SEL是方法的名稱method_name。char型的method_types表示方法的參數。最後的IMP就是實際的函數指針,指向函數的實現。

 

Class 
從上文的定義看,Class(類)被定義爲一個指向struct objc_class的指針,在objc/objc-class.h中它是這麼定義的:

  1. struct objc_class {  
  2.     struct objc_class *isa;  
  3.     struct objc_class *super_class;  
  4.     const char *name;  
  5.     long version;  
  6.     long info;  
  7.     long instance_size;  
  8.     struct objc_ivar_list *ivars;  
  9.     struct objc_method_list **methodLists;  
  10.     struct objc_cache *cache;  
  11.     struct objc_protocol_list *protocols;  
  12. };  

 

由以上的結構信息,我們可以像類似於C語言中結構體操作一樣來使用成員。比如下面取得類的名稱:
  1. Class cls;  
  2. cls = [NSString class];  
  3.   
  4. printf("class name %s\n", ((struct objc_class*)cls)->name);  
 發送消息與函數調用的不同

Objective-C的消息傳送如下圖所示 :

 

 

 

Objective-C的消息傳送

 

發送消息的過程,可以總結爲以下內容 :

  • 首先,指定調用的方法
  • 爲了方法調用,取得 selector

源代碼被編譯以後,方法被解釋爲 selector。這裏的 selector 只是單純的字符串。

  • 消息發送給對象B

消息傳送使用到了 objc_msgSend 運行時API。這個API只是將 selector 傳遞給目標對象B。

  • 從 selector 取得實際的方法實現

首先,從對象B取得類的信息,查詢方法的實現是否被緩存(上面類定義中的struct objc_cache *cache;)。如果沒有被緩 存,則在方法鏈表中查詢(上面類定義中的struct objc_method_list **methodLists;)。

  • 執行

利用函數指針,調用方法的實現。這時,第一個參數是對象實例,第二個是 selector。

  • 傳送返回值

利用 objc_msgSend API 經方法的返回值傳送回去。

 

簡單地從上面發送消息的過程可以看到,最終還是以函數指針的方式調用了函數。爲什麼特意花那麼大的功夫繞個大圈子呢?

 

這些年,隨着程序庫尺寸的擴大,動態鏈接庫的使用已經非常普遍。就是說,應用程序本身並不包括庫代碼,而是在啓動時或者運行過程中動態加載程序庫。這樣一來一方面可以減小程序大小,另一方面可以提升了代碼重用(不用再造輪子)。但是,隨之帶來了向下兼容的問題。

 

如果程序庫反覆升級,添加新的方法的時候,開發者與用戶間必須保持一致的版本,否則將產生運行時錯誤。一般,解決這個問題是,調用新定義的方法的時 候,實現檢查當前系統中是否存在新方法的實現。如果沒有,跳過它或者簡單地產生警告信息。 Objective-C中的respondsToSelector:方法就可以用來實現這樣的動作。

 

但是,這並不是萬全的解決方案。如果應用程序與新的動態程序庫(含有新定義的API)一起編譯後,新定義的API符號也被包含進去。而這樣的應用程 序放到比較舊的系統(舊的動態程序庫)中運行的時候,因爲找不到鏈接符號,程序將不能啓動。這就是 win32系統中常見的「DLL地域」。

 

爲了解決這個問題,Objective-C 編譯得到的二進制文件中,函數是作爲 selector 來保存的。就是說,不管調用什麼函數,二進制文件中不會包含符號信息。爲了驗證 Objective-C 編譯的二進制文件是否包含符號信息,這裏用 nm 命令來查看。

  1. int main (int argc, const char * argv[])  
  2. {  
  3.     NSString*   string;  
  4.     int         length;  
  5.     string = [[NSString alloc] initWithString:@"Objective-C"];  
  6.     length = [string length];  
  7.   
  8.     return  0;  
  9. }  

 

這裏調用了 alloc、initWithString:、length 等方法。

  1. % nm Test  
  2.          U .objc_class_name_NSString  
  3. 00003000 D _NXArgc  
  4. 00003004 D _NXArgv  
  5.          U ___CFConstantStringClassReference  
  6. 00002b98 T ___darwin_gcc3_preregister_frame_info  
  7.          U ___keymgr_dwarf2_register_sections  
  8.          U ___keymgr_global  
  9. 0000300c D ___progname  
  10. 000025ec t __call_mod_init_funcs  
  11. 000026ec t __call_objcInit  
  12.          U __cthread_init_routine  
  13. 00002900 t __dyld_func_lookup  
  14. 000028a8 t __dyld_init_check  
  15.          U __dyld_register_func_for_add_image  
  16.          U __dyld_register_func_for_remove_image  
  17. ...  

 

可以看到,這裏沒有alloc、initWithString:、length3個方法的符號。所以,即使我們添加了新的方法,也可以在任何新舊系統中運 行。當然,函數調用之前,需要使用 respondsToSelector: 來確定方法是否存在。正是這樣的特性,使得程序可以運行時動態地查詢要執行的方法,提高了 Objective-C 語言的柔韌性。

 

Target-Action Paradigm

Objective-C 語言中,GUI控件對象間的通信利用 Target-Action Paradigm。不像其他事件驅動的 GUI 系統實現的那樣,需要以回調函數的形式註冊消息處理函數(Win32/MFC,Java AWT, X Window)。Target-Action Paradigm 完全是面向對象的事件傳遞機制。

 

例如用戶點擊菜單的事件,用Target-Action Paradigm來解釋就是,調用菜單中被設定目標的Action。這個Action對應的方法不一定需要實現。目標與Action的指定與方法的實現沒有關係,源代碼編譯的時候不會檢測,只是在運行時確認(參考前面消息傳送的機制)。

 

運行時,通過respondsToSelector: 方法來檢查實現的情況。如果有實現,那麼使用performSelector:withObject:來調用具體的Action,像是下面的代碼:

  1. // 目標對象  
  2. id target;  
  3. // 具體Action的 selector  
  4. SEL action;  
  5. ...  
  6.   
  7. // 確認目標是否實現Action  
  8. if ([target respondsToSelector:actioin]) {  
  9.     // 調用具體Action  
  10.     [target performSelector:action withObject:self];  
  11. }  

 

通過這樣的架構,利用 setTarget: 可以更該其他的目標,或者 setAction: 變換不同的Action。實現動態的方法調用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章