Effective Objective-C讀後感

一、熟悉OC

1、瞭解OC語言的起源

該語言使用“消息結構”(message structure)。
OC是C的“超類”(superset)。

2、在類的頭文件中儘量少引入其他頭文件

  • 除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明來提及別的類,並在實現文件中引入那些類的頭文件。這樣做可以儘量降低類之前的耦合(coupling)。

向前聲明(forward declaring) @class xxxx;(也可以解決兩個類相互引用問題)。

將引入頭文件的時機儘量延後,只在確有需要時才引入,這樣就可以減少累的使用者所需引入的頭文件數量。

  • 疑問
    減少編譯時間,但是別的類如果需要使用該class,必須再次引用,是否增加了工作量?

  • 有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況下,儘量把“該類遵循某協議”的這條聲明移至“class-continuation分類”中。如果不行的話,就把協議單獨放在一個頭文件中,然後將其引入。

若因爲要實現屬性、實例變量或者要遵循協議而必須引入頭文件,則應儘量將其移至“class-continuation分類”。

3、多用字面量語法,少用與之等價的方法

  • 應該使用字面量語法來創建字符串、數值、數組、字典。與創建此類對象的常規方法相比,這麼做更加簡明扼要。
  • 應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
  • 用字面量語法創建數組或字典時,若值中有nil,則會拋出異常。因此,務必確保值裏不含nil。

4、多用類型常量,少用#define預處理命令

  • 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前根據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量值不一致。
  • 在實現文件中使用 static const來定義“只在編譯單元內可見的常量”(translation-unit-specific constant)。由於此類常量不在全局符號表中,所以無須爲其名稱加前綴。
  • 在頭文件中使用extern來聲明全局常量,並在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴。

常用的命名法是:
若常量侷限於某“編譯單元”(translation unit,也就是“實現文件”,implementation file)之內,則在前面加字母k;若常量在類之外可見,則通常以類名爲前綴。

5、用枚舉表示狀態、選項、狀態碼

  • 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
  • 如果把傳遞給某個方法的選擇表示爲枚舉類型,而多個選項又可同時使用,那麼就將各選項值定義爲2的冪,以便通過按位或操作將其組合起來。
  • 用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,並指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會才用編譯器所選的類型。
  • 在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之後,編譯器就會提示開發者:switch語句並未處理所有枚舉。

二、對象、消息、運行期

用OC等面嚮對象語言編程時,“對象”(object)就是“基本構造單元”(building block),開發者可以通過對象來存儲並傳遞數據。

在對象之間傳遞數據並執行任務的過程就叫做“消息傳遞”(Messaging)。

當應用程序運行起來以後,爲其提供相關支持的代碼叫做“OC運行期環境”(Objective-C runtime),它提供了一些使得對象之間能夠傳遞消息的重要函數,並且包含創建類實例所用的全部邏輯。

6、理解“屬性”這一概念

  • 可以用@property語法來定義對象中所封裝的數據。
  • 通過“特質”來指定存儲數據所需的正確語義。
  • 在設置屬性所對應的實例變量時,一定要遵守該屬性所聲明的語義。
  • 開發iOS程序時應該使用nonatomic屬性,因爲atomic屬性會嚴重影響性能。

可以把屬性(property)當做一種簡稱,其意思就是:編譯器會自動寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量。

可以在類的實現代碼裏通過@synthesize語法來制定實例變量的名字。

使用@dynamic關鍵字,它會告訴編譯器:不要自動創建實現屬性所用的實例變量,也不要爲其創建存取方法。

7、在對象內部儘量直接訪問實例變量

  • 在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,則應通過屬性來寫。

“通過屬性訪問”與“直接訪問”有幾個區別:
由於不經過OC的“方法派送”(method dispatch),所以直接訪問實例變量的速度當然比較快。在這種情況下,編譯器所生成的代碼會直接訪問保存對象實例變量的那塊內存。

直接訪問實例變量時,不會調用其“設置方法”,這就繞過了爲相關屬性所定義的“內存管理語義”。比方說,如果在ARC下直接訪問一個聲明爲copy的屬性,那麼並不會拷貝該屬性,只會保留新值並釋放舊值。

