1. 程式人生 > >Effective Objective-C 2.0 筆記 (二)

Effective Objective-C 2.0 筆記 (二)

第15條 用字首避免名稱空間衝突

最好遵循蘋果的程式設計規範,使用 3個字的字首。

對於全域性的變數,常量以及C函式,也應該加上字首。

第16條 提供“全能初始化方法“

這種程式設計模式就是定義一個引數最全的初始化方法,在其中初始化所有的成員變數,其餘的初始化方法都呼叫這個初始化方法。目的是確保所有的成員變數都已經初始化,所有必要的過程都已經呼叫。

下邊是書上的例子:

@implementation EOCRectangle

-(id)initWithWidth:(float)width andHeight:(float)height{
    if(self = [super init]){
        _width = width;
        _height = height;
    }
    return self;
}

-(id)init{
    return [self initWithWidth:5.0f andHeight:10.0f];
}

@end

如果使用者使用預設的init函式初始化,會呼叫全能初始化函式得到一個寬5高10的長方形。這個例子其實不太好,對於矩形這個類來說,預設長寬為0可能是使用者期望的。如果其他類,比如一個人,可能期望的預設名字是@“匿名”或者是@“”,而不是nil。(另外如果有未預料nil,在後續的操作中可能還會引起bug)。

總之,使用全能初始化函式能保證一個物件的所有成員變數預設都是自己設定的值。

在繼承體系中,初始化函式有些複雜。比如有一個正方形類繼承自EOCRectangle:

@implementation EOCSquare

-(id)initWithDimension:(float)dimension{
    return [super initWithWidth:dimension andHeight:dimension];
}

@end

EOCSquare只需要一個引數就可以建立,因此提供了一個額外的初始化函式initWithDimension,在這個初始化函式中呼叫了基類ECORectangle的全能初始化函式。

但是使用者在初始化的時候,仍然可以使用ECORectangle的initWithWidth:andHeight:方法,還有init方法。這樣就可以創建出長和寬不等的正方形了。為了避免這種情況出現,又有了如下規則:

子類應該覆蓋基類的全部全能初始化函式

一個可能的實現是:

-(id)initWithWidth:(float)width andHeight:(float)height{
    float dimension = MAX(width, height);
    return [super initWithWidth:dimension andHeight:dimension];
}

如果在EOCSquare上呼叫init,此時self指向的是EOCSquare,因此會呼叫EOCSquare的initWithWidth:andHeight:方法。所以子類就不需要覆蓋基類的init方法了。

上邊的這套邏輯在Swift語言中可以通過編譯器保證。

第17條 實現description方法

description和debugDescription是NSObject上的方法。自定義物件實現前者可以通過NSLog列印,實現後者可以實現在控制檯po。

第18條 儘量使用不可變物件

這也是遵循了程式設計中的許可權最小原則,使用不可變物件的好處顯然是簡單,你能確定這個值不會被修改。還有一個額外的好處就是在多執行緒環境下,不可變的物件可以簡化程式設計難度。

有幾個點:

1)如果物件的屬性對外只讀,但是內部可以修改,可以在擴充套件中使用readwrite改寫。也可以在內部使用可變物件,對外提供一份不可變的copy。

2)readonly不能阻止外界使用KVC的方式改變

3)readonly更不能阻止外界通過執行時獲取變數地址的方式改變

第19條 使用清晰而協調的命名方式

起名這種東西,是程式設計最難的部分之一,不可能通過一個條目學會,只能多看規範的程式碼。

第20條 為私有方法名加字首

為私有方法加上字首,以便於區分,但是不要使用下劃線作為字首。因為OC的機制,下劃線可能覆蓋了蘋果自己的方法。

第21條 理解Objective-C錯誤模型

Objective-C一般不使用異常機制傳遞錯誤,這是因為ARC機制不是異常安全的,在丟擲異常的時候會產生記憶體洩漏。如果想讓ARC程式碼支援異常,需要開啟編譯器的-fobjc-arc-exceptions標誌。但是這會產生額外的程式碼,即使沒有發生異常也會執行。

Objective-C的錯誤處理程式設計正規化是傳遞NSError。一個函式如果可能發生錯誤,就返回一個BOOL值,然後通過引數返回的方式檢查錯誤:

-(BOOL)fooWithParams:(NSDictionary *)params error:(NSError **)error{
    //if some error happen
    *error = [NSError errorWithDomain:@"test" code:-1 
              userInfo:@{NSLocalizedDescriptionKey: @"this is a test error"}];
    return YES;
}


