iOS文檔補完計劃--NSObject

目錄

  • NSObject類
  • 類的初始化
    • load
    • initialize
  • 創建、複製和銷燬
    • alloc
    • allocWithZone
    • init
    • new
    • copy
    • mutableCopy
    • copyWithZone:
    • mutableCopyWithZone:
    • dealloc
  • 類/對象的識別與判等
    • class
    • superclass
    • hash
    • isEqual
    • isProxy
    • isKindOfClass
    • isMemberOfClass
    • isSubclassOfClass:
  • 類/對象的測試
    • instancesRespondToSelector:
    • conformsToProtocol:
  • 獲取方法信息
    • methodForSelector:
    • instanceMethodForSelector:
  • 類/對象的描述
    • debugDescription
    • description
  • 發送消息
    • performSelector:withObject:afterDelay:
    • performSelector:withObject:afterDelay:inModes:
    • performSelectorOnMainThread:withObject:waitUntilDone:
    • performSelectorOnMainThread:withObject:waitUntilDone:modes:
    • performSelector:onThread:withObject:waitUntilDone:
    • performSelector:onThread:withObject:waitUntilDone:modes:
    • performSelectorInBackground:withObject:
    • cancelPreviousPerformRequestsWithTarget:
    • cancelPreviousPerformRequestsWithTarget:selector:object:
  • 動態解析(消息轉發)
    • 解決階段
      • resolveClassMethod
      • resolveInstanceMethod
    • 重定向(Fast Forwarding)
      • forwardingTargetForSelector
    • 消息轉發(Normal Forwarding)
      • methodSignatureForSelector
      • instanceMethodSignatureForSelector
      • forwardInvocation
    • 錯誤處理
      • doesNotRecognizeSelector:
  • Weak相關
    • allowsWeakReference
    • retainWeakReference

NSObject類/NSObject協議

幾乎所有OC對象都可以使用NSObject的方法、因爲絕大部分OC對象都繼承者他。

NSObject協議

方法很多與NSObject的方法相同、只是從類方法(爲了簡便)變成了對象方法/屬性這種形式。
並且、他也被NSProxy遵循、這點正體現了OC的多繼承。比如NSObjectNSProxy對象都可以使用isKindOfClass方法。
而類對象、也可以使用對象方法(大概是因爲類對象也是一種對象吧)。

所以、下文中:

  1. 對於"-"的標記、本身就是可以作用於類對象的。
  2. 而"+"、但並不代表只能用於類對象。(NSObject協議中可能聲明瞭"-"的版本)。
    比如[NSObject hash]、[[NSObject class] hash]、[[NSObject new] hash]

類的初始化

  • + (void)load

程序運行時加載(添加到Runtime中)一個Class、或者Category時調用。
並且只會調用一次。

1. +(void)load整個類最先被調用的方法
所以、對於method swizzle這種從一開始就希望起作用的操作、需要放在這裏。

2. 父類先於子類、主類優先於分類
需要注意的是如果子類沒有使用+(void)load方法、父類並不會被優先調用(也就是依舊按照Compile Sources的順序)。
由於這個規則存在、我們也不需要主動實現[super load]方法。

3. 與Compile Sources的關係
只要加入Compile Sources中、即使項目中沒有人對其#import也一樣會調用(畢竟是動態語言)。
默認的調用的順序、也與Compile Sources中的順序相同。

4. 不主動實現、就不會被調用
+ load會按照模塊被存儲在loadable_classes/loadable_categories結構體中。而後取出、並且通過C函數指針調用。
所以、也不會經過消息轉發的過程(子類沒實現、並不會調用父類)。
詳情可以參考《iOS基礎(九) - load和initialize的實現原理》

5. 在load方法被自動調用之前、一個類仍然可以被使用
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

也就是你可以這樣寫、但我不知道有什麼意義~

@implementation Test
+ (void)load {
    [[Test3 new]hahaha];
}
  • + (void)initialize [ɪ'nɪʃəlaɪz]

向一個類發送第一條消息前被調用、對於父類實現(注意不是父類)的調用可能不止一次。

1. 父類調用在子類之前
在本類initialize(callInitialize(cls))調用之前、如果父類沒被調用過、會主動調用一次。並且父類中也如此實現、也就是會遞歸調用。

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    ...    
    if (reallyInitialize) {
            callInitialize(cls);
    }
    ...
}

