在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中摘錄的一段,定義了一些數據類型:
- // objc.h
- typedef struct objc_class *Class;
- typedef struct objc_object {
- Class isa;
- } *id;
- typedef struct objc_selector *SEL;
- typedef id (*IMP)(id, SEL, …);
- typedef signed char BOOL;
- #define YES (BOOL)1
- #define NO (BOOL)0
- #ifndef Nil
- #define Nil 0 /* id of Nil class */
- #endif
- #ifndef nil
- #define nil 0 /* id of Nil instance */
- #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方法,那么以下代碼:
- NSLog (@"SEL=%s", @selector(blah));
會輸出為 SEL=blah。
說白了SEL就是返回方法名。
這樣的機制大大的增加了我們的程序的靈活性,我們可以通過給一個方法傳遞SEL參數,讓這個方法動態的執行某一個方法;我們也可以通過配置文件指定需要執行的方法,程序讀取配置文件之后把方法的字符串翻譯成為SEL變量然后給相應的對象發送這個消息。
在 Objective-C 運行時庫中,selector 是作為數組來管理的。這都是從效率的角度出發:函數調用的時候,不是通過方法名字比較而是指針值的比較來查找方法,由於整數的查找和匹配比字符串要快得多,所以這樣可以在某種程度上提高執行的效率。
這樣就必須保證所有類中的 selector 須指向同一實體(數組)。一旦有新的類被定義,其中的 selector 也需要映射到這個數組中。
實際情況下,總共有兩種 selector 的數組:預先定義好的內置selector數組 和用於動態追加的selector數組 。
- 內置selector
- #define NUM_BUILTIN_SELS 16371
- /* base-2 log of greatest power of 2 < NUM_BUILTIN_SELS */
- #define LG_NUM_BUILTIN_SELS 13
- static const char * const _objc_builtin_selectors[NUM_BUILTIN_SELS] = {
- ".cxx_construct",
- ".cxx_destruct",
- "CGColorSpace",
- "CGCompositeOperationInContext:",
- "CIContext",
- "CI_affineTransform",
- "CI_arrayWithAffineTransform:",
- "CI_copyWithZone:map:",
- "CI_initWithAffineTransform:",
- "CI_initWithRect:",
- "CI_rect",
- "CTM",
- "DOMDocument",
- "DTD",
- ...
- };
- 動態追加selector
另一個用於動態追加的 selector,其定義在 objc-sel.m 和 objc-sel-set.m 文件中 新的 selector 都被追加到 _buckets 成員中,其中追加和搜索使用 Hash 算法。
- static struct __objc_sel_set *_objc_selectors = NULL;
- struct __objc_sel_set {
- uint32_t _count;
- uint32_t _capacity;
- uint32_t _bucketsNum;
- SEL *_buckets;
- };
IMP
從上面的頭文件中我們可以看到,IMP定義為
- id (*IMP) (id, SEL, …)
這樣說來, IMP是一個指向函數的指針,這個被指向的函數包括id(“self”指針),調用的SEL(方法名),再加上一些其他參數。
說白了IMP就是實現方法。
我們取得了函數指針之后,也就意味着我們取得了執行的時候的這段方法的代碼的入口,這樣我們就可以像普通的 C語言函數調用一樣使用這個函數指針。當然我們可以把函數指針作為參數傳遞到其他的方法,或者實例變量里面,從而獲得極大的動態性。我們獲得了動態性,但 是付出的代價就是編譯器不知道我們要執行哪一個方法所以在編譯的時候不會替我們找出錯誤,我們只有執行的時候才知道,我們寫的函數指針是否是正確的。所 以,在使用函數指針的時候要非常准確地把握能夠出現的所有可能,並且做出預防。尤其是當你在寫一個供他人調用的接口API的時候,這一點非常重要。
Method
在objc/objc-class.h中定義了叫做Method的類型,是這樣定義的:
- typedef struct objc_method *Method;
- struct objc_method {
- SEL method_name;
- char *method_types;
- IMP method_imp;
- };
這個定義看上去包括了我們上面說過的其他類型。也就是說,Method(我們常說的方法)表示一種類型,這種類型與selector和實現(implementation)相關。
最初的SEL是方法的名稱method_name。char型的method_types表示方法的參數。最后的IMP就是實際的函數指針,指向函數的實現。
Class
從上文的定義看,Class(類)被定義為一個指向struct objc_class的指針,在objc/objc-class.h中它是這么定義的:
- struct objc_class {
- struct objc_class *isa;
- 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 cls;
- cls = [NSString class];
- 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 命令來查看。
- int main (int argc, const char * argv[])
- {
- NSString* string;
- int length;
- string = [[NSString alloc] initWithString:@"Objective-C"];
- length = [string length];
- return 0;
- }
這里調用了 alloc、initWithString:、length 等方法。
- % nm Test
- U .objc_class_name_NSString
- 00003000 D _NXArgc
- 00003004 D _NXArgv
- U ___CFConstantStringClassReference
- 00002b98 T ___darwin_gcc3_preregister_frame_info
- U ___keymgr_dwarf2_register_sections
- U ___keymgr_global
- 0000300c D ___progname
- 000025ec t __call_mod_init_funcs
- 000026ec t __call_objcInit
- U __cthread_init_routine
- 00002900 t __dyld_func_lookup
- 000028a8 t __dyld_init_check
- U __dyld_register_func_for_add_image
- U __dyld_register_func_for_remove_image
- ...
可以看到,這里沒有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,像是下面的代碼:
- // 目標對象
- id target;
- // 具體Action的 selector
- SEL action;
- ...
- // 確認目標是否實現Action
- if ([target respondsToSelector:actioin]) {
- // 調用具體Action
- [target performSelector:action withObject:self];
- }
類型 |
常量實例 |
NSlog字符 |
Char |
‘a’,’/n’ |
%c |
Short int |
-- |
%hi,%hx,%ho |
Unsigned short int |
-- |
%hu,%hx,%ho |
Int |
12,-97,0xFFE0,0177 |
%i,%x,%o |
Unsigned int |
12u,100U,0xFFu |
%u,%x,%o |
Long int |
12L,-200l,0xffffL |
%li,%lx,%lo |
Unsigned long int |
12UL,100ul,0xffeeUL |
%lu,%lx,%lo |
Long long int |
0xe5e5c5e5LL,500ll |
%lli,%llx,%llo |
Unsigned long long int |
12ull,0xffeeULL |
%llu,%llx,%llo |
Float |
12.34f,3.1e-5f, |
%f,%e,%g,%a |
Double |
12.34,3.1e-5,0x.1p3 |
%f,%e,%g,%a |
Long double |
12.34l,3.1e-5l |
%Lf,%Le,%Lg |
id |
nil |
%p |
NSLog的格式如下所示:
- %@ 對象
- %d, %i 整數
- %u 無符整形
- %f 浮點/雙字
- %x, %X 二進制整數
- %o 八進制整數
- %zu size_t
- %p 指針
- %e 浮點/雙字 (科學計算)
- %g 浮點/雙字
- %s C 字符串
- %.*s Pascal字符串
- %c 字符
- %C unichar
- %lld 64位長整數(long long)
- %llu 無符64位長整數
- %Lf 64位雙字