Effective Objective-C 2.0 學(xué)習(xí)筆記

1. 理解objc_msgSend

objc_msgSend函數(shù)會依據(jù)接受者與選擇子的類型來調(diào)用適當(dāng)?shù)姆椒?。為了完成次操作,改方法需要在接受者所屬的類中搜尋起“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就調(diào)到其實(shí)現(xiàn)代碼。若是找不到,那就沿著繼承體系繼續(xù)向上查找,等找到合適的方法之后再跳轉(zhuǎn)。如果最終還是找不到相符的方法,那就執(zhí)行“消息轉(zhuǎn)發(fā)”(message forwarding)操作。

  • objc_msgSend_stret:如果待發(fā)送的消息要返回結(jié)構(gòu)體,那么可交由此函數(shù)處理。只有當(dāng)CPU的寄存器能夠容納得下消息返回類型時,這個函數(shù)才能處理此消息。若是返回值無法容納于CPU寄存器中(比如說返回的結(jié)構(gòu)體太大了),那么就由另一個函數(shù)執(zhí)行派發(fā)。此時,那個函數(shù)會通過分配在棧上的某個變量來處理消息所返回的結(jié)構(gòu)體。
  • objc_msgSend_fpret:如果消息返回的是浮點(diǎn)數(shù),那么金額交由次函數(shù)處理。
  • objc_msgSendSuper:如果要給超類發(fā)消息,例如[super message:parameter],那么就交由次函數(shù)處理。也有另外兩個于objc_msgSend_stret和objc_msgSend_fpret等效的函數(shù),用于處理發(fā)送給super的相應(yīng)消息。

2. 消息轉(zhuǎn)發(fā)

如果在控制臺中看到下面這種提示信息,那就說明你曾向某個對象發(fā)送過一條其無法解讀的消息,從而啟動了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認(rèn)實(shí)現(xiàn)。

unrecognized selector sent to instance 0x87

上面這段異常信息是由NSObject的“doesNotRecognizeSelector:”方法所拋出的。

消息轉(zhuǎn)發(fā)分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動態(tài)添加方法,以處理當(dāng)前這個“未知的選擇子”(unknown selector),這叫做“動態(tài)方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”(full forwarding mechanism)。如果運(yùn)行期系統(tǒng)以及把第一階段執(zhí)行完了,那邊接收者自己就無法再以動態(tài)新增方法的手段來響應(yīng)包含該選擇子的消息了。此時,運(yùn)行期系統(tǒng)會請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用。這又細(xì)分為2小步。首先,請接收者看看有沒有其他對象能處理這條消息。若有,則運(yùn)行期系統(tǒng)會把消息轉(zhuǎn)給那個對象,于是消息轉(zhuǎn)發(fā)過程結(jié)束,一切如常。若沒有“被援的接收者”(replacement receiver),則啟動完整的消息轉(zhuǎn)發(fā)機(jī)制,運(yùn)行期系統(tǒng)會把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象中,再給接收者最后一次機(jī)會,令其設(shè)法解決當(dāng)前還未處理的這條消息。

動態(tài)方法解析

對象在接收到無法解讀的消息后,首先將調(diào)用其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector
備援接收者

當(dāng)前接收者還有第二次機(jī)會能處理位置的選擇子,在這一步中,運(yùn)行期系統(tǒng)會問它:能不能把這條消息轉(zhuǎn)給其他接收者來處理。與該步驟對應(yīng)的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector
完整的消息轉(zhuǎn)發(fā)

如果轉(zhuǎn)發(fā)算法已經(jīng)來到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。首先創(chuàng)建NSInvocation對象,把與尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封裝在其中,此對象包括選擇子,目標(biāo)(target)及參數(shù)。在觸發(fā)NSInvocation對象時,消息派發(fā)系統(tǒng)(message-dispatch-system)將親自出馬,把消息指派給目標(biāo)對象。

此步驟會調(diào)用下列方法來轉(zhuǎn)發(fā)消息:

