此文是《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》的閱讀筆記
目錄
第1章 熟悉Objective-C
- 第1條:了解Objective-C語言的起源
- 第2條:在類的頭文件中盡量少引入其他頭文件
- 第3條:多用字面量語法,少用與之等價的方法
- 第4條:多用類型常量,少用#define預處理指令
- 第5條:用枚舉表示狀態、選項、狀態碼
第2章 對象、消息、運行期
- 第6條:理解“屬性”這一概念
- 第7條:在對象內部盡量直接訪問實例變量
- 第8條:理解“對象等同性”這一概念
- 第9條:以“類族模式”隱藏實現細節
- 第10條:在既有類中使用關聯對象存在自定義數據
- 第11條:理解objc_msgSend的作用
- 第12條:理解消息轉發機制(
推薦看原文
) - 第13條:用“方法調配技術”調試“黑盒方法”
- 第14條: 理解“類對象”的用意
第3章 接口與API設計
- 第15條:用前綴避免命名空間沖突
- 第16條:提供“全能初始化方法”
- 第17條:實現description方法
- 第18條:盡量使用不可變對象
- 第19條:使用清晰而協調的命名方法
- 第20條:為私有方法名加前綴
- 第21條:理解Objective-C錯誤模型
- 第22條:理解NSCopying協議
第4章 協議與分類
- 第23條:通過委托與數據源協議進行對象間通信
- 第24條:將類的實現代碼分散到便于管理的數個分類之中
- 第25條:總是為第三方類的分類名稱加前綴
- 第26條:勿在分類中聲明屬性
- 第27條:使用"class-continuation分類"隱藏實現細節
- 第28條:通過協議提供匿名對象
第5章 內存管理
- 第29條:理解引用計數
- 第30條:以ARC簡化引用計數
- 第31條:在dealloc方法中只釋放引用并解除監聽
- 第32條:編寫“異常安全代碼”時留意內存管理問題
- 第33條:以弱引用避免保留環
- 第34條:以“自動釋放池塊”降低內存峰值
- 第35條:用“僵尸對象”調試內存管理問題(
推薦看原文
) - 第36條:不要使用retainCount
第6章 塊與大中樞派發
- 第37條:理解“塊”這一概念
- 第38條:為常用的塊類型創建typedef
- 第39條:用handler塊降低代碼分散程度
- 第40條:用塊引用其所屬對象時不要出現保留環
- 第41條:多用派發隊列,少用同步鎖
- 第42條:多用GCD,少用performSelector系列方法
- 第43條:掌握GCD及操作隊列的使用時機
- 第44條:通過Dispatch Group機制,根據系統資源狀況來執行任務
- 第45條:使用dispatch_once來執行只需運行一次的線程安全代碼
- 第46條:不要使用dispatch_get_current_queue
第7章 系統框架
- 第47條:熟悉系統框架
- 第48條:多用塊枚舉,少用for循環
- 第49條:對自定義其內存管理語義的collection使用無縫橋接
- 第50條:構建緩存時選用NSCache而非NSDictionary
- 第51條:精簡initialize與load的實現代碼
- 第52條:別忘了NSTimer會保留其目標對象
===================================
第一章 熟悉Objective-C
===================================
第1條:了解Objective-C語言的起源
- Objective-C由Smalltalk(消息型語言的鼻祖)演化而來,使用的是“消息結構”(messageing structure)而非"函數調用"(functioncalling),這兩者之間的區別就像這樣:
// Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith: parameter1 and: parameter2];
// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
- Objective-C 為C語言添加了對象特性,是其超集。Objective-C使用動態綁定的消息結構,也就是說,在運行時才會檢查對象類型。接收一條消息之后,究竟應執行何種代碼,由運行期環境而非編譯器來決定。
- Objective-C語言中的指針是用來指示對象的,聲明一個變量,令其指代某個對象的語法基本上是照搬C語言的。
NSString *someString = @"The string";
變量someString指向分配在堆里的某塊內存,其中含有一個NSString的對象"The string"。另外Objective-C的對象分配總是分配在"堆空間"(heap space)中,有些不含有*的變量,它們可能會使用棧空間(stack space),例如CGRect類型的分配。而這些分配在"堆空間"的對象,需要內存管理(現在不用手動管理了)。
第2條:在類的頭文件中盡量少引入其他的頭文件
- 當不需要知道某個類的全部細節時,用
@class TheClass
,來替代#import "TheClass.h"
, 這叫做“向前聲明”(forward declaring)該類。@class
的使用既可以減少不必要的導入,也可以避免頭文件因互相import而編譯通不過。
// EOCPerson.m
# import "EOCPerson.h"
// # import "EOCEmployer.h"
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonatomic, strong) EOCEmployer *employer;
@end
將引入的頭文件時機盡量延后,只在確有需要時才引入,這樣就可以減少類的使用者所需引入的頭文件數量, 減少編譯時間。
# import "ViewController.h"
# import "EOCPerson.h"
# import "EOCEmployer.h"
@interface ViewController()
@end
@implementation
- (void)viewDidload {
[super viewDidload];
EOCPerson *person = [EOCPerson new];
EOCEmployer *employer = [EOCEmployer new];
employer.name = "Coder"; // 如果沒有 # import "EOCEmployer.h"會編譯報錯
person.employer = employer;
}
@end
- 聲明類遵循某個協議時,最好把協議單獨放在一個頭文件中,再來導入。然而有些協議,例如“委托協議”(delegate protocol),就不用單獨寫一個頭文件了。在那種情況下,協議只有與接收協議委托的類放在一起定義才有意義。
第3條:多用字面量語法,少用與之等價的方法
- 字面量語法(literal syntax)實際上只是一種“語法糖”(syntactic sugar),例如普通的創建NSNumber對象是:
NSNumber *someNumber = [NSNumber numberWithInt:1];
, 字面量的語法是NSNumber *someNumber = @1;
- 應該使用字面量語法來創建字符串、數值、數組、字典。與創建此類對象的常規方法相比,這么做更加簡明扼要。
- 應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
- 用字面量語法創建數組或字典時,若值中有nil,則會拋出異常。因此務必確保值里不含nil。
- 字面量語法的局限性:除了字符串之外,所創建出來的對象必須屬于Foundation框架才行。如果定義了這些類的子類,則無法用字面量創建對象。而且使用字面量語法創建的字符串、數組、字典對象都是不可變的(immutable)。
第4條:多用類型常量,少用#define預處理指令
- 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量不一致。
- 在實現文件中使用static const來定義“只在編譯單元內可見的常量”(translation-unit-specitfic constant)。另外,在Objective-C的語境下,“編譯單元”一詞通常指每個類的實現文件(以.m為后綴名)。通常命名時以"k"作為前綴,然后駝峰命名。
static const NSTimeInterval kAnimationDuration = 0.3
- 在頭文件中使用extern來聲明全局變量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名加前綴。Objective-C沒有“名稱空間”(namespace)這一概念,以類名作為前綴,是為了避免命名沖突。例如UIKit就按照這種方式來聲明用作通知名稱的全局常量。其中有類似UIApplicationDidEnterBackgroundNotification與UIApplicationWillEnerForegroundNotification這樣的常量名。
// EOCLoginManager.h
#import <Foundation/Foundation.h>
// 此常量需放在“全局符號表”(global symbol table)中,以便可以在定義該常量的編譯單元之外使用。
extern NSString *const EOCLoginManagerDidLoginNotification;
@interface EOCLoginManager : NSObject
- (void)login;
@end
// EOCLoginManager.m
#import "EOCLoginManager.h"
NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";
@implementation EOCLoginManager
@end
第5條:用枚舉表示狀態、選項、狀態碼
- 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
- 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將個選項值定義為2的冪,以便通過按位或操作將其組合起來。
- 用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,并指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會采用編譯器所選的類型。
- 在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch 語句并未處理所有枚舉。
===================================
第二章 對象、消息、運行期
===================================
第6條:理解“屬性”這一概念
- “屬性”(property)是Objective-C的一項特性,用于封裝對象中的數據。利用@property聲明屬性,可以快速方便得為實例變量創建存取器set和get方法。@property還允許我們用點語法使用存取器。
@interface Person : NSObject
@property NSString *name;
@end
- @synthesize 的作用:是為屬性添加一個實例變量名,或者說別名。同時會為該屬性生成 setter/getter 方法。
/*
_name是實例變量,name是屬性。告訴編譯器name屬性為_name實例變量生成setter and getter方法的實現
*/
@synthesize name = _name
- 屬性特質
@property (nonatomic, readonly, assign, getter=isOpen) BOOL open;
- 【原子性】默認情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性(atomicity)。如果屬性具備nonatomic特質,則不使用同步鎖。
- 【讀、寫權限】a.具備readwirte特質的屬性擁有“獲取方法(getter)“與“設置方法(setter)”。若該屬性由@synthesize實現,則編譯器會自動生成這兩個方法。b.具備readonly特質的屬性僅擁有獲取方法,只有當該屬性有@synthesize實現時,編譯器才會為其合成獲取方法。你可以用此特質把某個屬性對外公開為只讀屬性,然后在"class-continuation分類"中將其重新定義為讀寫屬性。
- 【內存管理語義】
3.1assign
"設置方法"只會執行針對“純量類型”(scalar type,例如CGFloat或NSInteger等)的簡單賦值操作。
3.2strong
此特質表明該屬性定義了一種“擁有關系”(owning relationship)。為這種屬性設置新值時,設置方法會先保留新值,并釋放舊值,然后再將新值設置上去
3.3weak
表明了一種“非擁有關系”(nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似,然后在屬性所指的對象遭到摧毀時,屬性值也會清空。
3.4unsafe_unretained
語義和assign相同,但是它適用于“對象類型”,該特質表達一種“非擁有關系”(“不保留”,unretained),當目標對象遭到摧毀時,屬性值不會自動清空(“不安全”,unsafe),這一點與weak有區別。
3.5copy
此特質所表達的屬性關系與strong類似。然而設置方法并不保留新值,而是將其"拷貝"(copy)。 - 【方法名】
- getter=<name> 指定“獲取方法”的方法名。
- setter=<name> 指定“設置方法”的方法名。
- 在設置屬性對應的實例變量時,一定要遵從該屬性所聲明的語義。
- (id)initWithzName: (NSString *)name {
if (self = [super init]) {
_name = [name copy];
}
return self;
}
- 開發iOS程序時應該使用nonatomic,因為atomic屬性會嚴重影響性能(而且atomic并不能保證線程安全,若要實現“線程安全”的操作,還需要更為深層的鎖定機制才行)。
第7條:在對象內部盡量直接訪問實例變量
- 在對象內部讀取數據時,應該直接通過實例變量來讀(不經過Objective-C“方法派發”,訪問實例變量速讀比較快),而寫入數據時,則應通過屬性來寫。
- 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據。
- 有時會使用惰性初始化(lazy initialization)技術配置某份數據,這種情況下,需要通過屬性來讀取數據。
第8條:理解“對象等同性”這一概念
- 若想檢測對象的等同性,請提供“isEqual”與hash方法。
- 相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同。
- 不要盲目地逐個檢測每條屬性,而是應該按照具體需求來指定檢測方案。
- 編寫hash方法時,應該使用計算速度快而且哈希碼碰撞幾率低的算法。
例如
- (NSUInteger) hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
第9條:以“類族模式”隱藏實現細節
- “類族(class cluster)”是一種很有用的模式(pattern),可以隱藏“抽象基類”(abstract base class)背后的實現細節。Objective-C的系統框架中普遍使用此模式。比如UIKit的UIButton類。創建按鈕,需要調用類方法:
+ (UIButton *)buttonWithType: (UIButtonType)type;
。還有,NSnumber類也是類族。 - 大部分collection類都是類族,例如NSArray與其可變版本NSMutableArray。
- 子類應該繼承自類族中的抽象基類。(可惜Objective-C這門語言沒辦法指明某個基類是“抽象的”,通常是在文檔中寫明)。
- 子類應該定義自己的數據存儲方式。
開發者編寫NSArray子類時,經常在這個問題上受阻。子類必須用一個實例變量來存放數組中的對象。這似乎與大家預想的不同,我們以為NSArray自己肯定會保存那些對象,所以在子類中就無須再保存一份了。但是大家要記住,NSArray本身只不過是包在其他隱藏對象外面的殼,它僅僅定義了所有數組都具備的一些接口。對于這個自定義的數組子類來說,可以用NSArray來保存其實例。 - 子類應該覆寫超類文檔中指明需要覆寫的方法。
- 從類族的公共抽象基類中繼承子類時要當心,若有開發文檔,則應首先閱讀。
第10條:在既有類中使用關聯對象存放自定義數據
- “關聯對象”, 可以給某個對象關聯許多其他對象,這些對象通過來“鍵”來區分。存儲對象的時候,可以指明“存儲策略”(storage policy),用以維護相應的“內存管理語義”。存儲策略由名為objc_AssociationPolicy的枚舉所定義:
關聯類型 | 等效的@property屬性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
- 通過下列方法可以管理關聯對象:
- void objc_setAssociateObject(id object, void *key, id value, objc_AssociationPolicy policy)
此方法以給定的鍵和策略為某對象設置關聯對象 - id objc_getAssociateObject(id object, void *key)
此方法根據給定的鍵從某對象中獲取相應的關聯對象值。 - void objc_removeAssociatedObjects(id object)
此方法移除指定對象的全部關聯對象。
- void objc_setAssociateObject(id object, void *key, id value, objc_AssociationPolicy policy)
- 在設置關聯對象時,若想令兩個鍵匹配到同一個值,則二者必須是完全相同的指針才行。鑒于此,在設置關聯對象值時,通常使用靜態全局變量做鍵。
- 只有在其他做法不可行時才應選用關聯對象,因為這種做法通常會引入難于查找的bug。
第11條:理解objc_msgSend的作用
- 給某個對象“調用方法”(call a method),相當于給某個對象“發送消息”(invoke a message)。
- 給對象發送消息可以這樣寫:
id returnValue = [someObject messageName:paramter];
。someObject叫做“接收者”(receiver),messagename叫做“選擇子”(seletor)。選擇子與參數合起來稱為“消息”(message)。編譯器看到此消息后,將其轉換為一條標準的C語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數,叫做objc_msgSend,其“原型”:void objc_msgSend(id self, SEL cmd, ...)
。 - 消息傳遞機制:objc_msgSend函數會依據接收者與選擇子的類型來調用適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳轉至其實現代碼。若是找不到,那就沿著繼承體系繼續向上查找,等找到合適的方法之后再跳轉。如果最終還是找不到相符的方法,那就執行“消息轉發”(message forwarding)操作。
- 消息傳遞機制:
第12條: 理解消息轉發機制
消息轉發分為兩大階段。
- 第一階段:動態方法解析:先征詢接收者,所屬的類,看是否能動態添加方法,以處理當前這個“未知的選擇子”(unknown selector)。這個過程是調用類方法:
+ (BOOL)resolveInstanceMethod: (SEL)selector
(如果需要新增方法,就要在這個方法里面進行處理并返回YES) - 第二階段:第一階段執行完后,接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。這時,運行期系統會請求接收者以其他手段來處理消息相關的方法調用。分為兩小步:
- 尋找備援接收者。調用
- (id)forwardingTargetForSelector:(SEL)selector
,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。在一個對象內部,可能還有一系列其他對象,該對象可經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息似的。 - 完整的消息轉發。到這一步,系統會調用
- (void)forwardInvocation: (NSInvocation *)invocation
,其中invocation對象攜帶了那條尚未處理的消息有關的全部細節。在這里將消息指派給目標對象,然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以比較少用。
消息轉發全流程
- 尋找備援接收者。調用
第13條:用“方法調配技術”調試“黑盒方法”
- 類的方法列表會把選擇子的名稱映射到相關的方法實現之上,使得“動態消息派發系統”能夠據此找到應該調用的方法。這些方法均以函數指針的形式表示,這種指針叫做IMP。
- 方法調配(method swizzing):在運行期,向類中新增或替換選擇子對應的方法實現,常用來向原有實現中添加新功能。
第14條:理解“類對象”的用意
- Objective-C對象本質是一個結構體,該對象結構體的首個成員是Class類的變量。
typedef struct objc_object {
Class isa;
} *id;
- Class對象也是一個結構體,此結構體存放類的“元數據”(metadata),首個變量也是isa指針,這說明Class本身亦為Objective-C對象。結構體里還有個變量叫做super_class,它定義了本類的超類。類對象所屬的類型(也就是isa指針所指向的類型)是另外一個類,叫做“元類”(metaclass),用來表述類對象本身所具備的元數據?!邦惙椒ā本投x與此處,因為這些方法可以理解成類對象的實例方法。每個Class僅有一個“類對象”,而每個“類對象”僅有一個與之相關的“元類”。
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
super_class指針確立了繼承關系,而isa指針描述了實例所屬的類。通過這張布局關系圖即可執行“類型信息查詢”。
- 可以用類型信息查詢方法來檢視類繼承體系。"isMemberOfClass":能夠判斷出對象是否為某個特定類的實例,而"isKindOfClass":則能夠判斷出對象是否為某類或其派生類的實例。
- 如果對象類型無法在編譯器確定,那么就應該使用類型信息查詢方法來探知。
- 盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了轉發功能。
===================================
第三章 接口與API設計
===================================
第15條:用前綴避免命名空間沖突
- Objective-C沒有其他語言那種內置的命名空間(namespace)機制。所以為了避免命名沖突的唯一辦法就是變相實現命名空間:為所有名稱都加上適當前綴。Apple宣稱其保留使用所有"兩字母前綴"(two-letter prefix)的權利; 可以選擇與你的公司、應用程序或二者皆有關聯之名稱作為類名的前綴。
第16條:提供“全能初始化方法”
- “全能初始化方法”(designated initializer):可為對象提供必要信息以便其能完成工作的初始化方法。例如:
- (id)init
- (id)initWithString:(NSString *)string
- (id)initWithTimeIntervalSingNow:(NSTimeInterval)seconds
- (id)initWithTimeInterval:(NSTimeInterval)seconds
sinceDate:(NSDate *)refDate
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds
- (id)initWithTimeIntervalSince1970:(NSTimeInterval)seconds
其中的- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds
是全能初始化方法。也就是,其余的初始化方法都要調用它。于是,只有在全能初始化方法中,才會存儲內部數據。
- 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應調用此方法。
- 若全能初始化方法與超類不同,則需覆寫超類中的對應方法。
- 如果超類的初始化方法不適用于子類,那么應該覆寫這個超類方法,并在其中拋出異常。
第17條:實現description方法
- 實現description方法返回一個有意義的字符串,用以描述該實例
- 若想在調試時打印更詳盡的對象描述信息,則應實現。debugDescripiton方法(在NSObject類的默認實現中,此方法只是直接調用了description)。
第18條:盡量使用不可變對象
- 盡量創建不可變的對象。
- 若某屬性僅可于對象內部修改,則在“class-continuation分類”中將readonly屬性擴展為readwrite屬性。
- 不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection。
第19條:使用清晰而協調的命名方式
- 方法名要言簡意賅,從左至右讀起來更像個日常用語中的句子才行好。
- 方法命名:
- 如果方法的返回值是新創建的,那么方法的首個詞應是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵守這種命名方式,因為一般認為這些方法不會創建新對象,即便有時返回內部對象的一份拷貝,我們也認為那相當于原有的對象。這些存取方法應該按照其所對應的屬性來命名。
- 應該把表示參數類型的名詞放在參數前面。
- 如果方法要在當前對象上執行操作,那么應該包含動詞;若執行操作時還需要參數,則應該在動詞后面加上一個或多個名詞。
- 不要使用str這種簡稱,應該用string這樣的全稱。
- Boolean屬性應加is前綴。如果某方法返回非屬性的Boolean值,那么應該根據其功能,選用has或is當前綴。
- 將get這個前綴留給那些由“輸出參數”(out-parameter)來保存返回值的方法,比如說,把返回值填充到“C語言式數組”(C-style array)里的那種方法就可以使用這個詞做前綴。
- (void)getCharacter:(unichar *)buffer range:(NSRange)aRange
- 類與協議的命名:如果要從其他框架中繼承子類,那么務必遵守其命名慣例。比方說,要從UIView類中繼承自定義的子類,那么類名 末尾的詞必須是view。同理,若要創建自定義的委托協議,則其名稱中應該包含委托發起方的名稱,后面再跟上Delegate一詞。
第20條:為私有方法名加前綴
- 把私有方法標出來,這樣很容易就能看出哪些方法可以隨意修改,哪些不應輕易改動。
- 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開。但是不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的(Apple文檔),可以考慮用“p_”做前綴,或者用基于公司或者項目的“xx_”形式。
第21條:理解Objective-C錯誤模型
- Objective-C在默認情況下不是“異常安全的”(exception safe)。具體來說,這意味著:如果拋出異常,那么本應在作用域末尾釋放的對象現在卻不會自動釋放了。
- 如果想生成“異常安全”的代碼,可以通過設置編譯器的標志來實現,不過這將引入一些額外代碼,在不拋出異常時,也照樣執行這部分代碼。需要打開的編譯器標志叫做 -fobjc-arc-exceptions。換句話說在ARC中異??赡軙е聦ο蟮膬却嫘孤?。如果開啟了該選項,則ARC會額外為異常中的對象申請和釋放操作添加代碼,保證異常中ARC管理的對象也不會造成內存泄露。當然這樣一來缺點就是可能會生成大量平??赡芨居貌坏降拇a。(只有發生異常才會執行)
- 在出現非致命錯誤(nonfatal error)時,Objective-C語言所用的編程范式為:令方法返回nil/0,或是使用NSError,以表明其中 有錯誤發生。
- NSError對象里封裝了三條信息:
1. Error domain(錯誤范圍,其類型為字符串)。也就是產生錯誤的根源,通常用一個特有的全局變量來定義。
2. Error code(錯誤碼,其類型為整數)獨有的錯誤代碼,用以指明在某個范圍內具體發生了何種錯誤。
3. User info(用戶信息,其類型為字典)有關此錯誤的額外信息,其中或許包含一段“本地化的描述”(localized description)。 - 在處理錯誤時,除了可以指派“委托方法”(delegate method)來處理錯誤,也可以把錯誤信息放在NSError對象里,經由“輸出參數”返回給調用者。
NSError *error = nil;
Bool ret = [object doSomething: &error];
if (error) {
// There was an error
}
第22條:理解NSCopying協議
- 若想令自己所寫的對象具有拷貝功能,則需實現NSCopying協議。
- 如果自定義的對象分為可變版本與不可變版本,那么就要同時實現NSCopying與NSMutableCopying協議。
- 復制對象時需決定采用淺拷貝還是深拷貝,一般情況下應該盡量執行淺拷貝。
- 如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法。
===================================
第四章 協議與分類
===================================
第23條:通過委托與數據源協議進行對象間通信
- 委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象。
- 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的事件定義成方法。
- 當某對象需要從另外一個對象中獲取數據時,可以使用委托模式。這種情境下,該模式亦成“數據源協議”(data source protocal)。
- 若有必要,可實現含有位段的結構體(位段結構既能夠節省空間,又方便于操作),將委托對象是否能相應相關協議方法這一信息緩存至其中。
struct {
unsigned int didReceiveData: 1;
unsigned int didFailWithError: 1;
unsigned int didUpdateProgressTo: 1;
} _delegateFlags;
第24條:將類的實現代碼分散到便于管理的數個分類之中
- 使用分類把類的實現代碼劃分成易于管理的小塊
- 將應該視為“私有”的方法歸入名叫Private的分類中,以隱藏實現細節
第25條:總是為第三方類的分類名稱加前綴
- 分類機制通常用于向無源碼的既有分類中新增功能。
- 向第三方類中添加分類時,總應給其名稱加上你專用的前綴。
例如NSString分類,NSString(ABC_HTTP)
。 - 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴。
例如:- (NSString *)abc_urlDecodedString;
第26條:勿在分類中聲明屬性
- 屬性是用來封裝數據的,所要表達的意思是,類中有數據在支撐著它。
- 在"class-continuation分類"之外的其他分類中,可以定義存取方法(或者readonly屬性),但盡量不要定義屬性??梢园逊庋b數據所用的全部屬性定義在主接口中。
第27條:使用"class-continuation分類"隱藏實現細節
- 通過"class-continuation分類"向類中新增實例變量。
- 如果屬性在主接口中聲明為“只讀”,而類的內部又要用設置方法修改此屬性,那么就在"class-continuation分類"中將其擴展為“可讀寫”。
- 把私有方法的原型聲明在"class-continuation分類"里面。
- 若想使類所遵守的協議不為人所知,則可于"class-continuation分類"中聲明。
第28條:通過協議提供匿名對象
- 協議可在某種程度上提供匿名類型。具體的對象類型可以淡化遵從某協議的id類型,協議里規定了對象所應實現的方法。
- (id<EOCDatabaseConnection>)connectionWithIdentifiler:(NSString *)indentifier;
- 使用匿名對象來隱藏類型名稱(或類名)。
- 如果類型不重要,重要的是對象能夠響應(定義在協議里的)特定方法,那么可使用匿名對象來表示。
===================================
第五章 內存管理
===================================
第29條:理解引用計數
- Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數器。如果想使某個對象繼續存活,那就遞增其引用計數;用完了之后,就遞減其計數。計數變為0名,就表示沒人關注此對象了,于是,就可以把它銷毀。
- 在ARC工程中使用MRC: 在Project的Build Phase里面的Compile Source找到需要特殊處理的文件,加上編譯選項(Compiler Flags),在文件后用
-fno-objc-arc
修飾就行了。 - “懸掛指針”:為了避免在不經意間使用了無效對象,一般調用完release之后都清空指針。
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject: number];
[number release];
number = nil;
- 屬性存取方法中的內存管理。若屬性為"strong關系"(strong relationship),則設置的屬性會保留。
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
此方法將保留新值并釋放舊值,然后更新實例變量,令其指向新值。順序很重要!
- 自動釋放池。在Objective-C的引用計數架構中,自動釋放池是一項重要特性。調用release會立刻遞減對象的保留計數(而且還可能令系統回收對象),然而有時候可以不調用它,改為調用autorelease,此方法會在稍后遞減計數,通常是在下一次“事件循環”(event loop時遞減),除非你有自己的自動釋放池。
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}
- 通常采用“弱引用”(weak refefence)來打破保留環(retain cycle)。
第30條:以ARC簡化引用計數
- ARC環境下,引用計數實際上還是要執行的,只不過保留與釋放操作現在是由ARC自動為你添加。ARC會自動執行retain、release、autorelease等操作。因為ARC自動調用這些方法時,并不通過普的通Objective-C消息派發機制,而是直接調用其底層C語言版本,所以不能覆寫這些方法,這些方法從來不會被直接調用。
- 在ARC管理對象生命期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作。在ARC環境下,變量的內存管理語義可以通過修飾符(
_ _strong 、_ _weak、_ _unsafe_unretained、_ _atutoreleasing
)指明,而原來則需要手工執行“保留”及“釋放”操作。 - ARC只負責管理Objective-C對象的內存。尤其要注意:CoreFoundation對象不歸ARC管理,開發者必須適時調用CFRetain/CFRelease
第31條:在dealloc方法中只釋放引用并解除監聽
- 在dealloc方法里,應該做的事情就是釋放指向其他對象的引用,并取消原來訂閱的“鍵值觀測”(KVO)或NSNotificationCenter等通知,不要做其他事情。
- 如果對象持有文件描述等系統資源,那么應該專門編寫一個方法來釋放此種資源。這樣的類要和其使用者約定:用完資源后必須調用close方法。
- 執行異步任務的方法不應在dealloc里調用;只能在正常狀態下執行的那些方法也不應在dealloc里調用,因為此時對象已處于正在回收的狀態了。
第32條:編寫“異常安全代碼”時留意內存管理問題
- 應用程序因異常狀況而終止時才拋出異常,因此,如果應用程序即將終止,那么是否還會發生內存泄漏就已經無關緊要了。 但如果捕獲異常,那么一定要注意將try塊內所創立的對象清理干凈。
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (..) {
NSLog(@"Whoops, there was an error. Oh well..");
}
上面代碼情況是會發生內存泄漏的。
- 在默認情況下,ARC不生成安全處理異常所需的清理代碼。開啟編譯器標志(
-fobjc-arc-exceptions
)后,可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率。不過有種情況編譯器會自動把-fobjc-arc-exceptions
標志打開,就是處于Objective-C++模式時。因為C++處理異常所用的代碼與ARC實現的附加代碼類似,所以令ARC加入自己的代碼以安全處理異常,其性能損失并不太大。
第33條:以弱引用避免保留環
- 將某些引用設為weak,可避免出現“保留環”。
- weak引用可以自動清空,也可以不自動清空。自動清空(autonilling)是隨著ARC而引入的新特性,由運行期系統來實現。在具備自動清空功能的弱引用上,可以隨意讀取其數據,因為這種引用不會指向已經回收過的對象。
第34條: 以“自動釋放池塊”降低內存峰值
- 釋放對象由兩種方式:一種是調用release方法,使其保留計數立即遞減;另一種是調用autorelease方法,將其加入“自動釋放池”中。自動釋放池用于存放那些需要在稍后某個時刻釋放的對象。清空(drain)自動釋放池時,系統會向其中的對象發送release消息。
- 一般情況下無須擔心自動釋放池的創建問題。系統會自動創建一些線程,這些線程默認都有自動釋放池,每次執行“事件循環”(event loop)時,就會將其清空。通常只有一個地方需要創建自動釋放池,那就是在main函數里,我們用自動釋放池來包裹應用程序的主入口點(main application entry point)。
- 內存峰值(high-memory waterline)是指應用程序在某個特定時段內的最大內存用量(highest memory footproint)。
- 自動釋放池排布在棧中,對象收到autorelease消息后,系統將其放入最頂端的池里。
- @autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池。
第35條:用“僵尸對象”調試內存管理問題
- 向已回收的對象發送消息是不安全的。這么做有時可以,有時不可以,可以與否,取決于對象所占內存有沒有為其他內容所覆寫。而這塊內存有沒有移作他用,無法確定,因此應用程序只是偶爾崩潰。在沒有崩潰的情況下,可能那塊內存只復用了一部分,所以對象的某些二進制數據依然有效。還有另外一種可能,就是那塊內存恰好為另外一個有效且存活的對象所占據。在這種情況下,運行期系統會把消息發到新對象那里,而此對象也許能應答,也許不能。
- Xcode的“僵尸對象”(Zombie Object)調試功能打開時,運行期系統會把所有已經回收的實例轉化成特殊的“僵尸對象”,而不會真正回收它們。這種對象所在的核心內存無法重用,因此不可能遭到覆寫。僵尸對象收到消息后,會拋出異常,其中準確說明了發送過來的消息,并描述了回收之前的那個對象。僵尸對象是調試內存管理問題的最佳方式。給僵尸對象發送消息后,控制臺會打印消息,應用程序則會終止。打印消息就像這樣:
*** -[CFString respondsToSelector:]: message sent to deallocated instance 0x7ff9e9c0808e0
- 系統在回收對象時,可以不將其真的回收,而是把它轉化為僵尸對象。通過環境變量
NSZombieEnabled
可開啟此功能。 - 系統會修改對象的isa指針,令其指向特殊的僵尸類(例如由原來的EOCClass變為_NSZombile_EOCClass),從而使該對象變為僵尸對象。僵尸類能夠響應所有的選擇子,響應方式為:打印一條包含消息內容及其接收者的消息,然后終止應用程序。
第36條:不要使用retainCount
- retainCount的方法之所以無用,其首要原因在于:它所返回的保留計數只是某個給定時間點上的值。該方法并未考慮到系統稍后會把自動釋放池清空(參見第34條),因而不會將后續的釋放操作從返回值里減去,這樣的話,此值就未必能真實反應實際的保留計數了。
- 引入ARC之后,retainCount方法就正式廢止了,在ARC下調用該方法會導致編譯器報錯。
===================================
第六章 塊與大中樞派發
===================================
第37條:理解“塊”這一概念
- 塊其實就是個值,而且自有其相關類型。與int、float或Objective-C對象一樣,也可以把塊賦給變量,然后像使用其他變量那樣使用它。塊類型的語法與函數指針近似。
// 語法結構
return_type (^block_name)(parameters)
// 初始化賦值
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b;
}
- 默認情況下,為塊所捕獲的變量,是不可以在塊里修改的, 需要在聲明變量的時候加上_ _block修飾符才可以。
- 塊本身可視為對象,有引用計數。當最后一個指向塊的引用移走之后,塊就回收了。回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執行的保留操作。
-
塊的內部結構:
塊對象的內存布局
塊存放在塊對象的內存區域中,首個變量是指向Class對象的指針,該指針叫做isa。其余內存里含有塊對象正常運轉所需的各種信息。
在內存中 最重要的就是invoke變量,這個函數指針,指向塊的實現代碼。函數原型至少要接受一個void *型的參數,此參數代表塊。descriptor變量是指向結構體的指針,每個塊里都包含此結構體,其中聲明了塊對象的總體大小,還聲明了copy與dispose這兩個輔助函數所對應的函數指針。輔助函數在拷貝及丟棄塊對象時運行,其中會執行一些操作,比如說,前者要保留捕獲的對象,而后者則將之釋放。
塊還會把它所捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后面,捕獲了多少個變量,就要占據多少內存空間。請注意,拷貝的并不是對象本身,而是指向這些對象的指針變量。invoke函數為何需要把塊對象作為參數傳進來呢?原因在于,執行塊時,要從內存中把這些捕獲到的變量讀出來。 - 塊可以分配在?;蚨焉?,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣的話,就和標準的Objective-C對象一樣,具備引用計數了。
第38條:為常用的塊類型創建typedef
- 以typedef重新定義塊類型,可令塊變量用起來更加簡單。
- 定義新類型時應遵守現有的命名習慣,勿使其名稱與別的類型相沖突。
- 不妨為同一個塊簽名定義多個類型別名。如果要重構的代碼使用了塊類型的某個別名,那么只需修改相應typedef中的塊簽名即可,無須改動其他typedef。
第39條:用handler塊降低代碼分散程度
- 在創建對象時,可以使用內聯的handler塊將相關業務邏輯一并聲明。
- 在有多個實例需要監控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若改用handler塊來實現,則可直接將塊與相關對象放在一起。
- 設計API時如果用到了handler塊,那么可以增加一個參數,使調用者可通過此參數來決定應該把塊安排在哪個隊列上執行。
第40條:用塊引用其所屬對象時不要出現保留環
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當心保留環問題。
- 一定要找個適當的時機解除保留環,而不能把責任推給API的調用者。
第41條:多用派發隊列,少用同步鎖
- 濫用“同步塊”@synchronized(self)會降低代碼效率,因為共用同一個鎖的那些同步塊,都必須按順序執行。若是在self對象上頻繁加鎖,那么程序可能要等另一段與此無關的代碼執行完畢,才能繼續執行當前代碼。
- 使用NSLock對象,在極端情況下,同步塊會導致死鎖,另外效率也不見得很高,而如果直接使用鎖對象的話,一旦遇到死鎖,就會非常麻煩。
- 派發隊列可用來表述同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對象更簡單。
- 將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執行異步派發的線程。
- 使用同步隊列及柵欄塊,可以令同步行為更加高效。
第42條:多用GCD,少用performSelector系列方法
- performSelector系列方法在內存管理方面容易疏失。它無法確定將要執行的選擇子具體是什么,返回什么,因而ARC編譯器也就無法插入適當的內存管理方法。
- persormSelector系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發送給方法的參數個數都受到限制。
- 如果想把任務放在另一個線程上執行,那么最好不要用performSelector系列方法,而是應該把任務封裝在到塊里,然后調用大中樞派發機制的相關方法來實現。
第43條:掌握CGD及操作隊列的使用時機
- 在解決多線程與任務管理問題時,派發隊列并非唯一方案。
- 操作隊列(NSOperationQueue)提供了一套高級Objective-C API,能實現純GCD所具備的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
第44條:通過Dispatch Group機制,根據系統資源狀況來執行任務
- 一些列任務可歸入一個dispatch group之中。開發者可以在這組任務執行完畢時獲得通知。
- 通過dispatch group,可以在并發式派發隊列里同時執行多項任務。此時GCD會根據系統資源狀況來調度這些并發執行的任務。開發者若自己來實現功能,則需編寫大量代碼。
第45條:使用dispatch_once來執行只需運行一次的線程安全代碼
- 經常需要編寫“只需執行一次的線程安全代碼”(thread-safe single-code execution)。通過GCD所提供的dispatch_once函數,很容易就能實現此功能。
- 標記應該聲明在static或global作用域中,這樣的話,在把只需執行一次的塊傳給dispatch_once函數時,傳進去的標記也是相同的。
第46條:不要使用dispatch_get_current_queue(推薦看原文
)
- dispatch_get_current_queue函數的行為常常與開發者所預期的不同。此函數已經廢棄,只應做調試只用。
- 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
- dispatch_get_current_queue函數用于解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決。
===================================
第七章 系統框架
===================================
第47條:熟悉系統框架
- 系統框架都是動態庫
- 在為Mac OS X或iOS系統開發“帶圖形界面的應用程序”(graphical application)時,會用到名為Cocoa的框架,在iOS上稱為Cocoa Touch。其實Cocoa本身并不是框架,但是里面集成了一批創建應用程序時經常用到的框架。
- 許多系統框架都可以直接使用。其中最重要的是Foundation與CoreFoundation,這兩個框架提供了構建應用程序所需的許多核心功能。
- 很多常見任務都能用框架來做,例如音頻與視頻處理、網絡通信、數據管理等。
- 請記住:用純C寫成的框架與用Objective-C寫成的一樣重要,若想成為優秀的Objective-C開發者,應該掌握C語言的核心概念。
第48條:多用塊枚舉,少用for循環
- 遍歷collection有四種方式。最基本的方法是for循環,其次是NSEnumerator遍歷法及快速遍歷法,最新、最先進的方式則是“塊枚舉法”。
- “塊枚舉法”本身就能通過GCD來并發執行遍歷操作,無須另行編寫代碼。而采用其他遍歷方式則無法輕易實現這一點。
- 若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。
第49條:對自定義其內存管理語義的collection使用無縫橋接
- 通過無縫橋接技術,可以在Foundation框架中的Objective-C對象與CoreFoundation框架中的C語言數據結構之間來回轉換。
- 在CoreFoundation層面創建collection時,可以指定許多回調函數,這些函數表示此collection應如何處理其元素。然后,可運用無縫橋接技術,將其轉換成具備特殊內存管理語義的Objective-C collection。
第50條:構建緩存時選用NSCache而非NSDictionary
- 實現緩存時應選用NSCache而非NSDictionary對象。因為NSCache可以提供優雅的自動刪減功能,而且是“線程安全的”,此外,它與字典不同,并不會拷貝鍵。
- 可以給NSCache對象設置上限,用以限制緩存中的對象總個數及“總成本”,而這些尺度則定義了緩存刪減其中對象的時機。但是絕對不要把這些尺度當成可靠的“硬限制”(hard limit),它們僅對NSCache起指導作用。
- 將NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能,也就是說,當NSPurgeableData對象所占內存為系統所丟棄時,該對象自身也會從緩存中移除。
- 如果緩存使用得當,那么應用程序的響應速度就能提高。只有那種“重新計算起來很費事的”數據,才值得放入緩存,比如那些需要從網絡獲取或從磁盤讀取的數據。
第51條:精簡initialize與load的實現代碼(推薦看原文)
- 在加載階段(通常指應用程序啟動的時候),如果類實現了load方法,那么系統就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。與其他方法不同, load方法不參與覆寫機制。
- load方法中使用其他類是不安全的,因為不能卻確定其他類是否已經加載好了。
- 首次使用某個類之前,系統會向其發送initialize消息。由于此方法遵從普通的覆寫規則,所以通常應該在里面判斷當前初始化的是哪個類。
- load與initialize方法都應該實現得精簡一些,這有助于保持應用程序的響應能力,也能減少引入"依賴環"(interdependency cycle)的幾率。
- 無法在編譯器設定的全局變量,可以放在initialize方法里初始化。
第52條:別忘了NSTimer會保留其目標對象(推薦看原文)
- NSTimer 對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計時器失效,另外,一次性的計時器在觸發完任務之后也會失效。
- 反復執行任務的計時器(repeating timer),很容易引入保留環,如果這種計時器的目標對象又保留了計時器本身,那肯定會導致保留環。這種環狀保留關系,可能是直接發生的,也可能是通過對象圖里的其他對象間接發生的。
- 可以擴充NSTimer的功能,用“塊”來打破保留環。不過,除非NSTimer將來在公共接口里提供功能,否則必須創建分類,將相關實現代碼加入其中。