使用 Objectvice-C 進行全面對象編程時,除了需要知道語言本身的語法和面向對象的知識外,還需要了解Objectvice-C的根類 NSObject 的信息。
NSObject
根類的作用
作為一門動態編程語言,Objectstvice-C有很多動態的特性,因此,Objectvice-C不進需要編譯環境,同時還需要一個運行時系統(runtime system)來執行編譯好的代碼。運行時系統扮演的角色類似于Objectvice-C的操作系統,他負責完成對象生成、釋放時的內存管理、發來的消息查找對應的處理方法等工作。
通常情況下,程序無法直接使用運行時系統提供的功能。根類方法提供了運行時系統的基本工恩給你。繼承了 NSObject 的所有類都可以自由的使用運行時系統的功能,也就是說,根類就想到于系統的一個借口。
根類通過哪些方式提供了哪些功能對系統有很大的影響。因此,根類不同的系統之間是無法開發出通用的程序的。
Cocoa 是以OPENSTEPDE的核心 API 為基礎發展起來的。OPENSTEP的前身為 NeXTstep。在 NeXTstep 時代,根類是累 Object,而在 OPENSTEP 時代,根類則變為了 NSObject,同時類的設計也得到了大幅度的改進。
NSArray,NSString 等等NS前綴類、函數歸屬于cocoa Fundation基礎類庫,其"NS”的由來據說是這樣的:喬布斯被蘋果開除后,創立了NeSt公司,而cocoa Fundation基礎類庫就是出自于NeST公司,NeST中的"NS"被作為Fundation中所有成員的前綴
類和實例
NSObject 只是一個實例變量,就是 Class 類型的變量 isa。isa 用于表示實例對象屬于哪個類對象。因為 isa 決定著實例變量和類的關系,非常重要,所以子類不可以修改 isa 的值。另外,也不能通過直接訪問 isa 來查詢實例變量到底屬于哪個類,而是要通過實例方法 class 來完成查詢。
在運行時的代碼中我們可以查看到objc_class的定義如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
下面對類和實例變量的相關方法進行說明。NSObject 的方法與其說是為自己定義的,不如說是為了其子類和所有實例對象而定義的。
- (class) class
返回消息接收者所屬類的類對象
+ (class) class
返回類型對
雖然可以使用類名作為消息的接受者來調用類方法,但類對象是其他消息的參數,或者將類對象賦值給變量的時候,需要通過這個方法來獲取類的參數
- (id) self
返回接受者自身。是一個無任何實際動作但很有用的方法。
-(BOOL) isMemberOfClass: (Class) aClass
判斷消息接受者是不是參數 aClass 類的對象
-(BOOL) isKindOfClass: (Class) aClass
判斷消息接受者是否是參數 aClass 類或者 aClass 的子類的實例。這個函數和 isMemberOfClass:的區別在于當消息的接受者是 aClass 的子類的實例時也返回 YES。
- (BOOL) isSubclassOfClass: (Class) aClass
判斷消息接受者是不是參數 aClass 的子類或自身,如果是則返回 YES
- (Class) superclass
返回消息接受者所在類的父類的類對象。
+ (Class) superclass
返回消息接收類的父類和類對象
實例對象的生成和釋放
+ (id) alloc
生成消息接收類的實例對象。通常和 init 或者 init 開頭的方法連用,生成實例化對象的同事需要對其進行初始化。子類中不潤徐重寫 alloc 方法
+ (void) dealloc
釋放實例對象。dealloc 被稱之為 release 的結果調用。除了在子類中重寫 dealloc 的情況之外,程序不潤徐直接調用 dealloc
- (oneway void)release
將消息接受者的引用計數減1.引用計數變為0時,dealloc 方法被調用,消息接受者被釋放
- (id)retain
為消息接收者的引用計數加1,同事返回消息接收者
- (id)autorelease
把消息的接受者加入到自動釋放池中,同事返回消息接受者
- (NSUinteger) retainCount
返回消息接受者的引用計數,可在調試時使用這個方法。NSUInteger 是無符號證書類型
- (void)finalize
垃圾收集器在釋放接受者對象之前會執行該 finalize 方法。
上面從 dealloc 到 retainCount 都是手動引用計數管理內存時使用的方法,使用 ARC 時不可用。finalize 僅供垃圾回收有效時使用。
初始化
- (id) init
init 可對 alloc 生成的實例對象進行初始化。子類中可以重寫 init 或者自定義的心的以 init 開頭的初始化函數。
+ (void)initialize
被用于類的初始化,也就是對類中共同使用的變量進行初始化設定等。這個方法會在類收到第一個消息之前被自動執行,不需手動調用
+ (id) new
new 是 alloc 和 init 的組合。new 方法返回的實例對象的所有者就是調用 new 方法的對象。但是把 alloc 和 init 組合定義為 new 沒有什么優點,并不建議使用。
根據類的實現不同,new 方法并不會每次都返回一個全新的實例對象。有時new 方法會返回對象池中預先生成的對象,也有可能每次都返回同一個對象。
對象的比較
-(BOOL) isEqual: (id) anObject
消息的接受者如果和參數 anObject 相等則返回 YES
- (NSUInteger) hash
把對象放入容器的時候,返回系統內部用的散列表
原則上來講,具有相同 id 值也就是同一個指針指向的對象被認為是相等的。而子類在這個基礎上進行了擴展,把擁有相同值認為是相等。我們可以根據需求對“想通知”激進型定義,但一般都會讓具備“相同值”的對象返回相同的散列表,因此就需要對散列表方法進行重新定義。而反之則并不成立,也就是說,散列值相等的兩個對象不一定相等。
另外,有的累中還自定義了 compare:或者isEqualTo 之類的方法。至于到底是哪個方法或者自定義類的時候是否需要定義比較的方法,都需要根據目的和類的內容做具體分析。
對象的內容描述
+ (NSString*) description
返回一個 NSString 類型的字符串,表示消息接受者所屬類的內容。通常是這個類的名稱。
- (NSString*)description
返回一個 NSString 類型的字符串,表示消息接受者的實例對象的內容。通常是類名家 id 值。子類中可以重新定義 description 的返回值。例如 NSString的實例會返回字符串的內容,NSArray 的實例會對數組中的每一個元素調用 description,然后將調用結果用句號進行分割,并且一起返回。
消息發送機制
選擇器和 SEL 類型
至今為止我們把選擇器(方法名)和消息關鍵字放在一起進行說明。程序中的方法名(選擇器)在便有被一個內部標識符所代替,這個內部標識符所對應的數據類型就是 SEL 類型。
Objective-C為了能夠在程序中操作編譯后的選擇器,定義了@selector()指令。通過使用@selector()指令,就可以直接飲用編譯后的選擇器。使用方法如下:
@selector(mutableCopy)
@selector(compare:)
@selector(replaceObjectAtIndex:withObject:)
也可以聲明 SEL 類型的變量
選擇器不同的情況下,編譯器轉換后生成的 SEL 類型的值也一定不同,相同的算擇期對應的 SEL 類型的值一定相同。Objective-C的程序員不需要知道選擇器對應的 SEL 類型的值到底是什么,具體的值是和處理器相關的。但是如果 SEL 類型的便利功能無效的話,可設其為 NULL,或者也可以使用(SEL)0這種常見的表達方式
可以使用 SEL 類型的變量來發送消息,為此,NSObject 中準備了如下方法
-(id)performSelector: (SEL) aSelector
向消息的接收者發送 aSelector代表的消息,返回這個消息的執行結果
-(id)performSelector: (SEL) aSelector
withObject: (id) anObject
與上面的方法一直,不過可以傳遞參數
例如下面兩個消息表達式進行的處理是相同的
[target description]
[targ performSelector: @selector(description)];
下面的例子展示了如何根據條件動態決定執行那個方法
SEL method = (void1) ? @selector(activate:) : @selector(hide:);
id obj = (cond2) ? MyDocument : defaultDocument;
[target performSelector:method withObject:obj]
這種調用方式很想 C 語言中函數指針的用法,使用函數指針可以實現和上面的程序同樣的功能。
函數指針是函數在內存中的地址。指針對應的函數是在編譯的時候決定的,不能夠執行指定之外的函數。SEL 類型就相當于方法名,根據消息接受者的不同(上例子中 target 的賦值),來動態執行不同的方法。
通過 SEL 類型來指定要中子星的方法,這就是 Objectivce-C消息發送的方式。也正是通過這種方法才實現了 Objectivce-C的動態性
消息搜索
對象收到一個消息后執行哪個方法是動態決定的。
所有的實例變量都存在一個 Class 類型的 isa 變量,它就是類對象。當收到消息后,運行時系統會檢查類內是否有和這個消息選擇器相同的方法,如果有就執行對應的方法,如果沒有就通過類對象中指向父類的指針來查找父類中是否有對應的方法。如果一直找到根類都沒有找到對應的方法,就會提示執行時錯誤。
如果每次收到消息都需要查找對應的方法的話,消息發送過程的開銷就會很大,是針對這種情況,運行時內部會緩存一個散列表,表中記錄著某個類擁有和什么樣的選擇器相對應的方法、方法被定義在何處等信息。這樣一來,當下次在收到同樣的消息時,直接利用上次緩存好的信息即可。
NSObject 中定義了可以動態檢查一個對象是否能夠響應某個選擇器的方法。
- (BOOL) respondsToSelector: (SEL) aSelector
查詢消息的接收者中是否能夠響應 aSelector 的方法,包括從父類繼承來的方法,如果存在的話則返回 YES
- (BOOL) instancesRespondToSelector: (SEL) aSelector
查詢消息的接收者所屬的類中是否能夠響應 aSelector 的實例方法,如果存在的話則返回 YES
一函數的形式來調用方法
類中定義的方法通常是以函數的形式來實現的,但通常在編程的時候并不會直接操作方法所對應的函數。
但如果想盡可能第讓程序更快一點,或者需要按照 C 語言的管理傳遞函數指針的時候,可以直接調用方法對應的函數,以節省發送消息的開銷。另外執行時動態加載方法的定義等時,也可以將方法作為函數調用。但是需要注意的是,如果以函數的形式來調用方法的話,將無法利用面向對象的動態綁定等功能。雖然消息發送同函數調用相比確實慢一點,但卻有面向對象的動態綁定、多態等優點。同這些優點相比,速度上略微的損失是不值得一提的。而其實消息發送的速度也非常的快。
通過使用下面的方法,可以獲得某個對象持有的方法的函數指針,這些方法都被定義在 NSObject 中。
- (IMP) methodForSelector: (SEL) aSelector
搜索和執行選擇器對應的方法,并返回指向該方法實現的函數指針。實例對象和類對象都可以使用這個方法。對實例對象使用時,會返回實例方法對應的函數,對類對象使用時,會返回類對象對應的函數
+ (IMP) instanceMethodForSelector: (SEL)aSelector;
搜索和制定選擇器相對應的實例方法,并返回該指向實例方法實現的函數指針
IMP 是“implemention”的縮寫,它是一個函數指針,指向了方法實現代碼的入口
IMP 的定義為:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
這個被指向的函數包括 id(self 指針)、調用的 SEL(方法名),以及其他參數
例如:
- (id)setBox:(id)obj1 title:(id)obj2;
foo 是這個方法所屬類的一個實例變量。獲取指向 setBox 的函數指針,并且通過該指針進行函數調用的過程如下:
IMP funcp;
funcp = [foo methodForSelector:@selector[setBox:title]];
xyz = (*funcp)(foo,@selector[setBox:title:],param1,param2)
類對象和跟對象
因為累對象也是一個對象,所以累對象可以作為根類 NSObject 的某個子類的對象來使用。下面這句話看上去好像比較奇怪,但是實際上他是正確的,會返回 YES
[NSString class] isKindOfClass:[NSObject class]]
這就說明了相當于類對象的類的對象是存在的。而類對象的類就被叫做元類(metaclass)。實例對象(instance object)所屬的類是 class,類對象(class object)所屬的類是 metaclass。
Objective-C 中的很多概念都來源于 Smalltalk,元類的概念就是其中之一。但現在的 Objective-C中已經不存在元類的概念了,程序中不能操作元類。用于表示對象的 id 類型和表示類的 Class 類實際上都是指向結構體的指針。被詳細定義在/usr/include/objc 下面的 objc.h 頭文件中。通過查看 objc.h 中 id和 Class 的定義,就會發現類和元類的關系如圖所示。Objective-C2.0更新了基本的數據結構,但是沒有改變類和元類的關系。
類 A 是 NSObject 的子類,類 B是 A 的子類。類對象和實例對象都存在一個成員變量 isa,它是一個 objc_class 類型的指針
圖中帶有 isa 的實現表明了 isa 指向的對象,帶有 super_class 的虛線則表明了父類的關系。
類對象中保存的是實例方法,元對象中保存的是累方法。通過這樣的定義能夠統一實現實例方法和類方法的調用機制。
因為編程時不可以直接操作元類,所以并不需要完全了解圖中的細節。大家只需要記住任何一個類對象都是繼承了根類的元對象的一個實例即可。也就是說,類對象可以執行根類對象的實例方法。
例如,類對象可以執行 NSObject 的實例方法 performSelector:和 respondsToSelector:。當然提前是沒有將這些方法作為類方法再次定義。
下面讓我們總結一下。其中(1)和(2)我們已經介紹過了。(3)比較不容易理解,
- 所有類的實例對象都可以執行根類的實例方法
- 如果在派生類中重新定義了實例方法,新定義的方法會被執行
- 所有類的類對象都可以執行根類的類方法
- 如果在派生類中重新定義了類方法,新定義的方法會被執行
- 所有類的類對象都可以執行根類的實例方法
- 即使在派生類中重新定義了實例方法,根類中的方法也會被執行
- 如果在派生類中將實例方法作為類方法重新定義了的話,新定義的方法會被執行
參考文獻
① Objectivce-C編程全解(第三版) [日]荻原剛志 著 唐璐 翟俊杰 譯
更多內容請關注我的獨立博客:http://www.aircrayon.xyz