-(void)forwardInvocation:(NSInvocation *)invovation

這個方法可以實(shí)現(xiàn)的很簡單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來的方法與“備援接收者”方案所實(shí)現(xiàn)的方法等效,所以很少有人采用這么簡單的實(shí)現(xiàn)方式。比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另外一個參數(shù),或者改變選擇子等。

forwarding_flow.png

3. 用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”

類的方法列表會把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)上,使得“動態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來表示,這種指針叫做IMP,其原型如下:

id (*IMP)(id, SEL, ...)

isKindOfClassisMemberOfClass這樣的類型信息查詢方法原理是使用了isa指針獲取對所屬的類,然后通過super_class指針在繼承體系中游走。

4. 提供“全能初始化方法”

  • 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應(yīng)調(diào)用此方法。
  • 若全能初始化方法與超類不同,則需覆寫超類中的對應(yīng)方法。
  • 如果超類的初始化方法不適用于子類,那邊應(yīng)該覆寫這個超類方法,并在其中拋出異常。

5. 實(shí)現(xiàn)description方法

我們有時需要更為有用的信息, 只需要覆寫description方法并將描述次對象的信息返回即可。NSObject協(xié)議中還有個方法:debugDescription,此方法與description非常相似。二者的區(qū)別在于,debugDescription方法是開發(fā)者在調(diào)試器中以控制臺命令打印對象時才調(diào)用的,即當(dāng)運(yùn)行到斷點(diǎn)時,你使用LLDB的“po”命令打印輸出的內(nèi)容就是debugDescription。在NSObject類的默認(rèn)實(shí)現(xiàn)中,此方法只是直接調(diào)用了description。

6. 理解NSCopying協(xié)議

使用對象時經(jīng)常需要拷貝它。在OC中,此操作通過copy方法完成。如果想令自己的類支持拷貝操作,那就要實(shí)現(xiàn)NSCopying協(xié)議,該協(xié)議只有一個方法:

- (id)copyWithZone:(NSZone *)zone

為何會出現(xiàn)NSZone呢?因?yàn)樵谝郧伴_發(fā)程序時,會據(jù)此吧內(nèi)存分成不同的“區(qū)”(zone),而對象會創(chuàng)建在某個區(qū)里面?,F(xiàn)在不用了,每個程序只有一個區(qū):“默認(rèn)區(qū)”(default zone)。所以說,盡管必須實(shí)現(xiàn)這個方法,但是你不必?fù)?dān)心其中的zone參數(shù)。

若想使某個類支持拷貝功能,只需聲明該類遵從NSCopying協(xié)議,并實(shí)現(xiàn)其中的該方法。

  • 若想令自己所寫的對象具有拷貝功能,則需實(shí)現(xiàn)NSCopying協(xié)議。
  • 如果自定義的對象分為可變版本與不可變版本,那么就要同時實(shí)現(xiàn)NSCopying和NSMutableCopying協(xié)議。
  • 復(fù)制對象時需決定采用淺拷貝還是深拷貝,一般情況下應(yīng)該盡量執(zhí)行淺拷貝。
  • 如果你縮寫的對象需要深拷貝,那么可考慮新增一個專門執(zhí)行深拷貝的方法。

7. 內(nèi)存管理

ARC如何清理實(shí)例變量:

ARC會借用Objective-C++的一項(xiàng)特性來生成清理例程(cleanup routine)?;厥誒bjective-C++對象時,待回收的對象會調(diào)用所有C++對象的析構(gòu)函數(shù)(destructor)。編譯器如果發(fā)現(xiàn)某個對象里含有C++對象,就會生成名為.cxx_destruct的方法。而ARC則借助此特性,在該方法中生成清理內(nèi)存的代碼。