-(void)useFoo{
    NSError *error = nil;
    BOOL res = [self fooWithParams:nil error:&error];
    if(!res){
        NSLog(@"error happend: %@", error.localizedDescription);
    }
}

如果是非同步過程,則通過代理返回一個error引數來傳遞錯誤。一般代理的協議中會有這樣的方法簽名。

-(void)finishedWithData:(NSData *)data withError:(NSError *)error;

第22條 理解NSCopying協議

NSCopying協議裡只有一個方法:

-(id)copyWithZone:(NSZone: )zone;

如果我們自定義的類需要實現copy的方法,宣告遵守NSCopying協議並實現這個方法就行了。

一個例子:

@interface EOCPerson : NSObject<NSCopying>

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

-(id)initWithFirstName:(NSString *)firstName
           andLastName:(NSString *)lastName;

@end


@implementation EOCPerson{
    NSMutableSet *_friends;
}

//其他實現

-(id)copyWithZone:(NSZone *)zone{
    EOCPerson *copy = [[[self class] allocWithZone:zone]
                       initWithFirstName:_firstName
                       andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
    return copy;
}

@end

1)zone是歷史遺留的引數,現在已經不需要理解,照樣傳過去就行了。實際上不使用allocWithZone初始化函式,直接用alloc也不會有什麼問題。

2)[self class]是一個要注意的地方,不要使用EOCPerson,因為如果有子類繼承了EOCPerson,比如EOCTeacher,EOCTeacher也繼承了copy方法,如果此處使用EOCPerson,那麼EOCTeacher copy出來的就是一個EOCPerson,這不合語義。

3)在copyWithZone中對可變的集合型別的成員,要進行可變的copy,否則copy出來的物件不獨立。對於不可變的,不用copy,反正也沒有辦法變化,兩個物件公用一個還節省記憶體。

實際上,不可變物件的copy並不複製記憶體,而是直接返回自己。這點可以通過列印物件地址來驗證。因此,即使用了copy也沒有關係。

第23條 通過委託與資料來源協議進行物件間通訊

委託模式有幾種應用場景: 1)元件內部有些事情不能確定,需要使用者來提供。比如UITableView cell的高度。

2)不應該由元件負責的功能,比如元件內部事件的響應,如UITableView cell的點選響應。

3)非同步事件的回撥,如網路請求完成後的回撥。

使用委託模式的注意:

1)代理物件的弱引用,避免迴圈引用

2)在給代理物件傳送訊息之前判斷代理物件是否響應這個訊息。

技巧:

如果代理方法呼叫頻繁,可能判斷物件是否響應方法是一個耗時的操作,如果確定瓶頸在這,可以快取代理物件的判斷結果。書中介紹了一種使用C結構體快取方法。

第24條 將類的實現程式碼分散到便於管理的數個分類之中

Objective-C沒有私有方法機制,可以使用一個Private的分類來管理私有方法,增加程式碼的可讀性。除此之外,分類還可以用於將方法分門別類,介面更好讀。

第25條 總是為第三方類的分類名稱加字首

這是為了解決Objective-C沒有名稱空間產生的程式設計正規化。避免衝突。

第26條 勿在分類中宣告屬性

Objective-C的分類設計目的是擴充套件功能,而不是封裝資料。因此在分類中宣告的屬性不會被自動合成,需要使用關聯物件的方式,或者動態解析的方式來新增。如果有其他更正規的設計可以達到目的,儘量不要用。

第27條 使用“class-continueation”分類隱藏實現細節

就是每次Xcode建立viewController之後.m檔案中的匿名分類,也叫擴充套件。這裡可以放置"私有"成員。也可以修改.h檔案中的readonly修飾符。

還有一個應用場景,如果類的實現中使用了C++程式碼,如果在標頭檔案中聲明瞭C++的類,那麼要求使用者使用C++編譯器編譯程式碼,如果放在.mm檔案中,對外可以隱藏C++細節。

第28條 通過協議提供匿名物件

這就是協議的使用方式,面向介面程式設計。通過id來使用匿名物件,即不關係物件是什麼,只要實現了這個協議就可以使用。

下邊是NSMutableDictionary的setObject:forKey介面:

- (void)setObject:(ObjectType)anObject forKey:(id<NSCopying>)aKey;

其中,key在設定的時候是要copy一份的,因此要求key服從NSCopying協議,至於具體是什麼型別,不用關心。

第29條 理解引用計數 & 第30章 以ARC簡化引用計數

ARC 管理記憶體的方式是自動的插入記憶體管理程式碼,但是機制仍然是引用計數。