2. 如果子類未實現+ (void)initialize、則會調用一次父類
所以、在官方文檔以及xcode自動補全中採用以下寫法

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

3. 每個類只會被調用一次
分類如果實現、則不會調用主類、這與+ (void)load不同。
所以如果需要分別定製主類以及category、應該寫在+ (void)load中。

3. 調用在+(void)load之前
畢竟+(void)load也是個消息。

4. 如果一個類沒有被使用(即使被#import)、便不會被調用
但如果他自己實現了+(void)load方法、系統在調用+(void)load之前、會調用+ (void)initialize進行初始化。

loadinitialize內部都實現了加鎖、是線程安全的。


創建、複製和銷燬

  • alloc

爲該對象分配地址空間

對象創建後、isa以外的實例變量都默認初始化爲0

對於NSObject而言、alloc其實已經初始化完畢了。但對於其他(比如UIView)類、還需要init來進行進一步配置。

  • allocWithZone

作用於alloc相同。文檔上上說是由於歷史原因。

  • init

對已經分配了內存空間的對象進行進一步配置。

在某些情況下、init可能會返回一個新的對象(詳見《iOS架構補完計劃--設計模式》中對於工廠模式的介紹)。

  • new

集alloc和init於一身

相當於調用[[Class alloc] init];、也是一種歷史遺留的產物、不過還挺方便。

  • copy

通過自己實現<NSCopying>協議的copyWithZone:方法返回一個不可變的副本

如果沒有實現協議方法、則會崩潰。

  • mutableCopy

通過自己實現< NSMutableCopying >協議的mutableCopyWithZone:方法返回一個可變的副本

  • + (id)copyWithZone:
  • + (id)mutableCopyWithZone:

需要注意這兩個方法並不是<NSCopying/NSMutableCopying>那個對象方法、而是系統爲類對象實現的。

二者均被標記成OBJC_ARC_UNAVAILABLE、也就是ARC下不需要(主動實現?)。

但是官方文檔中指出This method exists so class objects can be used in situations where you need an object that conforms to the NSCopying protocol.

也就是說、類對象也可以被copy、並且系統幫我們進行了內部實現。
需要注意的是、類對象的copy只是單純的返回自身而已。
但是這個機制讓我們可以將類對象作爲key使用。

id obj0 = [Test class];
id obj1 = [Test copy];
id obj2 = [Test mutableCopy];
id obj3 = [obj0 copyWithZone:nil];
id obj4 = [obj0 mutableCopyWithZone:nil];
NSDictionary * dic = @{obj0:@"0",obj1:@"1",obj2:@"2",obj3:@"3",obj4:@"4"};
    
NSLog(@"%p_obj0",obj0);
NSLog(@"%p_obj1",obj1);
NSLog(@"%p_obj2",obj2);
NSLog(@"%p_obj3",obj3);
NSLog(@"%p_obj4",obj4);
NSLog(@"%@_dic",dic);


//打印
NSObject[46855:3785930] category_test_initialize
NSObject[46855:3785930] 0x10235a1d0_obj0
NSObject[46855:3785930] 0x10235a1d0_obj1
NSObject[46855:3785930] 0x10235a1d0_obj2
NSObject[46855:3785930] 0x10235a1d0_obj3
NSObject[46855:3785930] 0x10235a1d0_obj4
NSObject[46855:3785930] {
    Test = 0;
}_dic
關於深拷貝和淺拷貝

深拷貝
產生新對象的情況
淺拷貝
是指未產生新對象的情況(剛纔對類對象的拷貝就是典型的淺拷貝)
簡而言之
只有不可變對象的copy方式,是淺複製,其他都是深複製。
更多可以查閱《iOS基礎深入補完計劃--帶你重識Property》

  • dealloc

當一個對象的引用計數爲0時、系統就會將這個對象釋放。

我們不需要、也不應該主動調用該方法。只需要處置一些不會隨着實例生命週期而變化的事情即可(比如通知、C對象的free)。


類/對象的識別與判等

  • + class

返回類對象

對象的 [someObj class]方法、是NSObject的協議方法

  • + superclass

返回父類對象

  • + hash

通常來講、返回對象的地址(NSObject、UIView)。
對於字符串/字典/數組、根據內容不同可能對內容有不同的hash方式、可以看看《解讀Objective-C中的[NSString hash]方法》

id obj0 = [NSObject new];
id obj1 = [NSObject class];
id obj2 = [NSObject new];
id obj3 = [NSObject class];
id obj4 = [UIView new];
id obj5 = [UIView class];
id obj6 = [NSString new];
id obj7 = [NSString class];
id obj8 = [NSDictionary new];
id obj9 = [NSDictionary class];
id obj10 = [NSArray new];
id obj11 = [NSArray class];


NSLog(@"obj0::%zd_%ld",[obj0 hash],(NSUInteger)obj0);
NSLog(@"obj1::%zd_%ld",[obj1 hash],(NSUInteger)obj1);
NSLog(@"obj2::%zd_%ld",[obj2 hash],(NSUInteger)obj2);
NSLog(@"obj3::%zd_%ld",[obj3 hash],(NSUInteger)obj3);
NSLog(@"obj4::%zd_%ld",[obj4 hash],(NSUInteger)obj4);
NSLog(@"obj5::%zd_%ld",[obj5 hash],(NSUInteger)obj5);
NSLog(@"obj6::%zd_%ld",[obj6 hash],(NSUInteger)obj6);
NSLog(@"obj7::%zd_%ld",[obj7 hash],(NSUInteger)obj7);
NSLog(@"obj8::%zd_%ld",[obj8 hash],(NSUInteger)obj8);
NSLog(@"obj9::%zd_%ld",[obj9 hash],(NSUInteger)obj9);
NSLog(@"obj10::%zd_%ld",[obj10 hash],(NSUInteger)obj10);
NSLog(@"obj11::%zd_%ld",[obj11 hash],(NSUInteger)obj11);


//打印結果
obj0::105827994210384_105827994210384
obj1::4533444264_4533444264
obj2::105827994210416_105827994210416
obj3::4533444264_4533444264
obj4::140577323666816_140577323666816
obj5::4563397296_4563397296
obj6::0_4523287328
obj7::4523970768_4523970768
obj8::0_105553116300640
obj9::4539240872_4539240872
obj10::0_105553116300656
obj11::4539240232_4539240232

hash方法只在對象被添加至NSSet和設置爲NSDictionary的key時會調用

此時他會作爲key的查找以及判等依據避免重複添加

爲了優化判等的效率, 基於hash的NSSet和NSDictionary在判斷成員是否相等時, 會這樣做

Step 1: 集成成員的hash值是否和目標hash值相等, 如果相同進入Step 2, 如果不等, 直接判斷不相等

Step 2: hash值相同(即Step 1)的情況下, 再進行對象判等, 作爲判等的結果

也就是說。我們如果在插入對象之後手動修改了hash值、在進行查找的時候是查找不到滴。

自定義hash插入NSSet/NSDictionay

由於hash只返回對象地址、我們可以通過對象內容進行自定義hash。(特指你希望相同名字和生日不想重複插入這種情況

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}
  • - isEqual

判斷兩個對象內容是否相等、並不只是單純判斷是否爲同一個對象(內存地址)。

自定義對象需要自己實現判等邏輯。

  • - isProxy

判斷對象是否繼承NSProxy

需要注意我們絕大部分的類都繼承與NSObject而非NSProxy
所以絕大部分都會返回No。你可以自己做一個繼承於NSProxy的類來測試。

  • - isKindOfClass

判斷對象是否是指定類或其子類

具體比較的、應該是對象的isa指針。可以看下面的例子:

BOOL a = [NSString isKindOfClass:[NSString class]];
BOOL b = [NSString isKindOfClass:object_getClass([NSString class])];
BOOL c = [UIView isKindOfClass:[UIView class]];
BOOL d = [UIView isKindOfClass:object_getClass([UIView class])];
NSLog(@"a::%d",a);
NSLog(@"b::%d",b);
NSLog(@"c::%d",c);
NSLog(@"d::%d",d);

//打印結果
test[4039:505795] a::0
test[4039:505795] b::1
test[4039:505795] c::0
test[4039:505795] d::1
  • - isMemberOfClass

判斷對象是否是給定類的實例(注意不包含子類)

所比較的、依舊是isa指針。可以自己照上面試試。

需要注意的是:
對於NSString/NSDictionay/NSArray這類類族對象來說。直接用抽象產品(NSString/NSDictionay/NSArray)進行判等、是會失敗的。

對象的 [someObj superclass]方法、是NSObject的協議方法

  • + isSubclassOfClass:

查看一個類對象是否是另一個類對象的子類或者本身

BOOL a = [Test isSubclassOfClass:[Test2 class]];
BOOL b = [Test2 isSubclassOfClass:[Test class]];
BOOL c = [Test isSubclassOfClass:[Test class]];
NSLog(@"a==%d,b==%d,c==%d",a,b,c);

//打印
a==1,b==0,c==1

對對象而言、並沒有能直接比較從屬關係的方法。


類/對象的測試

  • - respondsToSelector:

判斷對象是否能夠調用給定的(對象)方法。(如果用類對象來測試、自然測試的就是類方法咯)

需要注意如果只做了聲明但沒有實現、也是會返回No的。

  • + instancesRespondToSelector:

[類對象]測試(類)方法是否被實現

需要注意如果只做了聲明但沒有實現、也是會返回No的。

所以說respondsToSelector既可以測類方法也可以測實例方法。
instancesRespondToSelector則可以用類對象來測試類方法。

  • + conformsToProtocol:

測試一個類是否遵循了某個協議

主要注意:1、遵循不代表實現。2、遵循不代表必須在.h中聲明。


獲取方法信息

  • - methodForSelector:
  • + instanceMethodForSelector:

分別返回類/對象的某個對象方法以及類方法的實現(IMP)


類/對象的描述

  • + debugDescription

控制檯中打印的信息、就是通過這個方法輸出。

類方法只打印出了類名、實例方法(NSObject協議)可能會打印出更多內容。

  • + description

NSLog、就是通過這個方法輸出。

類方法只打印出了類名、實例方法(NSObject協議)可能會打印出更多內容。


發送消息

  • - performSelector:withObject:afterDelay:

在延遲之後在當前線程上調用某對象的方法。

  • - performSelector:withObject:afterDelay:inModes:

在延遲之後使用指定的Runloop模式當前線程上調用某對象的方法。

  • - performSelectorOnMainThread:withObject:waitUntilDone:

使用在主線程上調用某對象的方法

  • - performSelectorOnMainThread:withObject:waitUntilDone:modes:

使用指定的Runloop模式主線程上調用某對象的方法。

  • - performSelector:onThread:withObject:waitUntilDone:

指定線程上調用某對象的方法

  • - performSelector:onThread:withObject:waitUntilDone:modes:

使用指定的Runloop模式在指定的線程上調用某對象的方法

  • - performSelectorInBackground:withObject:

在新的後臺線程上調用某對象的方法

  • + cancelPreviousPerformRequestsWithTarget:

取消執行某對象先前註冊的所有請求。

  • + cancelPreviousPerformRequestsWithTarget:selector:object:

取消執行某對象先前註冊的指定selector請求。

object參數必須與註冊時相同(並不需要是同一個、但內部會經過isEqual判斷)。

需要注意的是

1. 關於RunloopMode
如果沒有聲明指定的Runloop模式、那麼就會使用默認NSDefaultRunLoopMode
如果(正式發送消息時)當前Runloop模式不匹配、則會等待直到Runloop切換到對應模式。
2. 關於wait參數:
一個布爾值,指定當前線程是否阻塞,直到在主線程上的接收器上執行指定的選擇器之後。指定YES阻止此線程; 否則,指定NO立即返回此方法。
如果當前線程也是主線程,並且您YES爲此參數指定,則會立即傳遞和處理消息。
3. 關於取消
只有在使用afterDelay參數的方法上纔可以工作


動態解析(消息轉發)

如果runtime調用了一個未實現的方法、在崩潰(unrecognized selector)之前會經過一下四步。

  • 解決階段

+ resolveClassMethod:
+ resolveInstanceMethod:

允許嘗試解決這個問題、無論返回YES/NO。(這個我查了很久也沒能找到返回值到底有什麼用)

比如用runtime、爲當前類添加這個方法實現。舉一個官方文檔的例子:


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];
}