不過如果有非Objective-C的對象,比如CoreFoundation中的對象或是由malloc()分配在堆中的內(nèi)存,那么仍然需要清理。然而不需要像原來那樣調(diào)用超類的dealloc方法。ARC會自動在.cxx_destruct方法中生成代碼并運(yùn)行此方法,而在生成的代碼中會自動調(diào)用超類的dealloc方法。ARC環(huán)境下,dealloc方法可以如下寫:

- (void)dealloc{
  CFRelease(_coreFoundationObject);
  free(_heapAllocatedMemoryBlob);
}

以autoreleasepool降低內(nèi)存峰值:

通常,系統(tǒng)會自動創(chuàng)建一些線程,比如主線程或者GCD中的線程,默認(rèn)都有自動釋放池,每次執(zhí)行“事件循環(huán)”(event loop)時,就會將其清空。

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databseRecords) {
    @autoreleasepool {
        TTPerson *person = [[TTPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

8.GCD

block的內(nèi)部結(jié)構(gòu)

每個OC對象都占據(jù)著某個內(nèi)存區(qū)域,因?yàn)閷?shí)例變量的個數(shù)及對象所包含的關(guān)聯(lián)數(shù)據(jù)互不相同,所以每個對象所占的內(nèi)存區(qū)域也有大有小。block本身也是對象,在存放塊對象的內(nèi)存區(qū)域中,首個變量是指向Class對象的指針,該指針叫做isa。其余內(nèi)存中含有塊對象正常運(yùn)轉(zhuǎn)所需的各種信息。


block.png

在內(nèi)存布局中,最重要的就是invoke變量,這是個函數(shù)指針,指向塊的實(shí)現(xiàn)代碼。函數(shù)原型至少要接收一個void*型的參數(shù),此參數(shù)代表block(block其實(shí)就是一種代替函數(shù)指針的語法結(jié)構(gòu)),原來使用函數(shù)指針時,需要用“不透明的void指針”來傳遞狀態(tài)。而改用block之后,則可以把原來用標(biāo)準(zhǔn)C語言特性所編寫的代碼封裝成簡明易用的接口。

descriptor變量是指向結(jié)構(gòu)體的指針,每個block中都含有此結(jié)構(gòu)體,其中聲明了block對象的總體大小,還聲明了copy與dispose這兩個輔助函數(shù)所對應(yīng)的函數(shù)指針。輔助函數(shù)在拷貝及丟棄block對象時運(yùn)行,其中會執(zhí)行一些操作,比如,前者要保留捕獲的對象,而后者則將之釋放。

block還會把它所捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后面,捕獲了多少個變量,就要占據(jù)多少內(nèi)存空間。注意:拷貝的并不是對象本身,而是指向這些對象的指針變量。invoke函數(shù)為何需要把block對象作為參數(shù)傳進(jìn)來呢?原因在于,執(zhí)行block時要從內(nèi)存中把這些捕獲到的變量讀出來。

全局塊,棧塊及堆塊

定義block時,起占有的內(nèi)存區(qū)域是分配在棧中的。這就是說,block只在定義他的那個范圍內(nèi)有效。

例如,如下代碼就是危險的:

  void (^block)(void);
    if (XXX) {
        block = ^{
            NSLog(@"block A");
        };
    }else{
        block = ^{
            NSLog(@"block B");
        };
    }
    block();

因?yàn)槎x在if及else語句中的兩個塊都分配在棧內(nèi)存中。編譯器會給每個block分配好棧內(nèi)存,然而等離開了想要的范圍之后,編譯器有可能會把分配給快的內(nèi)存覆蓋掉。于是,這兩個block只能保證在對應(yīng)的if或else語句范圍內(nèi)有效,這樣寫的代碼可以編譯,但是運(yùn)行起來若編譯器覆寫了待執(zhí)行的block則程序會崩潰。

為解決此問題,可給block對象發(fā)送copy消息拷貝之。這樣就可以把block從棧復(fù)制到堆了??截惡蟮腷lock可以在定義他的那個范圍之外使用,而且一旦復(fù)制到堆上,塊就成了帶引用計數(shù)的對象了,后續(xù)的復(fù)制操作都不會真的執(zhí)行復(fù)制,二十遞增block對象的引用計數(shù)。

   void (^block)(void);
    if (1) {
        block = [^{
            NSLog(@"block A");
        } copy];
    }else{
        block = [^{
            NSLog(@"block B");
        } copy];
    }
    block();

全局塊(global block):這種塊不會捕捉任何狀態(tài)(比如外圍的變量等),運(yùn)行時也無需有狀態(tài)來參與。塊所使用的整個內(nèi)存區(qū)域,在編譯期就已經(jīng)完全確定了,因此,全局塊可以生命在全局內(nèi)存中,而不需要在每次用到的時候在棧中創(chuàng)建,另外,全局塊的拷貝操作是個空操作,因?yàn)槿謮K不可能被系統(tǒng)回收,這種塊相當(dāng)于單例,如下:

    //全局block
    void (^globalBlock)() = ^{
        NSLog(@"globalBlock");
    };