在ARC下,retain,release,autorelease,dealloc都是不能主動呼叫的,因為會干擾ARC新增這些記憶體管理的程式碼。

使用ARC必須遵循方法的命名規則:

alloc,new,copy,mutableCopy,以這4個開頭的方法都會返回一個物件,並且物件的所有權歸呼叫者。否則,返回的物件呼叫者不用管。實際上,在ARC下,從來不需要操心一個函式返回物件的記憶體釋放問題,主要關心的是迴圈引用。

使用ARC需要注意一點:CoreFoundation中的物件ARC不負責,因此需要自己手動管理記憶體。所以會經常看到CFRelease(XXX)

第31條 在dealloc方法中只釋放引用並解除監聽

在dealloc中應該做的事情有:

1)釋放CoreFoundation的物件

2)取消對訊息中心的監聽

3)取消KVO的監聽,是指取消對其他物件的監聽。如果有其他物件監聽自己,可以不用管。

4)釋放資源,如資料庫連線,套接字連線等。這些資源的釋放應該給出一個獨立的方法,這樣在需要的時候可以提前釋放,而不必等到dealloc再釋放。

5)在dealloc中不應該做任何業務的事情,不應該呼叫物件屬性(因為屬性可能處於KVO監聽中)。只做和釋放資源有關的事情。

6)dealloc不能確保在哪個執行緒被呼叫,因此,不要假定執行在主執行緒中。

第32條 編寫“異常安全程式碼”時留意記憶體管理問題

參見第21條

1)純OC程式碼不應該使用異常,如果使用,也是在程式無法繼續執行的時候使用。之後程式就崩潰了。

2)一般和C++混編的程式碼會使用異常,這時候應該開啟編譯選項-fobjc-arc-exceptions。

第33條 以弱引用避免保留環

引用計數和垃圾回收的一個區別就是,引用計數無法釋放環狀的引用孤島。應該使用weak來保證沒有迴圈引用。還有一個關鍵字unsafe_unretained,和weak的區別是,weak在指向的物件釋放後,自動將指標指向nil,更加安全,因此能使用weak的時候應該使用weak。有些型別不支援weak,才使用unsafe_unretained。

第34條 以”自動釋放池塊“降低記憶體峰值

書中給出的例子是一個for迴圈裡邊建立了很多臨時物件,如果不使用@autoreleasepool塊,記憶體峰值會比較高。但是我自己實驗得出的結論不一樣:

@implementation BigMemObj{
    NSMutableArray *_mutArr;
}

-(instancetype)init{
    if(self = [super init]){
        _mutArr = [[NSMutableArray alloc] initWithCapacity:1024*1024*30];
        for(int i = 0; i < 1024*1024*30; i++){
            [_mutArr addObject:@(i)];
        }
    }
    
    return self;
}
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    for(int i = 0 ; i < 10000; i++){
        @autoreleasepool {
            BigMemObj *mem = [[BigMemObj alloc] init];
            NSLog(@"%@", mem);
        }
    }
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    for(int i = 0 ; i < 10000; i++){
            BigMemObj *mem = [[BigMemObj alloc] init];
            NSLog(@"%@", mem);
    }

定義一個類,建立例項的時候分配30M個數字物件,新增到陣列中。第二段程式碼是使用了@autoreleasepool的,第三段沒有使用。下邊是執行一分鐘的記憶體使用情況,實驗手機是iPhone6s。

使用autoreleasepool的情況: 在這裡插入圖片描述

不使用autoreleasepool的情況:

在這裡插入圖片描述

發現不使用autoreleasepool的記憶體峰值反而更小。

在StackOverFlow上提問得到的回到是在ARC下不需要使用@autoreleasepool塊:

第35條 用“殭屍物件”除錯記憶體管理問題

使用殭屍物件除錯,主要處理向已經釋放了的物件傳送訊息引起的Crash問題。在ARC環境下,Foundation的物件已經很少出現這種問題。

開關在這裡: 在這裡插入圖片描述

在物件即將被系統回收時,如果發現打開了zombie objects開關,那麼就不回收這個物件,將這個物件轉化成為殭屍物件。如果後續程式碼向殭屍物件傳送訊息,則列印一條警告。

殭屍物件的原理是通過執行時建立一個殭屍類,然後修改物件的isa指標指向這個殭屍類,這樣這個物件的型別就在執行時改變了,這個殭屍類沒有實現任何方法,只是通過message forwarding來響應傳送過來的訊息。

第36條 不要使用retainCount

ARC環境下已經不能使用了,會編譯報錯。