官方文檔中還有這樣一句話:
This method is called before the Objective-C forwarding mechanism is invoked. If respondsToSelector: or instancesRespondToSelector: is invoked, the dynamic method resolver is given the opportunity to provide an IMP for the given selector first.
也就是說、這個方法會在啓用消息轉發前小調用。並且動態添加的方法可以被respondsToSelector/instancesRespondToSelector識別。

  • 重定向(Fast Forwarding)

- forwardingTargetForSelector

允許我們爲消息指定一個新的對象進行響應。

如果在前一步你沒能對問題進行解決、runtime允許你將這個消息轉發給一個特定的類。

從文檔規範上來講、你需要這樣實現:

  1. 返回一個非nil以及非self的對象。
  2. 不知道返回啥應該返回super調用(或者乾脆別實現了)
  3. 如果你指向做單純的消息轉發、用這個。反之如果想要做更高級的事(比如修改參數等等)、這個方法做不到。應該用下面的。
  • 消息轉發(Normal Forwarding)

- methodSignatureForSelector
+ instanceMethodSignatureForSelector

正常情況下:通過SEL獲取某個類/對象的對應方法簽名

在消息轉發(決議)的階段:如果返回一個函數簽名,系統就會創建一個NSInvocation對象並調用(下一步)-forwardInvocation:方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

