NSObject之一

Objective-C中有兩個NSObject,一個是NSObject類,另外一個是NSObject協議。而其中NSObject類採用了NSObject協議。在本文中,咱們主要整理一下NSObject類的使用。html

說到NSObject類,寫Objective-C的人都應該知道它。它是大部分Objective-C類繼承體系的根類。這個類提供了一些通用的方法,對象經過繼承NSObject,能夠從其中繼承訪問運行時的接口,並讓對象具有Objective-C對象的基本能力。如下咱們就來看看NSObejct提供給咱們的一些基礎功能。ios

+load與+initialize

這兩個方法可能平時用得比較少,但頗有用。在咱們的程序編譯後,類相關的數據結構會保留在目標文件中,在程序運行後會被解析和使用,此時類的信息會經歷加載和初始化兩個過程。在這兩個過程當中,會分別調用類的load方法和initialize方法,在這兩個方法中,咱們能夠適當地作一些定製處理。不當是類自己,類的分類也會經歷這兩個過程。對於一個類,咱們能夠在類的定義中重寫這兩個方法,也能夠在分類中重寫它們,或者同時重寫。git

load方法

對於load方法,當Objective-C運行時加載類或分類時,會調用這個方法;一般若是咱們有一些類級別的操做須要在加載類時處理,就能夠放在這裏面,如爲一個類執行Swizzling Method操做。程序員

load消息會被髮送到動態加載和靜態連接的類和分類裏面。不過,只有當咱們在類或分類裏面實現這個方法時,類/分類纔會去調用這個方法。github

在類繼承體系中,load方法的調用順序以下:objective-c

  1. 一個類的load方法會在其全部父類的load方法以後調用
  2. 分類的load方法會在對應類的load方法以後調用

在load的實現中,若是使用同一庫中的另一個類,則多是不安全的,由於可能存在的狀況是另一個類的load方法尚未運行,即另外一個類可能還沒有被加載。另外,在load方法裏面,咱們不須要顯示地去調用[super load],由於父類的load方法會自動被調用,且在子類以前。數組

在有依賴關係的兩個庫中,被依賴的庫中的類其load方法會優先調用。但在庫內部,各個類的load方法的調用順序是不肯定的。安全

initialize方法

當咱們在程序中向類或其任何子類發送第一條消息前,runtime會向該類發送initialize消息。runtime會以線程安全的方式來向類發起initialize消息。父類會在子類以前收到這條消息。父類的initialize實現可能在下面兩種狀況下被調用:數據結構

  1. 子類沒有實現initialize方法,runtime將會調用繼承而來的實現
  2. 子類的實現中顯示的調用了[super initialize]

若是咱們不想讓某個類中的initialize被調用屢次,則能夠像以下處理:app

+ (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。

消息轉發及動態解析方法

當一個對象能接收一個消息時,會走正常的方法調用流程。但若是一個對象沒法接收一個消息時,就會走消息轉發機制。

消息轉發機制基本上分爲三個步驟:

  1. 動態方法解析
  2. 備用接收者
  3. 完整轉發

具體流程可參考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:方法中能夠選擇將消息轉發給其它對象。

在這個方法中,主要是須要作兩件事:

  1. 找到一個能處理anInvocation調用的對象。
  2. 將消息以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 <Joining>

@interface MyClass : NSObject <AffiliationRequests, Normalization>

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的特性。

參考

  1. NSObject Class Reference
  2. Archives and Serializations Programming Guide
  3. NSObject的load和initialize方法
  4. Objective-C Runtime 運行時之三:方法與消息
  5. 《Effective Objective-C 2.0》

http://southpeak.github.io/blog/2015/01/31/nsobjectzhi-%5B%3F%5D/

相關文章
相關標籤/搜索