Objective-C中有兩個NSObject,一個是NSObject類,另一個是NSObject協議。而其中NSObject類采用了NSObject協議。在本文中,我們主要整理一下NSObject類的使用。
說到NSObject類,寫Objective-C的人都應該知道它。它是大部分Objective-C類繼承體系的根類。這個類提供了一些通用的方法,對象通過繼承NSObject,可以從其中繼承訪問運行時的接口,並讓對象具備Objective-C對象的基本能力。以下我們就來看看NSObejct提供給我們的一些基礎功能。
+load與+initialize
這兩個方法可能平時用得比較少,但很有用。在我們的程序編譯後,類相關的數據結構會保留在目標文件中,在程序運行後會被解析和使用,此時類的信息會經歷加載和初始化兩個過程。在這兩個過程中,會分別調用類的load方法和initialize方法,在這兩個方法中,我們可以適當地做一些定制處理。不當是類本身,類的分類也會經歷這兩個過程。對於一個類,我們可以在類的定義中重寫這兩個方法,也可以在分類中重寫它們,或者同時重寫。
load方法
對於load方法,當Objective-C運行時加載類或分類時,會調用這個方法;通常如果我們有一些類級別的操作需要在加載類時處理,就可以放在這裡面,如為一個類執行Swizzling Method操作。
load消息會被發送到動態加載和靜態鏈接的類和分類裡面。不過,只有當我們在類或分類裡面實現這個方法時,類/分類才會去調用這個方法。
在類繼承體系中,load方法的調用順序如下:
一個類的load方法會在其所有父類的load方法之後調用
分類的load方法會在對應類的load方法之後調用
在load的實現中,如果使用同一庫中的另外一個類,則可能是不安全的,因為可能存在的情況是另外一個類的load方法還沒有運行,即另一個類可能尚未被加載。另外,在load方法裡面,我們不需要顯示地去調用[super load],因為父類的load方法會自動被調用,且在子類之前。
在有依賴關系的兩個庫中,被依賴的庫中的類其load方法會優先調用。但在庫內部,各個類的load方法的調用順序是不確定的。
initialize方法
當我們在程序中向類或其任何子類發送第一條消息前,runtime會向該類發送initialize消息。runtime會以線程安全的方式來向類發起initialize消息。父類會在子類之前收到這條消息。父類的initialize實現可能在下面兩種情況下被調用:
子類沒有實現initialize方法,runtime將會調用繼承而來的實現
子類的實現中顯示的調用了[super initialize]
如果我們不想讓某個類中的initialize被調用多次,則可以像如下處理:
+ (void)initialize { if (self == [ClassName self]) { // ... do the initialization ... } }
因為initialize是以線程安全的方式調用的,且在不同的類中initialize被調用的順序是不確定的,所以在initialize方法中,我們應該做少量的必須的工作。特別需要注意是,如果我們initialize方法中的代碼使用了鎖,則可能會導致死鎖。因此,我們不應該在initialize方法中實現復雜的初始化工作,而應該在類的初始化方法(如-init)中來初始化。
另外,每個類的initialize只會被調用一次。所以,如果我們想要為類和類的分類實現單獨的初始化操作,則應該實現load方法。
如果想詳細地了解這兩個方法的使用,可以查看《Effective Objective-C 2.0》的第51條,裡面有非常詳細的說明。如果想更深入地了解這兩個方法的調用,則可以參考objc庫的源碼,另外,NSObject的load和initialize方法一文從源碼層面為我們簡單介紹了這兩個方法。
對象的生命周期
一說到對象的創建,我們會立即想到[[NSObject alloc] init]這種經典的兩段式構造。對於這種兩段式構造,唐巧大神在他的”談ObjC對象的兩段構造模式“一文中作了詳細描述,大家可以參考一下。
本小節我們主要介紹一下與對象生命周期相關的一些方法。
對象分配
NSObject提供的對象分配的方法有alloc和allocWithZone:,它們都是類方法。這兩個方法負責創建對象並為其分配內存空間,返回一個新的對象實例。新的對象的isa實例變量使用一個數據結構來初始化,這個數據結構描述了對象的信息;創建完成後,對象的其它實例變量被初始化為0。
alloc方法的定義如下:
+ (instancetype)alloc
而allocWithZone:方法的存在是由歷史原因造成的,它的調用基本上和alloc是一樣的。既然是歷史原因,我們就不說了,官方文檔只給了一句話:
This method exists for historical reasons; memory zones are no longer used by Objective-C.
我們只需要知道alloc方法的實現調用了allocWithZone:方法。
對象初始化
我們一般不去自己重寫alloc或allocWithZone:方法,不用去關心對象是如何創建、如何為其分配內存空間的;我們更關心的是如何去初始化這個對象。上面提到了,對象創建後,isa以外的實例變量都默認初始化為0。通常,我們希望將這些實例變量初始化為我們期望的值,這就是init方法的工作了。
NSObject類默認提供了一個init方法,其定義如下:
- (instancetype)init
正常情況下,它會初始化對象,如果由於某些原因無法完成對象的創建,則會返回nil。注意,對象在使用之前必須被初始化,否則無法使用。不過,NSObject中定義的init方法不做任何初始化操作,只是簡單地返回self。
當然,我們定義自己的類時,可以提供自定義的初始化方法,以滿足我們自己的初始化需求。需要注意的就是子類的初始化方法需要去調用父類的相應的初始化方法,以保證初始化的正確性。
講完兩段式構造的兩個部分,有必要來講講NSObject類的new方法了。
new方法實際上是集alloc和init於一身,它創建了對象並初始化了對象。它的實現如下:
+ (instancetype)new { return [[self alloc] init]; }
new方法更多的是一個歷史遺留產物,它源於NeXT時代。如果我們的初始化操作只是調用[[self alloc] init]時,就可以直接用new來代替。不過如果我們需要使用自定義的初始化方法時,通常就使用兩段式構造方式。
拷貝
說到拷貝,相信大家都很熟悉。拷貝可以分為“深拷貝”和“淺拷貝”。深拷貝拷貝的是對象的值,兩個對象相互不影響,而淺拷貝拷貝的是對象的引用,修改一個對象時會影響到另一個對象。
在Objective-C中,如果一個類想要支持拷貝操作,則需要實現NSCopying協議,並實現copyWithZone:【注意:NSObject類本身並沒有實現這個協議】。如果一個類不是直接繼承自NSObject,則在實現copyWithZone:方法時需要調用父類的實現。
雖然NSObject自身沒有實現拷貝協議,不過它提供了兩個拷貝方法,如下:
- (id)copy
這個是拷貝操作的便捷方法。它的返回值是NSCopying協議的copyWithZone:方法的返回值。如果我們的類沒有實現這個方法,則會拋出一個異常。
與copy對應的還有一個方法,即:
- (id)mutableCopy
從字面意義來講,copy可以理解為不可變拷貝操作,而mutableCopy可以理解為可變操作。這便引出了拷貝的另一個特性,即可變性。
顧名思義,不可變拷貝即拷貝後的對象具有不可變屬性,可變拷貝後的對象具有可變屬性。這對於數組、字典、字符串、URL這種分可變和不可變的對象來說是很有意義的。我們來看如下示例:
NSMutableArray *mutableArray = [NSMutableArray array]; NSMutableArray *array = [mutableArray copy]; [array addObject:@"test1"];
實際上,這段代碼是會崩潰的,我們來看看崩潰日志:
-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070'
從中可以看出,經過copy操作,我們的array實際上已經變成不可變的了,其底層元類是__NSArrayI。這個類是不支持addObject:方法的。
偶爾在代碼中,也會看到類似於下面的情況:
@property (copy) NSMutableArray *array;
這種屬性的聲明方式是有問題的,即上面提到的可變性問題。使用self.array = **賦值後,數組其實是不可變的,所以需要特別注意。
mutableCopy的使用也挺有意思的,具體的還請大家自己去試驗一下。
釋放
當一個對象的引用計數為0時,系統就會將這個對象釋放。此時run time會自動調用對象的dealloc方法。在ARC環境下,我們不再需要在此方法中去調用[super dealloc]了。我們重寫這個方法主要是為了釋放對象中用到的一些資源,如我們通過C方法分配的內存空間。dealloc方法的定義如下:
- (void)dealloc
需要注意的是,我們不應該直接去調用這個方法。這些事都讓run time去做吧。
消息發送
Objective-C中對方法的調用並不是像C++裡面那樣直接調用,而是通過消息分發機制來實現的。這個機制核心的方法是objc_msgSend函數。消息機制的具體實現我們在此不做討論,可以參考Objective-C Runtime 運行時之三:方法與消息。
對於消息的發送,除了使用[obj method]這種機制之外,NSObject類還提供了一系列的performSelector**方法。這些方法可以讓我們更加靈活地控制方法的調用。接下來我們就來看看這些方法的使用。
在線程中調用方法
如果我們想在當前線程中調用一個方法,則可以使用以下兩個方法:
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes
這兩個方法會在當前線程的Run loop中設置一個定時器,以在delay指定的時間之後執行aSelector。如果我們希望定時器運行在默認模式(NSDefaultRunLoopMode)下,可以使用前一個方法;如果想自己指定Run loop模式,則可以使用後一個方法。
當定時器啟動時,線程會從Run loop的隊列中獲取到消息,並執行相應的selector。如果Run loop運行在指定的模式下,則方法會成功調用;否則,定時器會處於等待狀態,直到Run loop運行在指定模式下。
需要注意的是,調用這些方法時,Run loop會保留方法接收者及相關的參數的引用(即對這些對象做retain操作),這樣在執行時才不至於丟失這些對象。當方法調用完成後,Run loop會調用這些對象的release方法,減少對象的引用計數。
如果我們想在主線程上執行某個對象的方法,則可以使用以下兩個方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array
我們都知道,iOS中所有的UI操作都需要在主線程中處理。如果想在某個二級線程的操作完成之後做UI操作,就可以使用這兩個方法。
這兩個方法會將消息放到主線程Run loop的隊列中,前一個方法使用的是NSRunLoopCommonModes運行時模式;如果想自己指定運行模式,則使用後一個方法。方法的執行與之前的兩個performSelector方法是類似的。當在一個線程中多次調用這個方法將不同的消息放入隊列時,消息的分發順序與入隊順序是一致的。
方法中的wait參數指定當前線程在指定的selector在主線程執行完成之後,是否被阻塞住。如果設置為YES,則當前線程被阻塞。如果當前線程是主線程,而該參數也被設置為YES,則消息會被立即發送並處理。
另外,這兩個方法分發的消息不能被取消。
如果我們想在指定的線程中分發某個消息,則可以使用以下兩個方法:
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array
這兩個方法基本上與在主線程的方法差不多。在此就不再討論。
如果想在後台線程中調用接收者的方法,可以使用以下方法:
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
這個方法會在程序中創建一個新的線程。由aSelector表示的方法必須像程序中的其它新線程一樣去設置它的線程環境。
當然,我們經常看到的performSelector系列方法中還有幾個方法,即:
- (id)performSelector:(SEL)aSelector - (id)performSelector:(SEL)aSelector withObject:(id)anObject - (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject
不過這幾個方法是在NSObject協議中定義的,NSObject類實現了這個協議,也就定義了相應的實現。這個我們將在NSObject協議中來介紹。
取消方法調用請求
對於使用performSelector:withObject:afterDelay:方法(僅限於此方法)注冊的執行請求,在調用發生前,我們可以使用以下兩個方法來取消:
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument
前一個方法會取消所以接收者為aTarget的執行請求,不過僅限於當前run loop,而不是所有的。
後一個方法則會取消由aTarget、aSelector和anArgument三個參數指定的執行請求。同樣僅限於當前run loop。
消息轉發及動態解析方法
當一個對象能接收一個消息時,會走正常的方法調用流程。但如果一個對象無法接收一個消息時,就會走消息轉發機制。
消息轉發機制基本上分為三個步驟:
動態方法解析
備用接收者
完整轉發
具體流程可參考Objective-C Runtime 運行時之三:方法與消息,《Effective Objective-C 2.0》一書的第12小節也有詳細描述。在此我們只介紹一下NSObject類為實現消息轉發提供的方法。
首先,對於動態方法解析,NSObject提供了以下兩個方法來處理:
+ (BOOL)resolveClassMethod:(SEL)name + (BOOL)resolveInstanceMethod:(SEL)name
從方法名我們可以看出,resolveClassMethod:是用於動態解析一個類方法;而resolveInstanceMethod:是用於動態解析一個實例方法。
我們知道,一個Objective-C方法是其實是一個C函數,它至少帶有兩個參數,即self和_cmd。我們使用class_addMethod函數,可以給類添加一個方法。我們以resolveInstanceMethod:為例,如果要給對象動態添加一個實例方法,則可以如下處理:
void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... } + (BOOL) resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSel]; }
其次,對於備用接收者,NSObject提供了以下方法來處理:
- (id)forwardingTargetForSelector:(SEL)aSelector
該方法返回未被接收消息最先被轉發到的對象。如果一個對象實現了這個方法,並返回一個非空的對象(且非對象本身),則這個被返回的對象成為消息的新接收者。另外如果在非根類裡面實現這個方法,如果對於給定的selector,我們沒有可用的對象可以返回,則應該調用父類的方法實現,並返回其結果。
最後,對於完整轉發,NSObject提供了以下方法來處理
- (void)forwardInvocation:(NSInvocation *)anInvocation
當前面兩步都無法處理消息時,運行時系統便會給接收者最後一個機會,將其轉發給其它代理對象來處理。這主要是通過創建一個表示消息的NSInvocation對象並將這個對象當作參數傳遞給forwardInvocation:方法。我們在forwardInvocation:方法中可以選擇將消息轉發給其它對象。
在這個方法中,主要是需要做兩件事:
找到一個能處理anInvocation調用的對象。
將消息以anInvocation的形式發送給對象。anInvocation將維護調用的結果,而運行時則會將這個結果返回給消息的原始發送者。
這一過程如下所示:
- (void)forwardInvocation:(NSInvocation *)invocation { SEL aSelector = [invocation selector]; if ([friend respondsToSelector:aSelector]) [invocation invokeWithTarget:friend]; else [super forwardInvocation:invocation]; }
當然,對於一個非根類,如果還是無法處理消息,則應該調用父類的實現。而NSObject類對於這個方法的實現,只是簡單地調用了doesNotRecognizeSelector:。它不再轉發任何消息,而是拋出一個異常。doesNotRecognizeSelector:的聲明如下:
- (void)doesNotRecognizeSelector:(SEL)aSelector
運行時系統在對象無法處理或轉發一個消息時會調用這個方法。這個方法引發一個NSInvalidArgumentException異常並生成一個錯誤消息。
任何doesNotRecognizeSelector:消息通常都是由運行時系統來發送的。不過,它們可以用於阻止一個方法被繼承。例如,一個NSObject的子類可以按以下方式來重寫copy或init方法以阻止繼承:
- (id)copy { [self doesNotRecognizeSelector:_cmd]; }
這段代碼阻止子類的實例響應copy消息或阻止父類轉發copy消息—雖然respondsToSelector:仍然報告接收者可以訪問copy方法。
當然,如果我們要重寫doesNotRecognizeSelector:方法,必須調用super的實現,或者在實現的最後引發一個NSInvalidArgumentException異常。它代表對象不能響應消息,所以總是應該引發一個異常。
獲取方法信息
在消息轉發的最後一步中,forwardInvocation:參數是一個NSInvocation對象,這個對象需要獲取方法簽名的信息,而這個簽名信息就是從methodSignatureForSelector:方法中獲取的。
該方法的聲明如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個方法返回包含方法描述信息的NSMethodSignature對象,如果找不到方法,則返回nil。如果我們的對象包含一個代理或者對象能夠處理它沒有直接實現的消息,則我們需要重寫這個方法來返回一個合適的方法簽名。
對應於實例方法,當然還有一個處理類方法的相應方法,其聲明如下:
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
另外,NSObject類提供了兩個方法來獲取一個selector對應的方法實現的地址,如下所示:
- (IMP)methodForSelector:(SEL)aSelector + (IMP)instanceMethodForSelector:(SEL)aSelector
獲取到了方法實現的地址,我們就可以直接將IMP以函數形式來調用。
對於methodForSelector:方法,如果接收者是一個對象,則aSelector應該是一個實例方法;如果接收者是一個類,則aSelector應該是一個類方法。
對於instanceMethodForSelector:方法,其只是向類對象索取實例方法的實現。如果接收者的實例無法響應aSelector消息,則產生一個錯誤。
測試類
對於類的測試,在NSObject類中定義了兩個方法,其中類方法instancesRespondToSelector:用於測試接收者的實例是否響應指定的消息,其聲明如下:
+ (BOOL)instancesRespondToSelector:(SEL)aSelector
如果aSelector消息被轉發到其它對象,則類的實例可以接收這個消息而不會引發錯誤,即使該方法返回NO。
為了詢問類是否能響應特定消息(注意:不是類的實例),則使用這個方法,而不使用NSObject協議的實例方法respondsToSelector:。
NSObject還提供了一個方法來查看類是否采用了某個協議,其聲明如下:
+ (BOOL)conformsToProtocol:(Protocol *)aProtocol
如果一個類直接或間接地采用了一個協議,則我們可以說這個類實現了該協議。我們可以看看以下這個例子:
@protocol AffiliationRequests @interface MyClass : NSObject BOOL canJoin = [MyClass conformsToProtocol:@protocol(Joining)];
通過繼承體系,MyClass類實現了Joining協議。
不過,這個方法並不檢查類是否實現了協議的方法,這應該是程序員自己的職責了。
識別類
NSObject類提供了幾個類方法來識別一個類,首先是我們常用的class類方法,該方法聲明如下:
+ (Class)class
該方法返回類對象。當類是消息的接收者時,我們只通過類的名稱來引用一個類。在其它情況下,類的對象必須通過這個方法類似的方法(-class實例方法)來獲取。如下所示:
BOOL test = [self isKindOfClass:[SomeClass class]];
NSObject還提供了superclass類方法來獲取接收者的父類,其聲明如下:
+ (Class)superclass
另外,我們還可以使用isSubclassOfClass:類方法查看一個類是否是另一個類的子類,其聲明如下:
+ (BOOL)isSubclassOfClass:(Class)aClass
描述類
描述類是使用description方法,它返回一個表示類的內容的字符串。其聲明如下:
+ (NSString *)description
我們在LLDB調試器中打印類的信息時,使用的就是這個方法。
當然,如果想打印類的實例的描述時,使用的是NSObject協議中的實例方法description,我們在此不多描述。
歸檔操作
一說到歸檔操作,你會首先想到什麼呢?我想到的是NSCoding協議以及它的兩個方法: initWithCoder:和encodeWithCoder:。如果我們的對象需要支持歸檔操作,則應該采用這個協議並提供兩個方法的具體實現。
在編碼與解碼的過程中,一個編碼器會調用一些方法,這些方法允許將對象編碼以替代一個更換類或實例本身。這樣,就可以使得歸檔在不同類層次結構或類的不同版本的實現中被共享。例如,類簇能有效地利用這一特性。這一特性也允許每個類在解碼時應該只維護單一的實例來執行這一策略。
NSObject類雖然沒有采用NSCoding協議,但卻提供了一些替代方法,以支持上述策略。這些方法分為兩類,即通用和專用的。
通用方法由NSCoder對象調用,主要有如下幾個方法和屬性:
@property(readonly) Class classForCoder - (id)replacementObjectForCoder:(NSCoder *)aCoder - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
專用的方法主要是針對NSKeyedArchiver對象的,主要有如下幾個方法和屬性:
@property(readonly) Class classForKeyedArchiver + (NSArray *)classFallbacksForKeyedArchiver + (Class)classForKeyedUnarchiver - (id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver
子類在歸檔的過程中如果有特殊的需求,可以重寫這些方法。這些方法的具體描述,可以參考官方文檔。
在解碼或解檔過程中,有一點需要考慮的就是對象所屬類的版本號,這樣能確保老版本的對象能被正確地解析。NSObject類對此提供了兩個方法,如下所示:
+ (void)setVersion:(NSInteger)aVersion + (NSInteger)version
它們都是類方法。默認情況下,如果沒有設置版本號,則默認是0.
總結
NSObject類是Objective-C中大部分類層次結構中的根類,並為我們提供了很多功能。了解這些功能更讓我們更好地發揮Objective-C的特性。
參考
NSObject Class Reference
Archives and Serializations Programming Guide
NSObject的load和initialize方法
Objective-C Runtime 運行時之三:方法與消息
《Effective Objective-C 2.0》