- forwardInvocation:

允許對方法簽名進行轉發(並由該對象嘗試執行)

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Test2 *test2 = [Test2 new];
    if ([test2 respondsToSelector:anInvocation.selector]) {

//        NSString * str;
//        [anInvocation getArgument:&str atIndex:2];
        NSString * str = @"5";
        [anInvocation setArgument:&str atIndex:2];
        
        [anInvocation invokeWithTarget:test2];
        //需要注意的是這裏的invaction是不需要、也不能調用invoke執行的。否則會執行兩次
//        [anInvocation invoke];

    }else {
        [super forwardInvocation:anInvocation];
    }
}
  1. 這裏新的Target對象會嘗試響應該方法。
  2. 如果新的Target對象依舊未實現該方法、會由該對象繼續進行決議(也允許繼續轉發)。
  3. 參數在methodSignatureForSelector返回簽名之後已經自動設置好了。我們只需要指定新的Target便可。
  4. 簽名的返回值將會發回給原調用方。
  • 錯誤處理

- doesNotRecognizeSelector:

處理接收方無法識別的消息

這個方法必須要調用父類實現、不推薦(允許)顛覆。否則將不會拋出錯誤信息。

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    //彈窗啊、打點啊、等等等等
    [super doesNotRecognizeSelector:aSelector];
}

官方提供了一寫應用舉例。當你不允許別人使用某個方法:

- (id)copy/init
{
    [self doesNotRecognizeSelector:_cmd];
}
需要注意的是

除非你從resolveInstanceMethod/resolveClassMethod階段就用runtime添加了方法。不然每一次調用該方法都需要重新走一次消息轉發的過程。


Weak相關

  • - allowsWeakReference:

允許弱引用標量、對於所有allowsWeakReference方法返回NO的類都絕對不能使用__weak修飾符。否則會崩潰。

  • - retainWeakReference

保留弱引用變量、在使用__weak修飾符的變量時、當被賦值對象的retainWeakReference方法返回NO的情況下、該變量將使用“nil” 。


最後

本文主要是自己的學習與總結。如果文內存在紕漏、萬望留言斧正。如果願意補充以及不吝賜教小弟會更加感激。


參考資料

官方文檔 - NSObject_Class
NSObject Class 淺析
iOS類方法load和initialize詳解
ios開發 之 NSObject詳解
iOS基礎(九) - load和initialize的實現原理
NSObject之一
深入理解Objective-C的Runtime機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章