如果直接訪問實例變量,那麼不會觸發“鍵值觀察”(Key-Value Observing,KVO)通知。這樣做是否會產生問題,還取決於具體的對象行爲。

通過屬性來訪問有助於排查與之相關的錯誤,因爲可以給“獲取方法”和“設置方法”中新增“斷點”(Breakpoint),監控該屬性的調用者及其訪問時機

  • 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據。
  • 有時會使用惰性初始化技術配置某份數據,這種情況下,需要通過屬性來讀取數據。

8、理解“對象等同性”這一概念

  • 若想檢測對象的等同性,請提供isEqual:與hash方法
-(BOOL)isEqual:(id)object {
    if (self == Object) return YES;
    if ([self class] != [object class]) return NO;
    EOCPerson *otherPerson = (EOCPerson *)object;
    if (![_firstName isEqualToString:otherPerson.firstName])
        return NO;
    if (![_lastName isEqualToString:otherPerson.lastName])
        return NO;
    if (_age != otherPerson.age)
        return NO;
    return YES;
}
  • 相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同。
  • 不要盲目地逐個檢測每條屬性,而是應該依照具體需求來制定檢測方案。
  • 編寫hash方法時,應該使用計算速度快而且哈希碼碰撞機率低的算法。
- (NSUInteger)hash {
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

9、以“類族模式”隱藏實現細節

  • 類族模式可以把實現細節隱藏在一套簡單的公共接口後面。
  • 系統框架中經常使用類族。

cocoa裏的類簇:
大部分collection類都是類族(NSArray、NSMutableArray)。

  • 從類族的公共抽象基類中繼承子類時要當心,若有開發文檔,則應首先閱讀。

類族有辦法新增子類,但是需要遵守幾條規則:
子類應該繼承自類族中的抽象基類;
子類應該定義自己的數據存儲方式;
子類應當覆寫超類文檔中指明需要覆寫的方法。

10、在既有類中使用關聯對象存放自定義數據

  • 可以通過“關聯對象”機制來把兩個對象連起來。
  • 定義關聯對象時可指定內存管理語義,用以模仿定義屬性時所採用的“擁有關係”與“非擁有關係”。
  • 只有在其他做法不可行時才應選用關聯對象,因爲這樣做法通常會引入難於查找的bug。

11、理解objc_msgSend的作用

  • 消息由接受者、選擇子及參數構成。給某對象“發送消息”(invoke a message)也就相當於在該對象上“調用方法”(call a method)

  • 疑問
    消息傳遞時如何傳遞一個基本類型?
    和正常對象一致

       NSInteger nTag = 1;
       NSMethodSignature * method = [NSMethodSignature signatureWithObjCTypes:"v@:@i"];
       NSInvocation * inv = [NSInvocation invocationWithMethodSignature:method];
       [inv setArgument:&nTag atIndex:3];
       
  • 發給某對象的全部消息都要由“動態消息派發系統”(dynamic message dispatch system)來處理,該系統會查出對應的方法,並執行其代碼。

12、理解消息轉發機制

  • 若對象無法響應某個選擇子,則進入消息轉發流程。

消息轉發分爲兩大階段。
第一階段先徵詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子”(unknown selector),這叫做“動態方法解析”(dynamic method resolution)。
第二階段涉及“完整的消息轉發機制”(full forwarding mechanism)。

  • 通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。

動態方法解析
對象在收到無法解讀的消息後,首先將調用其所屬類的下列類方法:
+(BOOL)resolveInstanceMethod:(SEL)selector

resolveClassMehtod:

  • 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理。

備援接收者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它;能不能把這條消息轉給其他接收者來處理。
-(id)forwardingTargetForSelector:(SEL)selector

  • 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啓動完整的消息轉發機制。

首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封與其中。
-(void)forwardInvocation:(NSIncovation *)invocation
在這裏插入圖片描述

13、用“方法調配技術”調試“黑盒方法”

  • 在運行期,可以向類中新增或替換選擇子所對應的方法實現。
  • 使用另一份實現來替換原有的方法實現,這道工序叫做“方法調配”(method swizzling),開發者常用此技術向原有實現中添加新功能。

通過此方案,開發者可以爲那些“完全不知道其具體實現的”(completely opaque,“完全不透明的”)黑盒方法增加日誌記錄功能,這非常有助於程序調試。

  • 一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用。

14、理解“類對象”的用意

  • 每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成了類的繼承體系。

  • 如果對象類型無法在編譯期確定,那麼就應該使用類型信息查詢方法來探知。
    isMemberOfClass:
    isKindOfClass:

  • 儘量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因爲某些對象可能實現了消息轉發功能。


三、接口與API設計

15、用前綴避免命名空間衝突

  • 選擇與你的公司、應用程序或者二者皆有關聯之名稱作爲類名的前綴,並在所有代碼中均使用這一前綴。
  • 若自己所開發的程序中用到了第三方庫,則應爲其中的名稱加上前綴。

16、提供“全能初始化方法”

  • 在類中提供一個全能初始化方法,並於文檔裏指明。其他初始化方法均應調用此方法。

  • 疑問
    會不會導致一個全能初始化方法修改,到處需要重新測試?

  • 若全能初始化方法與超類不同,則需覆寫超類中的對應方法。

如果超類的初始化方法不適用與子類,那麼應該覆寫這個超類方法,並在其中拋出異常。

17、實現description方法

  • 實現description方法返回一個有意義的字符串,用以描述該實例。
  • 若想在調試時打印出更詳盡的對象描述信息,則應實現debugDescription方法。

18、儘量使用不可變對象

  • 儘量創建不可變的對象。
  • 若某屬性僅可於對象內部修改,則在“class-continuation分類”中將其有readonly屬性擴展爲readwrite屬性。
  • 不要把可變的collection作爲屬性公開,而應提供相關方法,以此修改對象中的可變collection。

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

  • 起名時應遵從標準的OC命名規範,這樣創建出來的接口更容易爲開發者所理解。
  • 方法名要言簡意賅,從左至右讀起來要像日常用語中的句子纔好。

方法命名:
如果方法的返回值是新創建的,那麼方法名的首個詞應是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,因爲一般認爲這些方法不會創建新對象,即使有時返回內部對象的一份拷貝,我們也認爲那相當於原有的對象。這些存取方法應該按照其所對應的屬性來命名:
-localizedString
-lowercaseString

應該把表示參數類型的名詞放在參數前面。

如果方法要在當前對象上執行操作,那麼就應該包含動詞;若執行操作時還需要參數,則應該在動詞後面加上一個或多個名詞。

  • 方法名裏不要使用縮略後的類型名稱。

不要使用str這種簡稱,應該用視圖string這樣的全稱。

Boolean屬性應加is前綴。如果某方法返回非屬性的Boolean值,那麼應該根據其功能,選用has或is當前綴
-hasPrefix
-isEqualToString
有個屬性叫做enabled,則其兩個存取方法應該分別起名爲setEnabled:isEnabled

將get這個前綴留個前些藉由“輸出參數”來保存返回值的方法,比如說,把返回值填充到“C語言式數組”(C-style array)裏的那種方法就可以使用這個詞做前綴
-getCharacters:range:
OC一般不以get開頭。該方法用get作其前綴,原因在於,調用此方法時,要在其首個參數中傳入數組,而該方法所獲取的字符串正是要放在這個數組裏面。

  • 給方法起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符。

類與協議的命名
繼承自UITableView的子類命名爲EOCImageTableView。不過這時要加上自己的前綴EOC,而不是延用超類的前綴UI。這樣做的原因在於,你不應該把自己的類放在其他框架額命名空間裏面。

如果要從其他框架中繼承子類,那麼務必遵循其命名習慣。比方說,要從UIView類中繼承自定義的子類,那麼類名末尾的詞必須是View。同理,若要創建自定義的委託協議,則其名稱中應該包含委託發起方的名稱,後面再跟上Delegate一詞。

20、爲私有方法名加前綴

  • 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開。

爲私有方法名加前綴還有一個原因,就是便於修改方法名或方法簽名。對於公共方法來說,修改其名稱或簽名之前要三思,因爲來的公共API不便隨意改動。
具體使用何種前綴可根據個人喜好來定,其中最好包含下劃線與字母p。筆者喜歡用p_

  • 疑問
    私有方法前綴使用--

  • 不要單用一個下劃線做私有方法的前綴,因爲這種做法是預留給蘋果公司用的。

21、理解OC錯誤模型

  • 只有發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常。

只在極其罕見的情況下拋出異常,異常拋出後,無須考慮恢復問題,而且應用程序此時也應該退出。

  • 在錯誤不那麼嚴重的情況下,可以指派“委託方法”。

在設計API時,NSError的第一種常見用法是通過委託協議來傳遞此錯誤。

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

NSError的另外一種常見用法是:經由方法的“輸出參數”返回給調用者。比如像這樣:

-(BOOL)doSomething:(NSError **)error {
    //Do something that may cause an error
    if (/*there was an error*/) {
        if (error) {
            //Pass the ‘error’ through the out-parameter
            *error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
        }
        return NO; ///< Indicate failure
    } else {
        return YES; ///< Indicate success
    }
}

這段代碼以*error語法爲error參數“解引用”(dereference),也就是說,error所指的那個指針現在要指向一個新的Error對象。在解引用之前,必須先保證error參數不是nil,因爲空指針解引用會導致“段錯誤”(segmentation fault)並使應用程序崩潰。

NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
    //There was an error
}