9. 用handler塊降低代碼分散程度

異步方法在執(zhí)行完任務(wù)之后,需要以某種手段通知相關(guān)代碼。實(shí)現(xiàn)此功能有很多辦法。常用的技巧是設(shè)計一個委托協(xié)議,令關(guān)注此事件的對象遵從改協(xié)議。對象成為delegate之后,就可以在相關(guān)事件發(fā)生時得到通知了。例如:

@class TTNetworkFetcher;

@protocol TTNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(TTNetworkFetcher *)networkFetcher
     didFinishWithData:(NSData *)data;
@end

@interface TTNetworkFetcher : NSObject
@property (nonatomic, weak) id <TTNetworkFetcherDelegate> delegate;
- (id)initWithURL:(NSURL *)url;
- (void)start;

我們也可以把completion handler定義為塊類型,將其當(dāng)做參數(shù)直接傳給start方法:

typedef void(^TTNetworkFetcherCompletionHandler)(NSData *data);

@interface TTNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(TTNetworkFetcherCompletionHandler)handle;

//.m
//3.用handler塊降低代碼分散程度
    NSURL *url = [[NSURL alloc] initWithString:@"XXX"];
    TTNetworkFetcher *fetcher = [[TTNetworkFetcher alloc] initWithURL:url];    
    [fetcher startWithCompletionHandler:^(NSData *data) {
        
    }];

與使用委托模式的代碼相比,用塊寫出了的代碼顯然更加整潔。異步任務(wù)執(zhí)行完畢后所需運(yùn)行的業(yè)務(wù)邏輯,和啟動異步任務(wù)所用的代碼放在了一起。而且,由于塊聲明在創(chuàng)建獲取器的范圍呢,所以他可以訪問此范圍內(nèi)的全部變量。

委托模式有個缺點(diǎn),如果類要分別使用多個獲取器下載不同數(shù)據(jù),那么就得在delegate回調(diào)方法里根據(jù)傳入的獲取器參數(shù)來切換。代碼寫法如下:

- (void)initXXX {
    NSURL *url1 = [[NSURL alloc] initWithString:@"XXX"];
    _fetcher1 = [[TTNetworkFetcher alloc] initWithURL:url1];
    _fetcher1.delegate = self;
    [_fetcher1 start];
    
    NSURL *url2 = [[NSURL alloc] initWithString:@"XXX"];
    _fetcher2 = [[TTNetworkFetcher alloc] initWithURL:url2];
    _fetcher2.delegate = self;
    [_fetcher2 start];
}

- (void)networkFetcher:(TTNetworkFetcher *)networkFetcher didFinishWithData:(NSData *)data{
    if (networkFetcher == _fetcher1) {
        //XXX = data;
        _fetcher1 = nil;
    }
    else if (networkFetcher == _fetcher2){
        //data handler
        _fetcher2 = nil;
    }
    //etc.
}

