Objective-C,通常寫作ObjC和較少用的Objective C或Obj-C,是擴充C的物件導向程式設計語言。所以有一定C/C++語言基礎理解和掌握Objective-C也會相應的快些。這回,我們將比較著學習Objective-C語言,掌握其文法並理解其思想。
文法
讓我們先來看看C++和Objective-C中對於類的宣言 :
C++
#include "BaseClass.h"
class MyClass : public BaseClass
{
public:
MyClass();
virtual ~MyClass();
virtual int GetValue() const;
virtual void SetValue(int inValue);
bool IsValid() const;
static MyClass* GetInstance();
private:
int mValue;
static MyClass* sInstance;
};
Objective-C
#import "BaseClass.h"
@interface MyClass : BaseClass
{
int mValue;
}
- (int) getValue;
- (void) setValue: (int) inValue;
- (BOOL) isValid;
+ (MyClass*) getInstance;
@end
通過比較上面兩段代碼,從文法的角度上我們看到 Objective-C 語言有以下特點:
用 #import 取代了 #include
#import 相當於 C/C++ 語言中的 #include+#pragma once。當標頭檔嵌套包含的時候,它的作用就發揮出來了。
當某一標頭檔已經被讀取後,又一次被 #include 的時候,#pragma once 這會跳過該次讀取。
比如我們在C/C++語言的標頭檔中常常這樣定義,就是為了實現 #pragma once 而做的 :1
2
3 #ifndef INCLUDED_BASECLASS_H
#include "BaseClass.h"
#endif
繼承的時候沒有限定符
繼承都是 public 的。沒有構建和虛構函數
成員變數/函數沒有限定符
成員變數預設是 private 的,而函數是 public 的。沒有const關鍵字
沒有virtual關鍵字
Objective-C 中函數預設的就是 virtual 的。接下來再看看具體的實現 :
C++
#include "MyClass.h"
#include <assert.h>
MyClass* MyClass::sInstance = 0;
MyClass::MyClass() : mValue(0)
{
}
MyClass::~MyClass()
{
mValue = -1;
}
int MyClass::GetValue() const
{
return (mValue);
}
void MyClass::SetValue(int inValue)
{
assert(IsValid());
mValue = inValue;
}
bool MyClass::IsValid() const
{
return (0 <= inValue && inValue <= 1000);
}
MyClass* MyClass::GetInstance()
{
if (sInstance == 0) {
sInstance = new MyClass();
}
return (sInstance);
}
Objective-C
#import "MyClass.h"
static MyClass* sInstance = 0;
@implementation MyClass
- (int) getValue
{
return (mValue);
}
- (void) setValue: (int) inValue
{
NSParameterAssert([self isValid]);
mValue = inValue;
}
- (BOOL) isValid
{
return (0 <= inValue && inValue <= 1000);
}
+ (MyClass*) getInstance
{
if (sInstance == 0) {
sInstance = [MyClass alloc];
}
return (sInstance);
}
@end
執行個體方法
方法前面的“-”是執行個體方法(類似於C++中的類成員函數)類方法
首碼為“+”的是類方法(類似於C++中的靜態成員函數,或者是全域函數)類變數
與C/C++語言中的靜態變數一樣,Objective-C 中的類變數就是以 static 聲明的變數。(只在當前定義檔案中有效)
如果子類也想參照父類中的類變數的時候,須定義屬性參照方法(類方法)。(這與物件導向中的封裝概念有所背馳,降低了凝聚度)單一繼承
Objective-C 與 Java 語言一樣,都是單一繼承。
如果想實現多重繼承,可以只用類似Java 中 implements 的方法。(Objective-C 中叫做 protocol)發送訊息
Objective-C 中類似於C/C++中函數調用的地方都被稱作“發送訊息”。調用某個函數,被稱為發送了某個訊息。其形式如所示 :
Objective-C的發送訊息
方法,SEL,方法實現
Objective-C 中方法,SEL型,實現的關係如如所示 :
Objective-C的方法
概念
SEL,IMP的定義
接下來,我們來看看 Objective-C 語言中的標頭檔 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的一個子類。
Objective-C的Object
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
/* 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",
...
};
可以看到,數組的大小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;
struct __objc_sel_set {
uint32_t _count;
uint32_t _capacity;
uint32_t _bucketsNum;
SEL *_buckets;
};
IMP
從上面的標頭檔中我們可以看到,IMP定義為
1 id (*IMP) (id, SEL, …)。
這樣說來,IMP是一個指向函數的指標,這個被指向的函數包括id(“self”指標),調用的SEL(方法名),再加上一些其他參數。說白了IMP就是實現方法。
我們取得了函數指標之後,也就意味著我們取得了執行的時候的這段方法的代碼的入口,這樣我們就可以像普通的C語言函數調用一樣使用這個函數指標。當然我們可以把函數指標作為參數傳遞到其他的方法,或者執行個體變數裡面,從而獲得極大的動態性。我們獲得了動態性,但是付出的代價就是編譯器不知道我們要執行哪一個方法所以在編譯的時候不會替我們找出錯誤,我們只有執行的時候才知道,我們寫的函數指標是否是正確的。所以,在使用函數指標的時候要非常準確地把握能夠出現的所有可能,並且做出預防。尤其是當你在寫一個供他人調用的介面API的時候,這一點非常重要。
方法的定義
在標頭檔 objc-class.h 中,有方法的定義 :
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; /* metaclass */
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; /* protocol鏈表 */
};
由以上的結構資訊,我們可以像類似於C語言中結構體操作一樣來使用成員。比如下面取得類的名稱:
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 經方法的傳回值傳送回去。
簡單地從上面發送訊息的過程可以看到,最終還是以函數指標的方式調用了函數。為什麼特意花那麼大的功夫繞個大圈子呢?1
這些年,隨著程式庫尺寸的擴大,動態連結程式庫的使用已經非常普遍。就是說,應用程式本身並不包括庫代碼,而是在啟動時或者運行過程中動態載入程式庫。這樣一來一方面可以減小程式大小,另一方面可以提升了代碼重用(不用再造輪子)。但是,隨之帶來了向下相容的問題。
如果程式庫反覆升級,添加新的方法的時候,開發人員與使用者間必須保持一致的版本,否則將產生執行階段錯誤。一般,解決這個問題是,調用新定義的方法的時候,實現檢查當前系統中是否存在新方法的實現。如果沒有,跳過它或者簡單地產生警告資訊。 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];
}
通過這樣的架構,利用 setTarget: 可以更該其他的目標,或者 setAction: 變換不同的Action。實現動態方法調用。
--------------------------------------------------------------------------------
1. C/C++語言中用回呼函數(callback)的概念來實現程式的動態語義,一般該回呼函數都是全域或靜態函數,使用Thunk的方法可以將類的成員函數作為回呼函數來使用—利用平台相關的技術將對象執行個體(this指標)傳遞給調用端。
作者:易飛揚