BOOL ret = [object doSomething:nil];
if (ret) {
    //There was an error
}

在使用ARC時,編譯器會把方法簽名中的NSError **轉換成NSError __autoreleasing,也就是說,指針所指的對象會在方法執行完畢後自動釋放。

22、理解NSCopying協議

  • 若想令自己所寫的對象具有拷貝功能,則需實現NSCopying
- (id)copyWithZone:(NSZone *)zone

爲何會出現NSZone呢?因爲以前開發程序時,會據此把內存分成不同的“區”(zone),而對象會創建在某個區裏面。現在不用了,每個程序只有一個區:“默認區”(default zone)。

  • 如果自定義的對象分爲可變版本與不可變版本,那麼就要同時實現NSCopying與NSMutableCopying協議。

  • 複製對象時需決定採用淺拷貝還是深拷貝,一般情況下應該儘量執行淺拷貝。

Foundation框架中的所有collection類在默認情況下都執行淺拷貝。

  • 如果你所寫的對象需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。

四、協議與分類

23、通過委託與數據源協議進行對象間通信

  • 委託模式爲對象提供了一套接口,使其可由此將相關事件告知其他對象。
  • 將委託對象應該支持的接口定義爲協議,在協議中把可能需要處理的事件定義成方法。
  • 當某對象需要從另外一個對象中獲取數據時,可以使用委託模式。這種情境下,該模式亦稱“數據源協議”(data source protocol)。

