原文鏈接:http://www.cnblogs.com/yaski/
6,NSObject的奧祕
本系列講座有着很強的前後相關性,如果你是第一次閱讀本篇文章,爲了更好的理解本章內容,筆者建議你最好從本系列講座的第1章開始閱讀,請點擊這裏。
在上一章裏面,筆者向大家介紹了在Objective-C裏面的幾個非常重要的概念, 簡單的說就是SEL,Class和IMP。我們知道Objective-C是C語言的擴展,有了這3個概念還有我們以前講過的繼承和封裝的概念,Objective-C發生了翻天覆地的變化,既兼容C語言的高效特性又實現了面向對象的功能。
Objective-C從本質上來說,還是C語言的。那麼內部究竟是怎樣實現SEL,Class和IMP,還有封裝和繼承的?爲了解答這個問題,筆者決定在本章向大家概要的介紹一下Objective-C的最主要的一個類,NSObject。
不過說實在話,如果同學們覺得本章的內容比較晦澀難懂的話,不閱讀本章的內容絲毫不會對寫程序產生任何不良的影響,但是如果掌握了本章的內容的話,對加深對Objective-C的理解,對於今後筆者將要講述的內容而言,將會是一個極大的促進。
6.1,本章程序的執行結果
在本章裏面,我們將要繼續使用我們在前面幾章已經構築好的類Cattle和Bull。由於在現在的Xcode版本里面,把一些重要的東西比如說Class的原型定義都放到了LIB文件裏面,所以這些東西的具體的定義,對於我們來說是不可見的。
我們首先把第4章的代碼打開,然後打開“Cattle.h” 文件,把鼠標移動到“NSObject”上面,單擊鼠標右鍵,在彈出菜單裏面選擇“Jump to Definition”。然後會彈出一個小菜單,我們選擇“interface NSObject” 。我們可以看到如下代碼
Class isa;
我們知道了,所謂的NSObject裏面只有一個變量,就是Class類型的isa。isa的英文的意思就是is a pointer的意思。也就是說NSObject裏面只有一個實例變量isa。好的,我們需要知道Class是一個什麼東西,我們把鼠標移動到“Class”上面,單擊鼠標右鍵,在彈出菜單裏面選擇“Jump to Definition”,我們看到了如下的代碼:
typedef struct objc_object {
Class isa;
} *id;
...
我們在這裏知道了,Class實際上是一個objc_class的指針類型,我們把鼠標移動到“objc_class”上面,單擊鼠標右鍵,在彈出菜單裏面選擇“Jump to Definition”,發現我們還是在這個窗口裏面,Xcode並沒有把我們帶到objc_class的定義去,所以我們無從知道objc_class內部究竟是一個什麼樣的東西。
筆者順便提一下,大家也許注意到了id的定義,id實際上是objc_object結構的一個指針,裏面只有一個元素那就是Class。那麼根據上面我們看到的,所謂的id就是objc_class的指針的指針。讓我們回憶一下下面的代碼:
這句話是在初始化和實例話cattle對象,這個過程,實際上可以理解爲,runtime爲我們初始化好了Class的指針,並且把這個指針返回給我們。我們初始化對象完成了之後,實際上我們得到的對象就是一個指向這個對象的Class指針。
讓我們在回過頭來說說這個神祕的Class,我們無法在Xcode裏面看到Class也就是objc_class的定義。慶幸的是這部分的定義是GCC代碼,是開源的。筆者下載了開源的代碼之後,把開源的代碼作了一些小小的調整,然後把Class的定義等等放到了我們的工程文件裏面去,通過類型轉化之後,我們終於可以看到Class,SEL,還有isa等等過去對我們來說比較“神祕”的東西的真正面目。
我們在前面幾章裏面在每一個章的第一節裏面都要介紹一下本章程序執行結果的屏幕拷貝,本章也是一樣,但是本章的執行結果非常簡單。因爲對於本章而言重點應該是放在對NSObject機制的理解上。
圖6-1,本章程序運行結果
大家看到本章程序的運行結果的屏幕拷貝的時候,也許會覺得很無趣,因爲單單從結果畫面,我們沒有發現任何令人感到很有興趣的東西,相反,都是同學們已經很熟悉的一些老面孔。但是本章所要講述的東西也許是同學們在其他語言裏面從來沒有遇到過的東西,這些東西將會令人感到新鮮和激動。
6.2,實現步驟
第一步,按照我們在第2章所述的方法,新建一個項目,項目的名字叫做06-NSObject。如果你是第一次看本篇文章,請到這裏參看第二章的內容。
第二步,按照我們在第4章的4.2節的第二,三,四步所述的方法,把在第4章已經使用過的“Cattle.h”,“Cattle.m”,“Bull.h”還有“Bull.m” 導入本章的項目裏面。如果你沒有第4章的代碼,請到這裏下載。如果你沒有閱讀第4章的內容,請參看這裏。
第三步,把鼠標移動到項目瀏覽器上面的“Source”上面,然後在彈出的菜單上面選擇“Add”,然後在子菜單裏面選擇“New File”,然後在新建文件對話框的左側最下面選擇“Other”,然後在右側窗口選擇“Empty File”,選擇“Next”,在“New File”對話框裏面的“File Name”欄內輸入“MyNSObject.h”。然後輸入(或者是拷貝也可以,因爲這是C的代碼,如果你很熟悉C語言的話,可以拷貝一下節省時間)如下代碼:
typedef const struct objc_selector
{
void *sel_id;
const char *sel_types;
} *MySEL;
typedef struct my_objc_object {
struct my_objc_class* class_pointer;
} *myId;
typedef myId (*MyIMP)(myId, MySEL, );
typedef char *STR; /* String alias */
typedef struct my_objc_class *MetaClass;
typedef struct my_objc_class *MyClass;
struct my_objc_class {
MetaClass class_pointer;
struct my_objc_class* super_class;
const char* name;
long version;
unsigned long info;
long instance_size;
struct objc_ivar_list* ivars;
struct objc_method_list* methods;
struct sarray * dtable;
struct my_objc_class* subclass_list;
struct my_objc_class* sibling_class;
struct objc_protocol_list *protocols;
void* gc_object_type;
};
typedef struct objc_protocol {
struct my_objc_class* class_pointer;
char *protocol_name;
struct objc_protocol_list *protocol_list;
struct objc_method_description_list *instance_methods, *class_methods;
} Protocol;
typedef void* retval_t;
typedef void(*apply_t)(void);
typedef union arglist {
char *arg_ptr;
char arg_regs[sizeof (char*)];
} *arglist_t;
typedef struct objc_ivar* Ivar_t;
typedef struct objc_ivar_list {
int ivar_count;
struct objc_ivar {
const char* ivar_name;
const char* ivar_type;
int ivar_offset;
} ivar_list[1];
} IvarList, *IvarList_t;
typedef struct objc_method {
MySEL method_name;
const char* method_types;
MyIMP method_imp;
} Method, *Method_t;
typedef struct objc_method_list {
struct objc_method_list* method_next;
int method_count;
Method method_list[1];
} MethodList, *MethodList_t;
struct objc_protocol_list {
struct objc_protocol_list *next;
size_t count;
Protocol *list[1];
};
第四步,打開06-NSObject.m文件,輸入如下代碼並且保存
#import "Cattle.h"
#import "Bull.h"
#import "MyNSObject.h"
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id cattle = [Cattle new];
id redBull = [Bull new];
SEL setLegsCount_SEL = @selector(setLegsCount:);
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
[cattle setLegsCount:4];
[redBull setLegsCount:4];
[redBull setSkinColor:@"red"];
Class cattle_class = cattle->isa;
MyClass my_cattle_class = cattle->isa;
SEL say = @selector(saySomething);
IMP cattle_sayFunc = [cattle methodForSelector:say];
cattle_sayFunc(cattle, say);
Class redBull_class = redBull->isa;
MyClass my_redBull_class = redBull->isa;
IMP redBull_sayFunc = [redBull methodForSelector:say];
redBull_sayFunc(redBull, say);
[pool drain];
return 0;
}
第五步,在06-NSObject.m文件的窗口的“[pool drain];”代碼的左側單擊一下窗口的邊框,確認一下是否出現一個藍色的小棒棒,如果有的話那麼斷點被選擇好了。如圖6-2所示
圖6-2,選擇執行斷點
第六步,選擇Xcode上面的菜單的“Run”,然後選擇“Debuger” ,在Debuger窗口裏面選擇“Build and Go”。
好的,大家就停在這裏,不要做其他的操作,我們把程序中斷在程序幾乎執行到最後的斷點上,我們將要通過Debuger來看看Objective-C內部究竟發生了什麼樣的奇妙的魔法。
6.3,超類方法的調用
我們現在打開“06-NSObject.m”文件,發現下面的代碼:
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
這一段代碼,對同學們來說不是什麼新鮮的內容了,我們在第5章裏面已經講過,這個是SEL和IMP的概念。我們在這裏取得了cattle對象和redBull對象的setLegsCount:的函數指針。
如果大家現在已經不在Debuger裏面的話,那麼請選擇Xcode菜單裏面的,“Run”然後選擇“Debuger” 。
我們注意到在Debuger裏面,cattle_setLegsCount_IMP的地址和redBull_setLegsCount_IMP是完全一樣的,如圖6-3所示:
圖6-3,cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址。
cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址完全一樣,說明他們使用的是相同的代碼段。這種結果是怎樣產生的呢?大家請打開“MyNSObject.h”,參照下列代碼:
MetaClass class_pointer;
struct my_objc_class* super_class;
const char* name;
long version;
unsigned long info;
long instance_size;
struct objc_ivar_list* ivars;
struct objc_method_list* methods;
struct sarray * dtable;
struct my_objc_class* subclass_list;
struct my_objc_class* sibling_class;
struct objc_protocol_list *protocols;
void* gc_object_type;
};
筆者在這裏把開源代碼的名字的定義加上了“my_”前綴,僅僅是爲了區分一下。“MyNSObject.h”裏面的代碼問題很多,筆者從來沒有也不會在實際的代碼裏面使用這段代碼,使用這些代碼的主要目的是爲了向大家講解概念,請大家忽略掉代碼裏面的種種問題。
我們注意到這裏的methods變量,裏面包存的就是類的方法名字(SEL)定義,方法的指針地址(IMP)。當我們執行
的時候,runtime會通過dtable這個數組,快速的查找到我們需要的函數指針,查找函數的定義如下:
objc_msg_lookup(id receiver, SEL op)
{
if(receiver)
return sarray_get(receiver->class_pointer->dtable, (sidx)op);
else
return nil_method;
好的,現在我們的cattle_setLegsCount_IMP沒有問題了,那麼redBull_setLegsCount_IMP怎麼辦?在Bull類裏面我們並沒有定義實例方法setLegsCount:,所以在Bull的Class裏面,runtime難道找不到setLegsCount:麼?答案是,是的runtime直接找不到,因爲我們在Bull類裏面根本就沒有定義setLegsCount:。
但是,從結果上來看很明顯runtime聰明的找到了setLegsCount:的地址,runtime是怎樣找到的?答案就在:
在自己的類裏面沒有找到的話,runtime會去Bull類的超類cattle裏面去尋找,慶幸的是它成功的在cattle類裏面runtime找到了setLegsCount:的執行地址入口,所以我們得到了redBull_setLegsCount_IMP。 redBull_setLegsCount_IMP和cattle_setLegsCount_IMP都是在Cattle類裏面定義的,所以他們的代碼的地址也是完全一樣的。
我們現在假設,如果runtime在cattle裏面也找不到setLegsCount:呢?沒有關係,cattle裏面也有超類的,那就是NSObject。所以runtime會去NSObject裏面尋找。當然,NSObject不會神奇到可以預測我們要定義setLegsCount:所以runtime是找不到的。
在這個時候,runtime 並沒有放棄最後的努力,再沒有找到對應的方法的時候,runtime會向對象發送一個forwardInvocation:的消息,並且把原始的消息以及消息的參數打成一個NSInvocation的一個對象裏面,作爲forwardInvocation:的唯一的參數。 forwardInvocation:本身是在NSObject裏面定義的,如果你需要重載這個函數的話,那麼任何試圖向你的類發送一個沒有定義的消息的話,你都可以在forwardInvocation:裏面捕捉到,並且把消息送到某一個安全的地方,從而避免了系統報錯。
筆者沒有在本章代碼中重寫forwardInvocation:,但是在重寫forwardInvocation:的時候一定要注意避免消息的循環發送。比如說,同學們在A類對象的forwardInvocation裏面,把A類不能響應的消息以及消息的參數發給B類的對象;同時在B類的forwardInvocation裏面把B類不能響應的消息發給A類的時候,容易形成死循環。當然一個人寫代碼的時候不容易出現這個問題,當你在一個工作小組裏面做的時候,如果你重寫forwardInvocation:的時候,需要和小組的其他人達成共識,從而避免循環調用。
6.4,重載方法的調用
讓我們繼續關注“06-NSObject.m”文件,請大家參考一下下面的代碼:
2 MyClass my_cattle_class = cattle->isa;
3 SEL say = @selector(saySomething);
4 IMP cattle_sayFunc = [cattle methodForSelector:say];
5 cattle_sayFunc(cattle, say);
6
7 Class redBull_class = redBull->isa;
8 MyClass my_redBull_class = redBull->isa;
9
10 IMP redBull_sayFunc = [redBull methodForSelector:say];
11 redBull_sayFunc(redBull, say);
本節的內容和6.3節的內容比較類似,關於代碼部分筆者認爲就不需要解釋了,如果同學們有所不熟悉的話,可以參考一下第5章的內容。
在我們的Cattle類和Bull類裏面,都有saySometing這個實例方法。我們知道只要方法的定義相同,那麼它們的SEL是完全一樣的。我們根據一個SEL say,在cattle和redBull對象裏面找到了他們的函數指針。根據6.3節的講述,我們知道當runtime接收到尋找方法的時候,會首先在這個類裏面尋找,尋找到了之後尋找的過程也就結束了,同時把這個方法的IMP返回給我們。所以,在上面的代碼裏面的cattle_sayFunc和redBull_sayFunc應該是不一樣的,如圖6-4所示:
圖6-4, cattle_sayFunc和redBull_sayFunc的地址
6.5,超類和子類中的Class
在類進行內存分配的時候,對於一個類而言,runtime需要找到這個類的超類,然後把超類的Class的指針的地址賦值給isa裏面的super_class。所以,我們的cattle裏面的Class應該和redBull裏面的Class裏面的super_class應該是完全相同的,請參照圖6-5:
圖6-5, cattle裏面的Class和redBull裏面的Class裏面的super_class
6.6,實例變量的內存分配的位置
我們先來回憶一下對象是怎樣被創建的。創建對象的時候,類的內容需要被調入到內存當中我們稱之爲內存分配(Allocation),然後需要把實體變量進行初始化(Initialization),當這些步驟都結束了之後,我們的類就被實例化了,我們把實例化完成的類叫做對象(Object)。
對於內存分配的過程,runtime需要知道分配多少內存還有各個實例變量的位置。我們回到“MyNSObject.h”,參照如下代碼:
2 typedef struct objc_ivar_list {
3 int ivar_count;
4 struct objc_ivar {
5 const char* ivar_name;
6 const char* ivar_type;
7 int ivar_offset;
8 } ivar_list[1];
9 } IvarList, *IvarList_t;
我們仔細看看第5行的ivar_name,顧名思義這個是實例變量的名字,第6行的ivar_type是實例變量的類型,第7行的ivar_offset,這個就是位置的定義。runtime從類的isa裏面取得了這些信息之後就知道了如何去分配內存。我們來看看圖6-6:
圖6-6,實例變量在內存中的位置
在cattle裏面,我們看到了第一個實例變量是isa,第二個就是我們定義的legsCount。其中isa是超類的變量,legsCount是Cattle類的變量。我們可以看出來,總是把超類的變量放在前頭,然後是子類的變量。
那麼對於redBull而言是什麼樣子呢?我們來看看圖6-7
圖6-7,redBull裏面的實例變量的位置
我們通過圖6-7可以發現redBull的Class裏面的skinColor的位置偏移是8,很明顯,runtime爲isa和legsCount預留了2個位置。
6.7本章總結
非常感謝大家!
在本章裏面,筆者通過一個小小的“把戲”爲同學們揭開了NSObject的神祕的面紗。本章的內容,雖然對理解Objective-C不是必需的,但是對以後的章節的內容的理解會有一個非常好的輔助作用,希望同學們花費一點點心思和時間閱讀一下。
另外,筆者需要再次強調一下,由於筆者沒有得到官方的正式的文檔說明,所以不能保證本章的內容是完整而且準確的,本章內容僅供大家參考和娛樂,希望大家諒解。