這么寫代碼,不僅會令delegate回調(diào)方法變的很長,而且還要把網(wǎng)絡(luò)數(shù)據(jù)獲取器對象保存為實(shí)例變量,以便在判斷語句中使用。這么做可能有其他原因,比如稍后要根據(jù)情況解除監(jiān)聽等,然而這種寫法有副作用,通常很快就會使類的代碼激增。改用塊來寫的好處是:無須保存獲取器,也無須在回調(diào)方法里切換,每個completion handler的業(yè)務(wù)邏輯,都是和相關(guān)的獲取器對象一起來定義的。另外我們可以把處理成功情況和失敗情況放在一個方法中。同時也需要注意循環(huán)引用的問題。

10. 多用派發(fā)隊列,少用同步鎖

濫用@synchronized(self)會很危險,因?yàn)樗型綁K都會彼此搶奪同一個鎖。要是有很多歌屬性都這么寫的話,那么每個屬性的同步塊都要等其他所有同步塊執(zhí)行完畢才能執(zhí)行。而且這樣做也不是絕對安全的,如果多線程同時操作屬性,那么取值時可能已經(jīng)是其他線程寫入的新的屬性值了。

可以使用“串行同步隊列”代替同步塊或鎖對象。將讀取操作及寫入操作都安排在同一個隊列里,即可保證數(shù)據(jù)同步。用法如下:

@synthesize someString = _someString;

- (void)viewDidLoad {
    [super viewDidLoad];
    //4.多用派發(fā)隊列,少用同步鎖
    _syncQueue = dispatch_queue_create("com.turkeyteo.syncQueue", NULL);
    
}

- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString{
    //設(shè)置方法并不一定非得同步,這里可使用異步能提高執(zhí)行速度。注意:如果只是執(zhí)行很簡單的操作,改用異步不見得會比同步快,因?yàn)閳?zhí)行異步派發(fā)時,需要拷貝塊,拷貝也是需要花費(fèi)時間的。
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

我們也可以在并行隊列中使用柵欄(barrier)來實(shí)現(xiàn)同步。串行隊列本來就是按順序執(zhí)行的,所以使用它沒有意義。使用如下:

_syncQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString2{
    __block NSString *localSomeString;
    dispatch_sync(_syncQ, ^{
        localSomeString = _someString2;
    });
    return localSomeString;
}

- (void)setSomeString2:(NSString *)someString2{
    dispatch_barrier_async(_syncQ, ^{
        _someString2 = someString2;
    });
}

使用柵欄性能會比使用串行隊列要快。因?yàn)槠潆m然寫入操作必須單獨(dú)執(zhí)行,但是讀取操作可以并行,相比執(zhí)行就更加高效了。

11. 系統(tǒng)架構(gòu)

將一系列代碼封裝為動態(tài)庫(dynamic library),并在其中放入描述其接口的頭文件,這樣做出來的東西就叫框架。在開發(fā)“圖形界面的應(yīng)用程序”(graphical application)時,會用到名為Cocoa的框架,在iOS上稱為Cocoa Touch。其實(shí)Cocoa本身并不是框架,但是里面集成了一批創(chuàng)建應(yīng)用程序時經(jīng)常會用到的框架。

iOS的基石是Foundation框架,他提供了collection等核心功能和字符串處理等復(fù)雜功能。還有個與Foundation相伴的框架,叫做CoreFoundation。Foundation框架中的許多功能,都可以在此框架中找到對應(yīng)的C語言API。其中有個功能叫做“無縫橋接”(toll-free bridging),可以把CoreFoundation中的C語言數(shù)據(jù)結(jié)構(gòu)平滑轉(zhuǎn)換為Foundation中的OC對象,也可以反向轉(zhuǎn)換。比如NSString可以轉(zhuǎn)到CoreFoundation中與之等效的CFString對象。