若有必要,可實現含有段位的結構體,將委託對象是否能響應相關的協議方法這一信息緩存至其中。
將方法響應能力緩存起來的最佳途徑是使用“位段”(bitfield)數據類型。以網絡數據獲取器爲例,可以在該實例中嵌入一個含有段位的結構體作爲實例變量,而結構體的每個位段則表示delegate對象是否實現了協議中的相關方法。此結構體的用法如下:

struct {
    unsigned int didReceiveData : 1;
    unsigned int didFailWithError : 1;
    unsigned int didUpdateProgressTo : 1;
}_delegateFlags;

- (void)setDelegate:(id<EOCNetworkFetcher>)delegate {
    _delegate = delegate;
    _delegateFlags.didReceiveData = [delegate respondsToSelector@selector(networkFetcher:didReceiveData)];
    ……
}

if (_delegateFlags.didUpdateProgressTo) {
    [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}

24、將類的實現代碼分散到便於管理的數個分類之中

  • 使用分類機制把類的實現代碼劃分成易於管理的小塊。
  • 將應該視爲“私有”的方法歸入名叫Private的分類中,以隱藏實現細節。

25、總是爲第三方類的分類名稱加前綴

  • 向第三方類中添加分類時,總應給其名稱加上你專用的前綴。
  • 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴。
@interface NSString (ABC_HTTP)
- (NSString *)abc_urlEncodedString;
- (NSString *)abc_urlDecodedString;

26、勿在分類中聲明屬性

  • 把封裝數據所用的全部屬性都定義在主接口裏。
  • 在“class-continuation分類”之外的其他分類中,可以定義存取方法,但儘量不要定義屬性。

只讀屬性還是可以在分類中使用的。

27、使用“class-continuation分類”隱藏實現細節

  • 通過“class-continuation分類”向類中新增實例變量。

如果某屬性在主接口中聲明爲“只讀”,而類的內部又要用設置方法修改此屬性,那麼就在“class-continuation分類”中將其擴展爲“可讀寫”。

//公共接口:
#import <Foundation/Foundation>
@interface EOCPerson:NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end

//我們一般會在“class-continuation分類”中把這兩個屬性擴展爲“可讀寫”
@interface EOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end

若觀察者(observer)正讀取屬性值而內部代碼又在寫入該屬性時,則有可能引發“競爭條件”(race condition)。合理使用同步機制(41條)能緩解此問題。

  • 把私有方法的原型聲明在“class-continuation分類”裏面。
@interface EOCPerson()
- (void)p_privetaMethod;
@end

筆者在編寫類的實現代碼之前,經常喜歡像這樣先把方法原型寫出來,然後再逐個實現。要想使類的代碼更易讀懂,可以試試這個好方法。

  • 若想使類所遵循的協議不爲人所知,則可於“class-continuation分類”中聲明。

28、通過協議提供匿名對象

  • 協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某協議的id類型,協議裏規定了對象所應實現的方法。
  • 使用匿名對象來隱藏類型名稱(或類名)。
  • 如果具體類型不重要,重要的是對象能夠響應(定義在協議裏的)特定方法,那麼可使用匿名對象來表示。
@property (nonatomic, weak) id<EOCDelegate> delegate;

//NSDictionary
- (void)setObject:(id)object forKey:(id<NSCopying>)key;

五、內存管理

29、理解引用計數

  • 引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好之後,其保留計數至少爲1。若保留計數爲正,則對象繼續存活。當保留計數降爲0時,對象就被銷燬了。
  • 在對象生命期中,其餘對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數。

30、以ARC簡化引用計數

  • 在ARC之後,程序員就無須擔心內存管理問題了。使用ARC來編程,可省去類中的許多“樣板代碼”。
  • ARC管理對象生命期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作。在ARC環境下,變量的內存管理語義可以通過修飾符指明,而原來則需要手工執行“保留”及“釋放”操作。

除了會自動調用“保留”與“釋放”方法外,使用ARC還有其他好處,它可以執行一些手動操作很難甚至無法完成的優化。

_myPerson = [EOCPerson personWithName:@“BOb Smith”];
EOCPerson *tmp = [EOCPerson personWithName:@“BOb Smith”];
_myPerson = [tmp retain];

這段代碼演示了ARC是如何通過這些特殊函數來優化程序的:

+(EOCPerson *)personWithName:(NSString *)name {
    EOCPerson *person = [[EOCPerson alloc] init];
    person.name = name;
    objc_autoreleaseReturnValue(person);
}

EOCPerson *tmp = [EOCPerson personWithName:@“BOb Smith”];
_myPerson = objc_retainAutoreleaseReturnValue(tmp);

爲了求得最佳效率,這些特殊函數的實現代碼都因處理器而異。下面這段僞代碼描述了其中的步驟:

id objc_autoreleaseReturnValue(id object) {
    if (/*caller will retain object*/) {
        set_flag(object);
        return object; ///< No autorelease
    } else {
        return [object autorelease];
    }
}

id objc_retainAutoreleaseReturnValue(id object) {
    if (get_flag(object)) {
        clear_flag(object);
        return object; ///< No retain
    } else {
        return [object retain];
    }
}

變量的內存管理語義

- (void)setObject:(id)object {
    [_object release];
    _object = [object retain];
}

- (void)setObject:(id)object {
    _object = object;
}
  • 由方法所返回的對象,其內存管理語義總是通過方法名來體現。ARC將此確定爲開發者必須遵守的規則。

使用ARC時必須遵循的方法命名規則:
若方法名以下列詞語開頭,則其返回的對象歸調用者所有:alloc、new、copy、mutableCopy。

  • ARC只負責管理OC對象的內存。尤其要注意:CoreFoundation對象不歸ARC管理,開發者必須適時調用CFRetain/CGRelease。

如果有非OC的對象,比如CoreFoundation中的對象或是由malloc()分配在堆中的內存,那麼仍然需要清理。

- (void)dealloc {
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}
發佈了31 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章