CFNetwork:提供了C語言級別的網(wǎng)絡(luò)通信能力,它將“BSD套接字”抽象成易于使用的網(wǎng)絡(luò)接口。而Foundation則將其部分內(nèi)容封裝成OC接口以便于網(wǎng)絡(luò)通信。例如可以用NSURLConnection從URL中下載數(shù)據(jù)。

12. 構(gòu)建緩存時選用NSCache而非NSDictionary

NSCache勝過NSDictionary之處自傲與,當(dāng)系統(tǒng)資源將要耗盡時,他可以自動刪減緩存。如果采用普通的字段,那么叫自己編寫掛鉤,在系統(tǒng)低內(nèi)存通知時手工刪減緩存。此外,NSCache還會先行刪減“最久未使用的”對象。

NSCache并不會“拷貝”鍵,而是“保留”它,NSCache對象不拷貝鍵的原因在于:很多時候,鍵都是有不支持拷貝操作的對象來充當(dāng)?shù)?。因此,NSCache不會自動拷貝鍵,所以說,在鍵不支持拷貝操作的情況下,該類使用起來比字典更方便。另NSCache是線程安全的。緩存的時候線程安全很重要,你可能在某個線程中讀取數(shù)據(jù),此時如果發(fā)現(xiàn)緩存里沒有指定的鍵,那么就要去下載該鍵對應(yīng)的數(shù)據(jù),而下載完數(shù)據(jù)之后所要執(zhí)行的回調(diào)函數(shù),有可能會在后臺線程中運(yùn)行,這樣就是另外一個線程在寫入緩存了。

13. 精簡initialize與load的實(shí)現(xiàn)代碼

有時候,類必須先執(zhí)行某些初始化操作。load方法,對于加入運(yùn)行期系統(tǒng)中的每個類(class)和分類(category)來說,必定會調(diào)用此方法,而且只會調(diào)用一次。

load方法的問題在于,執(zhí)行該方法時,運(yùn)行期系統(tǒng)處于“脆弱狀態(tài)”(fragile state)。在執(zhí)行子類的load方法之前,必定會先執(zhí)行所有超類的load方法。而如果代碼還依賴了其他程序庫,那么程序庫里相關(guān)類的load方法也必定會先執(zhí)行。然而,根據(jù)某個給定的程序庫,卻無法判斷出其中各個類的載入書序。因此,在load方法中使用其他類是不安全的。

而且load方法務(wù)必實(shí)現(xiàn)的精簡一些,也就是要盡量減少其所執(zhí)行的操作,因?yàn)檎麄€應(yīng)用程序在執(zhí)行l(wèi)oad方法時都會阻塞。

想要執(zhí)行與類相關(guān)的初始化操作,可以覆寫+(void)initialize ,對于每個類來說,該方法會在程序首次用該類之前調(diào)用,且只調(diào)用一次。它是由運(yùn)行期系統(tǒng)來調(diào)用的,絕不應(yīng)該通過代碼直接調(diào)用。它與load方法相似,但是有幾個區(qū)別。首先,它是“惰性調(diào)用的”,也就是說,只有當(dāng)傳給你下用到相關(guān)的類時才調(diào)用。如果某個類一直沒有使用,那么其initialize方法就一直不會運(yùn)行。也就是等于說,應(yīng)用程序無需先把每個類的initialize都執(zhí)行一遍,這與load方法不同,對于load必須阻塞兵等所有類的load都執(zhí)行完成,應(yīng)用程序才繼續(xù)。另一點(diǎn),運(yùn)行期系統(tǒng)在執(zhí)行該方法時,是處于正常狀態(tài)的,因此,此時可以安全使用并調(diào)用任意類中的任意方法。而且是線程安全的,也就是說,再有執(zhí)行initialize的那個線程可以操作類或者類實(shí)例,其他線程都要先阻塞,等著initialize執(zhí)行完。最后一個區(qū)別是:initialize方法與其他消息一樣,如果某個類未實(shí)現(xiàn)它,而其超類實(shí)現(xiàn)了,那么就會運(yùn)行超類的實(shí)現(xiàn)代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容