第一條 了解Objective-C預言起源
起源:Smalltalk
類型:使用消息結構的語言
區別:使用消息結構的語言,其運行時所應執行的代碼由運行環境來決定;而使用函數調用的語言則由編譯器決定。
對象分配在堆空間上,指針分配在棧空間上
結構體分配在棧空間(CGRect)
第二條 在類的頭文件中盡量少引入其他頭文件
用OC編寫任何類幾乎都需要引入Foundation.h。
向前聲明:@class EOCEmployer;
使用#import而非#include互相引用時不會死循環
要點:
1、除非有必要,不要引入頭文件。應在類的頭文件中使用向前聲明來提及別的類,在實現文件中引入那個類的頭文件。可以盡量降低類之間的耦合。
2、無法使用向前聲明,如聲明某個類遵循一項協議。盡量把“遵循某協議”的聲明移到“class-continuation分類”中。若不行,就把協議單獨放在一個頭文件中,再引入。
第三條 多用字面量語法,少用與之等價的方法
字面量語法只是一種“語法糖”
疑問:mrc下字面量創建的對象到底有沒有reatain+1
使用字面量語法創建出來的字符串、數組、字典對象都是不可變的,若想要可變版本的對象,需要復制一份
要點:
1、使用字面量語法來創建字符串、數值、數組、字典。
2、通過取下標操作來訪問數組下標或字典中的鍵所對應的元素
3、用字面量語法創建數組或字典時,若值中有nil,則會拋出異常。因此,務必確保值里不含nil
第四條 多用類型常量,少用#define 預處理指令
#define ANIMATION_DURATION 0.3
static const NSTimeInterval kAnimationDuration = 0.3;
若不打算公開某個常量,則應將其定義在使用該常量的實現文件里。static修飾符意味著該變量僅在定義此變量的編譯單元可見。在OC語境下,“編譯單元”指每個類的實現文件(以.m為后綴名)。假如聲明此變量時不加static,則編譯器會為它創建一個“外部符號”。此時若是另一個編譯單元中也聲明了同名變量,那么編譯器就會拋出錯誤。
如果一個變量既聲明為static,又聲明為const,編譯器根本不會創建符號,而是會像#define一樣,把所遇到的變量都替換成常值。
常量放在“全局符號表中”:
//In the header file
extern NSString *const EOCStringConstant;
//In the implementation file
NSString *const EOCStringConstant = @"VALUE";
第五條 用枚舉表示狀態、選項、狀態嗎
要點
如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將各選項值定義為2的冪,以便通過按位或操作將其組合起來。
用NS_ENUM與NS_OPTIONS宏定義枚舉類型,并指明底層數據類型。這樣可以確保枚舉是用開發者所選的底層數據類型實現出來,而不會采用編譯器所選的類型。
在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉。
第六條 理解 “屬性” 這一概念
編譯器會把“點語法”轉換為對存取方法的調用,使用“點語法”的效果與直接調用存取方法相同。
如果使用了屬性,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫“自動合成(autosynthesis)”。這個過程由編譯器在編譯期執行,所以編輯器里看不到“合成方法(synthesied method)”的源代碼。除了生成方法代碼,編譯器還要自動向類中添加適當類型的實例變量,并且在屬性名前面加下劃線,以此作為實例變量的名字。也可以在類的實現代碼里通過@synthesize語法來指定實例變量的名字
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
有一種辦法能阻止編譯器自動合成存取方法,就是使用@dynamic關鍵字,它會告訴編譯器:不要自動創建實現屬性所用的實例變量,也不要為其創建存取方法。而且,在編譯訪問屬性的代碼時,即使編譯器發現沒有定義存取方法,也不會報錯,它相信這些方法能在運行期找到。
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName;
@end
屬性的特質:原子性,讀/寫權限,內存管理語義,方法名
要點
用@property語法來定義對象中所封裝的數據
通過“特質”來指定存儲數據所需的正確語義
在設置屬性所對應的實例變量時,一定要遵從該屬性所聲明的語義
開發iOS程序時應該使用nonatomic屬性,因為atomic屬性會嚴重影響性能
第七條 在對象內部盡量直接訪問實例變量
直接訪問實例變量,不會觸發 kvo通知。
在寫入實例變量時,通過設置方法來做,在讀取實例變量時,則直接訪問。
在初始化方法中設置屬性值應該直接訪問實例變量,因為子類可能會覆寫設置方法
delloc中直接通過
惰性化初始技術需要采用存取方法。
第八條 理解“對象等同性” 這一概念
使用NSObject協議中聲明的 isEqual 方法來判斷兩個對象的等同性
NSObject協議中兩個用于判斷等同性的關鍵方法:
- (BOOL)isEqual:(id)object;
- (NSUInterger)hash;
等同性約定
如果 isEqual: 判定兩個對象相等,那么hash方法也必須返回同一個值.但是,如果兩個對象的hash方法返回同一個值, isEqual: 未必會認為兩者相等
有一種情況要注意,在容器中放入可變類對象時,把某個對象放入collection之后,就不應再改變其哈希碼了。
要點:
若想檢測對象的等同性,請提供“isEquel:”與hash方法
相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同
第九條 以“類族模式”隱藏實現細節
類族可以隱藏“抽象基類”背后的實現細節。
系統框架有很多類族。大部分collection類都是類族。
在傳統的類族模式中,通常只有一個類具備“公共借口”,這個類就是類族中的抽象基類
Cocoa中NSArray這樣的類族來說,新增子類需遵守幾條規則:
自類應該繼承自類族中的抽象基類
子類應該定義自己的數據存儲方式
子類應當覆寫超類文檔中指明需要覆寫的方法
第十條 在既有類中使用關聯對象存放自定義數據
設置關聯對象
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
取關聯對象值
id objc_getAssociatedObject(id object, void *key)
移除指定對象的全部關聯對象
void objc_removeAssociatedObjects(id object)
要點
定義關聯對象可指定內存管理語義,用以模仿定義屬性時所采用的“擁有關系”與“非擁有關系”
只有在其他做法不可行時才應選用關聯對象,因為這種做法通常會引入難于查找的bug
第十一條 理解objc_msgSend的作用
C語言使用“靜態綁定”,在編譯期就能決定運行時調用的函數
在Objective-C中,如果向某個對象傳遞消息,那就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通的C語言函數,然而對象收到消息后,究竟該調用哪個方法則完全于運行期決定,甚至可以在程序運行時改變,這些特性使得Objective-C成為一門真正的動態語言。
id returnValue = [someObject messageName:parameter];
someObject: 接受者(receiver)
messageName: : 選擇子(selector)
選擇子與參數 合起來稱為“消息(message)”
編譯器看到此消息后,將其轉換為一條標準的C語言函數調用,其原型
void objc_msgSend(id self, SEL cmd, ...)
轉換后:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
該方法需要在接收者所屬的類中搜尋其“方法列表”,如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若找不到,就沿著繼承體系繼續向上查找,等找到合適的方法之后再跳轉,如果最終還是找不到相符的方法,就執行“消息轉發(message forwarding)”
objc_msgSend會將匹配結果緩存在“快速映射表”(fast map)里,每個類都有這樣一個緩存,稍后還向該類發送與選擇子相同的消息,會快很多,但還是不如“靜態綁定的函數操作”那樣迅速。
消息派發并非應用程序的瓶頸所在。假如是瓶頸,可以只編寫純C函數
objc_msgSend等函數一旦找到應該調用的方法實現后,就會“跳轉過去。”之所以能這樣,因為Objective-C對象的每個方法都可以視為簡單的C函數,其原型如下
<return_type> Class_selector(id self, SEL _cmd, ...)
每個類里面都有一張表,其中的指針都指向這種函數,而選擇子的名稱是查表時所用的“鍵”。objc_msgSend正式通過這張表格來尋找應該執行的方法并跳至實現的。
要點
發給某對象的全部消息都要由“動態消息派發系統”(dynamic message dispatch system)來處理,該系統會查出對應的方法,并執行其代碼。
第十二條 理解消息轉發機制
當對象接收到無法解讀的消息后,就會啟動“消息轉發(message forwarding)”機制,程序員可經由此過程告訴對象應該如何處理未知消息。
消息轉發分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子(unknown selector)”,這叫做“動態方法解析”(dynamic method resolution)。
第二階段涉及“完整的消息轉發機制”(full forwarding mechanism)。如果運行期系統已經把第一階段執行完了,那么接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。此時,運行期系統會請求接收者以其他手段來處理與消息相關的方法調用。這又細分為兩小步
a、首先,接收者看看有沒有其他對象能處理這條消息。若有,則運行期系統會把消息轉給那個對象,消息轉發過程結束。
b、若沒有“備援的接收者”(replacement receiver),則啟動完整的消息轉發機制,運行期系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。
對象在收到無法解讀的消息后,首先將調用其所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector
使用這種方法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就可以了。此方案常用來實現@dynamic 屬性。
備援接收者:當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。該步驟對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
完整的消息轉發
- (void)forwardInvocation:(NSInvocation *)invocation
這個方法可以實現得很簡單:只需改變調用目標,使消息在新目標得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人采用這么簡單的實現方式。比較有用的實現方式為:在出發消息前先以某種方式改變消息內容,比如追加另外一個參數,或者改變選擇子,等等。
接收者在每一步中均有機會處理消息。步驟越往后,處理消息的代價就越大。
第十三條 用“方法調配技術” 調試 “黑盒方法”
類的方法列表會把選擇子的名稱映射到相關的方法實現上,使得“消息派發系統”能夠根據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做IMP,其原型如下:
id (*IMP)(id, SEL, ...)
交換方法實現,可用下列函數:
void method_exchangeImpentations(Method m1, Method m2)
此函數的兩個參數表示待交換的兩個方法實現,而方法實現則可通過下列函數獲得
Method class_getInstanceMethod(Class aClass, SEL aSelector)
要點:
使用另一份實現來替換原有的方法實現,這道工序叫做“方法調配”,開發者常用此技術向原有實現中添加新功能。
一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用
第十四條 理解“類對象”的用意
描述Objective-C對象所用的數據結構定義在運行期程序庫的頭文件里,id類型本身也在定義在這里:
typedef struct objc_object {
Class isa;
} *id;
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
每個類僅有一個“類對象”,而每個“類對象”僅有一個與之相關的“元類”
isMemberOfClass: 判斷對象是否為某個特定類的實例
isKindOfClass: 對象是否為某類或其派生類的實例
類對象是“單例”,在應用程序范圍內,每個類的Class僅有一個實例。
要點
每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成類的集成體系
如果對象類型無法在編譯器確定,那么就應該使用類型信息查詢方法來探知
盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能
第十五條 用前綴避免命名空間沖突
Objective-C沒有其他語言語言那種內置的命名空間機制
使用Cocoa創建應用程序時一定要注意,Apple宣稱其保留使用所有“兩字母前綴”的權利,所以你自己選用的前綴應該是三字母的。
要點
選擇與你的公司,應用程序或二者皆有關聯之名稱作為類名的前綴,并在所有代碼中均使用這一前綴,并在所有代碼中均使用這一前綴
若自己所開發的程序庫中用到了第三房庫,則應為其中的名稱加上前綴
第十六條 提供“全能初始化方法”
如果創建類實例的方法不止一種,那么這個類就會有多個初始化方法。要再其中選定一個作為全能初始化方法。
要點
在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應調用此方法
若全能初始化方法與超類不同,則需覆寫超類中的對應方法
如果超類的初始化方法不適用于子類,那么應該覆寫這個超類方法,并在其中拋出異常
第十七條 實現description方法
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}
//output:
// person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">
要點
實現description方法返回一個有意義的字符串,用以描述該實例
若想在調試時打印出更詳細的對象描述信息,則應實現debugDescription方法
第十八條 盡量使用不可變對象
應該盡量把對外公布出來的屬性設為只讀,而且只在確有必要時才將屬性對外公布。
有時可能想修改封裝在對象內部的數據,但是卻不想令這些數據為外人所改動。這種情況下,通常做法是在對象內部將readonly屬性重新聲明為readwrite
在定義類的公共API時,還要注意一件事情:對象里表示各種collection的那些屬性究竟應該設成可變的還是不可變的。例如,用某個類表示個人信息,該類里還存放了一些引用,指向此人的諸位朋友。把這個人的全部朋友放在一個列表里,并將其做成屬性,可以添加或刪除此人的朋友,這個屬性需要用可變的set來實現。在這種情況下,通常應該提供一個readonly屬性供外界使用,該屬性返回不可變的set,而此set是內部不可變set的一份拷貝
要點
盡量創建不可變的對象
若某屬性僅可于對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite
不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection
第十九條 使用清晰而協調的命名方式
不要吝于使用長方法名。把方法名起得稍微長一點,可以保證其能準確傳達出方法所執行的任務
給方法命名時的注意事項:
1、如果方法的返回值是新創建的,那么方法名的首個詞應是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,因為一般認為這些方法不會創建新對象,即便有時返回內部對象的一份拷貝,我們也認為那相當于原有的對象。這些存取方法應該按照其所對應的屬性來命名
2、應該把表示參數類型的名詞放在參數前面。
3、如果方法要在當前對象上執行操作,那么就應該包含動詞,若執行操作時還需要參數,則應該在動詞后面加上一個或多個名詞。
4、不要使用str這種簡稱,應該用string這樣的全稱
5、Boolean屬性應加is前綴。如果某方法返回屬性的Boolean值,那么應該根據其功能,選用has或is當前綴。
6、將get這個前綴留給那些借由“輸出參數”來保存返回值的方法。
第二十條 為私有方法名加前綴
與公有方法不同,私有方法不出現在接口定義中。有時可能要在“class-continuation分類”里聲明私有方法,然而最近修訂的編譯器已經不需要在使用方法前必須先聲明了。所以說,私有方法一般只在實現的時候聲明。
要點
給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開
不要單用一個下劃線做私有方法前綴,因為這種做法是預留給蘋果公司用的
第二十一條 理解Objective-C錯誤模型
ARC在默認情況下不是異常安全的
Objective-C語言現在所采用的辦法是:只在極其罕見的情況下拋出異常,異常拋出之后,無須考慮恢復問題,而且應用程序此時也應該退出。這就是說不用編寫復雜的異常安全代碼。
在出現不嚴重的錯誤時,Objective-C語言所用的編程范式為:令方法返回nil/0,或是使用NSError
要點
只有發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常
在錯誤不嚴重情況下,可以指派“委托方法(delegate method)”來處理錯誤,也可以把錯誤信息放在NSError對象里,經由“輸出參數”返回給調用者
第二十二條 理解NSCopying協議
如果想令自己的類支持copy操作,就要實現NSCopying協議,該協議只有一個方法
- (id)copyWithZone:(NSZone *)zone
以前開發程序時,會據NSZone把內存分成不同的區(zone),而對象會創建在某個區里面。現在不用了,每個程序只有一個區:默認區(default zone)。所以說,盡管必須實現這個方法,但是不必擔心其中的zone參數。
copy方法由NSObject實現,該方法只是以默認區為參數調用copyWithZone:。我們總是想覆寫copy方法,其實真正需要實現的卻是copyWithZone:方法。
要點
如果自定義的對象分為可變版本與不可變版本,那么就要同時實現NSCopying與NSMutableCopying協議
復制對象時需決定采用淺拷貝還是深拷貝,一般情況下應該盡量執行淺拷貝
如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法
第二十三條 通過委托與數據源協議進行對象間通信
Objective-C開發者廣泛使用一種名叫“委托模式(Delegate pattern)”的變成設計模式來實現對象間通信的通信,該模式的主旨時:定義一套接口,某對象若想接受另一個對象的委托,則需遵從此接口,以便成為其“委托對象(delegate)”。而這“另一個對象”則可以給委托對象回傳一些信息,也可以在發生相關事件時通知委托對象
@protocol EOCNetworkFetcherDelegate
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
@end
有了協議后,類就可以用一個屬性來存放其委托對象了
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;
@end
實現委托對象的的辦法:聲明某個類遵從委托協議,然后把協議中想實現的那些方法在類里實現。可以在接口中聲明,也可以在“class-continuation分類”中聲明。
@interface EOCDataModel () <EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data {
}
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error {
}
@end
委托對象上調用可選方法
NSData *data = xxx;
if ([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)) {
[_delegate networkFetcher:self didReceiveData:data];
}
通常把委托對象能否響應某個協議方法這一信息緩存起來,以優化程序效率
第二十四條 將類的實現代碼分散到便于管理的數個分類之中
要點
使用分類機制把類的實現代碼劃分成易于管理的小塊
將應該視為“私有”的方法歸入名為Private的分類中,以隱藏實現細節
第二十五條 總是為第三方類的分類名稱加前綴
分類中的方法是直接添加在類里面,它們就好比這個類中的固有方法。將分類方法加入類中這一操作是在運行期系統加載分類時完成的。運行期系統會把分類中所實現的每個方法都加入類的方法列表中。如果類中本來就有此方法,而分類又實現了一次,那么分類中的方法會覆蓋原來那一份實現代碼。實際上可能會發生很多次覆蓋,比如某個分類中的方法覆蓋了“主實現”中的相關方法,而另一個分類中的方法又覆蓋了這個分類中的方法。多次覆蓋的結果以最后一個分類為準。
要點:
向第三方類中添加分類時,總應該給其名稱加上你專有的前綴
向第三方類中添加分類時,總應該給其中的方法名加上你專有的前綴
第二十六條 勿在分類中聲明屬性
盡管從技術上說,分類里也可以聲明屬性,但這種做法還是要盡量避免。原因在于,除了“class-continuation分類”之外,其他分類都無法向類中新增實例變量,因此,它們無法把實現屬性所需的實例變量合成出來。
開發者需要在分類中為屬性實現存取方法。此時可以把存取方法聲明為@dynamic,也就是說,這些方法等到運行期再提供,編譯器目前是看不見的。如果決定使用消息轉發機制在運行期攔截方法調用,并提供實現,那么或許可以采用這種方法。
關聯對象能夠解決在分類中不能合成實例變量的問題。
#import <objc/runtime.h>
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson (Friendship)
- (NSArray *)friends {
return objc_getAssociatedObject(self, kFriendsPropertyKey);
}
- (void)setFriends:(NSArray *)frends {
objc_setAssociatedOjbect(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION__RETAIN_NONATOMIC);
}
本例中,正確做法是把所有屬性都定義在主接口里。類所封裝的全部數據都應該定義在主接口中,這里是唯一能夠定義實例變量(也就是數據)的地方。而屬性知識定義實例變量及相關存取方法的語法糖,所以也應遵循同實例變量一樣的規則。至于分類機制,則應將其理解為一種手段,目標在于擴展類的功能,而非封裝數據。
要點
把封裝數據所用的全部屬性都定義在主接口里
在“class-continuation分類”之外的其他分類中,可以定義存取方法,但盡量不要定義屬性
第二十七條 使用“class-continuation分類”隱藏實現細節
Objective-C動態消息系統的工作方式決定了其不可能實現真正的私有方法或私有變量
“class-continuation分類”和普通分類不同,它必須定義在其所連續的那個類的實現文件里。其重要之處在于,這是唯一能聲明實例變量的分類,而且此分類沒有特定的實現文件,其中的方法都應該定義在類的主實現文件里。與其他分類不同,“class-continuation分類”沒有名字
@interface EOCPerson () {
NSString *_anInstanceVariable;
}
//Method declarations here
@end
@implementation EOCPerson {
int _anotherInstanceVariable;
}
//Method implementations here
@end
"class-continuation分類"還有一種合理用法,就是將public接口中聲明為“只讀”的屬性擴展為“可讀寫”
只會在類的實現中用到的私有方法也可以聲明在“class-continuation分類”中。
新版編譯器不強制要求開發者在使用方法前必須先聲明。
若對象所遵從的協議只應視為私有,則可在“class-continuation分類”中聲明
第二十八條 通過協議提供匿名對象
要點
協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某協議的id類型,協議里規定了對象所應實現的方法
使用匿名對象來隱藏類型名稱
如果具體類型不重要,重要的是對象能夠響應(定義在協議里的)特定方法,那么可使用匿名對象來表示
第二十九條 理解引用計數
retain 遞增保留計數
release 遞減保留計數
autorelease 待稍后清理“自動釋放池”(autorelease pool)時,再遞減保留計數
查看保留計數的方法叫retainCount,此方法不太有用,不推薦使用這個方法
按“引用樹”回溯,那么最終會發現一個“根對象”(root object)。在Mac OS X應用程序中,此對象就是NSApplication對象;而在iOS應用程序中,則是 UIApplication對象。兩者都是應用程序啟動時創建的單例
調用autorelease,此方法會在稍后遞減計數,通常是在下一次“事件循環”(event loop)時遞減,不過也可能執行得更早些。
此方法可以保證對象在跨越“方法調用邊界”(method call boundary)后一定存活。實際上,釋放操作會在清空最外層的自動釋放池時執行,除非你有自己的自動釋放池,否則這個時機指的就是當前線程的下一次事件循環。
要點
引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好之后,其保留計數至少為1。若保留計數為正,則對象繼續存活。當保留計數降為0時,對象就被銷毀了。
在對象生命期中,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數
第三十條 以ARC簡化引用計數
使用ARC時一定要記住,引用計數實際上還是要執行的,只不過保留與釋放操作現在是由ARC自動為你添加。
由于ARC會自動執行retain,release,autorelease等操作,所以直接在ARC下調用這些內存管理方法是非法的。具體來說,不能調用 retain,release,autorelease,dealloc
將內存管理語義在方法名中表示出來早已成為Objective-C的慣例,而ARC則將之確立為硬性規定。這些規則簡單地體現在方法名上。若方法名以下列詞語開頭,則其返回的對象歸調用者所有:alloc,new,copy,mutableCopy
若方法名不以上述四個詞語開頭,則表示其返回的對象并不歸調用者所有。這種情況下,返回的對象會自動釋放,所以其值在跨越方法調用邊界后依然有效。
使用ARC還有其他好處,它可以執行一些手工操作很難甚至無法完成的優化。例如,編譯期,ARC會把能夠互相抵消的retain、release、autorelease操作簡約。如果發現在同一個對象上執行了多次“保留”與“釋放”操作,那么ARC有時可以成對地移除這兩個操作。
ARC也會處理局部變量與實例變量的內存管理。默認情況下,每個變量都是指向對象的強引用。
@interface EOCClass : NSObject {
id __weak _weakObject;
id __unsafe_unretained _unsafeUnretainedObject;
}
不論采用上面哪種寫法,在設置實例變量時都不會保留其值。只有使用新版(Mac OS X 10.7,iOS 5.0及其后續版本)運行期程序庫時,加了__weak修飾符的weak引用才會自動清空。
塊會自動保留其所捕獲的全部對象,而如果這其中有某個對象又保留了塊本身,那么就可能導致“保留環”。可以用__weak局部變量來打破這種“保留環”
用了ARC后,就不需要再編寫這種dealloc方法了,不過如果有非Objective-C的對象,比如CoreFoundation中的對象或是由malloc()分配在堆中的內存,那么仍然需要清理。
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocateMemoryBlob);
}
要點
ARC只負責管理Objective-C對象的內存。尤其要注意:CoreFoundation對象不歸ARC管理,開發者必須適時調用CFRetain/CFRelease。
第三十一條:在dealloc方法中只釋放引用并解除監聽
在dealloc方法中,通常還要做一件事,那就是把原來配置過的觀測行為(observation behavior)都清理掉。如果用NSNotificationCenter給此對象訂閱過某種通知,那么一般應該在這里注銷,這樣的話,通知系統就不再把通知發給回收后的對象了,若是還向其發送通知,則必然會令應用程序崩潰
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
在dealloc里也不要調用屬性的存取方法,因為有人可能會覆寫這些方法,并于其中做一些無法在回首階段安全執行的操作。此外,屬性可能正處于“鍵值觀測”(Key-Value Observation, KVO)機制的監控之下,該屬性的觀察者(observer)可能會在屬性值改變時保留或使用這個即將回收的對象。這種做法會令運行期系統的狀態完全失調,從而導致一些莫名其妙的錯誤。
要點
執行異步任務的方法不應該在dealloc里調用;只能在正常狀態下執行的那些方法也不應在dealloc里調用,因為此時對象已經處在回收的狀態了。
第三十二條 編寫“異常安全代碼”時留意內存管理問題
異常處理例程將銷毀對象,然而在手動管理引用技術時,銷毀工作有些麻煩
EOCSomeClass *object;
@try {
object =[ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
@finally {
[object release];
}
在ARC下,問題更嚴重
@try {
EOCSomeClass *object =[ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
你可能會認為這種情況ARC自然會處理的。但實際上ARC不會自動處理。
-fobjc-arc-exceptions:開啟異常捕捉,默認不開啟
不開啟的原因:在Objective-C代碼中,只有當應用程序必須因異常狀況而終止時才應拋出異常。因此,如果應用程序即將終止,那么是否還會發生內存泄漏就無關緊要了。在應用程序必須立即終止的情況下,還去添加安全處理異常所用的附加代碼是沒有意義的。
有種情況編譯器會自動把-fobjc-arc-exceptions標志打開,就是處于Objective-C++模式時
要點
捕獲異常時,一定要注意將try塊內所創立的對象清理干凈。
在默認情況下,ARC不生成安全處理異常所需的清理代碼。開啟編譯器標志后,可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率
第三十三條 以弱引用避免保留環
用unsafe_unretained修飾的屬性特質,其語義同assign特質等價。然而,assign通常只用于“整體類型”(int、float、結構體等),unsafe_unretained則多用于對象類型。這個詞本身就表明其所修飾的屬性可能無法安全使用。
weak引用可以自動清空,也可以不自動清空。自動清空(autonilling)是隨著ARC而引入的新特性,由運行期系統來實現。在具備自動清空功能的弱引用上,可以隨意讀取其數據,因為這種引用不會指向已經回收過的對象。
第三十四條 以“自動釋放池塊”降低內存峰值
@autoreleasepool {
}
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate");
}
}
要點
自動釋放池排布棧中,對象收到autorelease消息后,系統將其放入最頂端的池里
合理運用自動釋放池,可降低應用程序的內存峰值
@autoreleasepool這種新式寫法能夠創建出更為輕便的自動釋放池
第三十五條 用“僵尸對象”調試內存管理問題
Cocoa提供了“僵尸對象”這個功能。啟用這項調試功能之后,運行期系統會把所有已經回收的實例轉化為特殊的“僵尸對象”,而不會真正回收它們。這種對象所在的核心內存無法重用,因此不可能遭到覆寫。僵尸對象收到消息后,會拋出異常,其中準確說明了發送過來的消息,并描述了回首之前的那個對象。
將NSZombieEnabled環境變量設為YES,即可開啟此功能。比方說,在Mac OSX系統中用bash運行程序時,可以這么做
export NSZombieEnabled="YES"
./app
要點
系統會修改對象的isa指針,令其指向特殊的僵尸類,從而使該對象變為僵尸對象。僵尸類能夠響應所有的選擇子,響應方式為:打印一條包含消息內容及其接受著的消息,然后終止應用程序。
第三十六條 不要使用retainCount
此方法之所以無用,其首要原因在于:他所返回的保留計數只是某個給定時間上的值。該方法并未考慮到系統會稍后把自動釋放池清空,因為不會將后續的釋放操作從返回值里減去,這樣的話,此值就未必能真實反映實際的保留計數了。
retainCount可能永遠不返回0,因為有時系統會優化對象的釋放行為,在保留技術還是1的時候就把它回首了。只有在系統不打算這么優化時,計數值才會遞減至0.
要點
引入ARC后,retainCount方法就正式廢止了,在ARC下調用該方法會導致編譯器報錯
第三十七條 理解“塊”這一概念
塊用^符號表示,后面跟著一對花括號,括號里面是塊的實現代碼。
^{
//Block implementation here
}
塊類型的語法與函數指針近似。下面列出的塊,沒有參數,也不返回值
void (^someBlock)() = ^{
//Block implementation here
};
塊類型的語法結構如下:
return_type (^block_name) (parameters)
下面這種寫法所定義的塊,返回nil,并且接受兩個int做參數:
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b;
};
int add = addBlock(2, 5);
塊的強大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b + additional;
};
int add = addBlock(2, 5);
默認情況下,為塊所捕獲的變量,是不可以在塊里修改的。不過,聲明變量的時候可以加上__block修飾符,就可以在塊內修改了。
如果塊捕獲的變量是對象類型,那么就會自動保留它。系統在釋放這個塊時,也會將其一并釋放。塊本身也和其他對象一樣,有引用計數。當最后一個指向塊的引用移走后,塊就回收了。回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執行的保留操作。
如果將塊定義在類的實例方法中,除了可以訪問類的所有實例變量外,還可以使用self變量。塊總能修改實例變量,所以在聲明時無須加__block。不過,如果通過讀取或寫入操作捕獲了實例變量,那么也會自動把self變量一并捕獲,因為實例變量與self所指代的實例關聯在一起的。
定義塊的時候,其所占的內存區域是分配在棧中的。這就是說,塊只在定義它的那個范圍內有效。
不安全:
void (^block) ();
if (/*some condition*/) {
block = ^{
NSLog(@"Block A");
}
}
else? {
block = ^{
NSLog(@"Block B");
};
}
block();
為解決此問題,可給塊對象發送copy消息以拷貝之。
void (^block) ();
if (/*some condition*/) {
block = [^{
NSLog(@"Block A");
} copy];
}
else? {
block =[ ^{
NSLog(@"Block B");
} copy];
}
block();
要點
塊可以分配在棧或堆上,也可以是全局的。分配在棧上的塊可拷貝到堆里。
第三十八條 為常用的塊類型創建typedef
為隱藏復雜的塊類型,需要用到C語言中名為“類型定義”的特性。
typedef int(^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value){
//Implementation
};
第三十九條 用handler塊降低代碼分散程度
與使用委托模式的代碼相比,用塊寫出來的代碼顯然更為整潔。
建議使用同一個塊來處理成功與失敗情況。
要點
在創建對象時,可以使用內聯的handler塊將相關業務邏輯一并聲明
在有多個實例需要監控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,若改為handler塊來實現,則可直接將塊與相關對象放在一起。
設計API時如果用到了handler塊,那么可以增加一個參數,使調用者可通過此參數來決定應該把塊安排在哪個隊列上執行
第四十條 用塊引用其所屬對象時不要出現保留環
要點
如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當心保留環問題
一定要找個適當的機會接觸保留環,而不能把責任推給API的調用者。
第四十一條 多用派發隊列,少用同步鎖
在OC中,如果有多個線程要執行同一份代碼,那么有時可能會出問題。這種情況下,通常要使用鎖來實現某種同步機制。在GCD出現之前,有兩種辦法,第一種是采用內置的“同步塊”(synchroniztion block):
- (void)synchronizedMethod {
@synchronized(self) {
//Safe
}
}
另一個辦法是直接使用NSLock對象:
_lock = [[NSLock allock] init];
- (void)synchronizedMethod {
[_lock lock];
//Safe
[_lock unlock];
}
有種簡單而高效的辦法可以替代同步塊或鎖對象,那就是使用“串行同步隊列”,將讀取操作及寫入操作都安排在同一個隊列里,既可保證數據同步。其用法如下:
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
-(NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
可以進一步優化,設置方法并不一定非得是同步的。設置實例變量所用的塊,并不需要向設置方法返回什么值。也就是說,設置方法的代碼可以改成下面這樣:
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
但這么改有個壞處:如果你測以下程序性能,那么可能會發現這種寫法比原來慢,因為執行異步派發時,需要拷貝塊。若拷貝塊所用的時間明顯超過執行塊所花的時間,則這種做法將比原來更慢,若派發給隊列的塊要執行更繁重的任務,那么可考慮這種備選方案。
多個獲取方法可以并發進行,而獲取方法和設置方法之間不能并發執行。這次不用串行隊列,改用并行隊列.
在隊列中,柵欄塊必須單獨執行,不能與其他塊并行。這只對并發隊列有意義,因為串行隊列中的塊總是按照順序逐個來執行的。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
-(NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
注意:設置函數也可以改為同步的柵欄塊來實現,那樣做可能更高效,其原因剛才已經解釋過。
第四十二條 多用GCD, 少用performSelector系列方法
下面兩行代碼的執行效果相同
[object performSelector:@selector(selectorName)];
[object selectorName];
在ARC下可能內存泄漏,因為編譯器并不知道將要調用的選擇子是什么,因此,也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚。而且,由于編譯器不知道方法名,所以就沒辦法運行ARC的內存管理規則來判定返回值是不是應該釋放。鑒于此,ARC采用了比較謹慎的做法,就是不添加釋放操作。然而這么做坑能導致內存泄漏,因為方法在返回對象時可能已經將其保留了。
另一個原因,返回值只能是void或對象類型,若返回值的類型為C語言結構體,則不可使用performSelector方法。
延后操作有兩種,優先考慮第二種
[self performSelector:@selector(doSomething)
? withObject:nil
? ? afertDelay:5.0];
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void) {
[self doSomething];
});
要點
performSelecor系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發送給方法的參數個數都受到限制
第四十三條 掌握GCD及操作隊列的使用時機
GCD與操作隊列區別:首先GCD是純C的API,而操作隊列則是Objective-C的對象。
使用NSOperation及NSOperationQueue的好處如下:
取消某個操作。
指定操作間的依賴關系。一個操作可以依賴其他多個操作。開發者能夠指定操作之間的依賴體系,使特定的操作必須在另外一個操作順利執行完畢后方可執行。
通過鍵值觀測機制(KVO)來監聽,比如可以通過isCancelled屬性來判斷任務是否取消,又比如可以通過isFinished屬性來判斷任務是否已完成。
指定操作的優先級。操作的優先級表示此操作與隊列中其他操作之間的優先關系。
重用NSOperation對象。
要點
在解決多線程與任務管理問題時,派發隊列并非唯一方案
操作隊列提供了一套高層的OC API,能實現純GCD所具備的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
第四十四條 通過Dispatch Group機制,根據系統資源狀況來執行任務
創建dispatch group
dispatch_group_t dispatch_group_create();
任務編組
方法一
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
方法二
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
等待dispatch group執行完畢:
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
除了可以用上面的函數外,還可以換個方法,使用下列函數:
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
可以向此函數傳入塊,等dispatch group執行完畢之后,塊會在特定的線程執行。假如當前線程不應阻塞,而開發者又想在那些任務全部完成時得到通知,那么此做法就很有必要了
dispatch_queue_t queue = dispatch_get_globla_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in collection) {
dispatch_group_async(disaptchGroup, queue, ^{
[object performTask];
});
}
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
//Continue processing after completing tasks
若當前線程不應阻塞,則可用notify
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup, notifyQueue, ^{
//Continue processing after completing tasks
});
在前面的范例代碼中,我們遍歷某個collection,并在其每個元素執行任務,而這也可以用另一個GCD函數實現
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void(^block)(size_t));
此函數會反復執行一定次數,每次傳給塊的參數值都會遞增,從0開始,直至iterations - 1
dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectives.queue", NULL);
dispatch_apply(10, queue, ^(size_t i){
//perform task
});
可以用并發隊列
dispatch_apply會持續阻塞,直到所有任務都執行完畢為止。由此可見,假如把塊派給了當前隊列,就講導致鎖死。
第四十五條 使用dispatch_once來執行只需運行一次的線程安全代碼
+sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
要點
經常要編寫“只需執行一次的線程安全代碼”。通過GCD所提供的dispatch_once函數,很容易就能實現此功能。
標記應該聲明在static或global作用域中,這樣的話,在把只需執行一次的塊傳給dispatch_once函數時,穿進去的標記也是相同的。
第四十六條 不要使用dispatch_get_current_queue
該函數有種典型錯誤用法,就是用它檢測當前隊列是不是某個特定的隊列,試圖以此來避免執行同步派發時可能遭遇的死鎖問題。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_blcok_t block = ^{ /*...*/};
if (dispatch_get_current_queue() == queueA) {
block();
}
else {
dispatch_sync(queueA, block);
}
});
});
要解決這個問題,最好的辦法就是通過GCD所提供的功能來設定“隊列特有數據”,此功能可以把任意數據以鍵值對的形式關聯到隊列里。最重要之處在于,假如根據制定的鍵過去不到關聯數據,大么系統就會沿著層級體系向上查找,直至找到數據或到達根隊列為止。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR(”queueA”)
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_blcok_t block = ^{ /*.NO DEADLOCK!*/};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
}
else {
dispatch_sync(queueA, block);
}
});
要點
dispatch_get_current_queue函數的行為常常與開發者所預期的不同。此函數已經廢棄,只應做調試之用。
由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
dispatch_get_current_queue函數用于解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決
第四十七條 熟悉系統框架
將一系列代碼封裝為動態庫,并在其中放入描述其接口的頭文件,這樣做出來的東西就叫框架。有時iOS平臺構建的第三方框架所使用的是靜態庫,這是因為iOS應用程序不允許在其中包含動態庫。iOS系統框架仍然使用動態庫。
Foundation框架中的許多功能,都可以在此框架中找到對應的C語言API。
要點
許多系統框架都可以直接使用。其中最重要的是Foundation 與CoreFoundation,這兩個框架提供了構建應用程序所需的許多核心功能。
很多常見任務都能用框架來做,例如音頻與視頻處理、網絡通信、數據管理等
第四十八條 多用塊枚舉,少用for循環
要點
遍歷collection有四種方式。最基本的辦法是for循環,其次是NSEnumerator遍歷法及快速遍歷法,最新、最先進的方式則是“塊枚舉法”。
“塊枚舉法”本身就能通過GCD來并發執行遍歷操作,無須另行編寫代碼。而采用其他遍歷方式則無法輕易實現這一點。
若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。
第四十九條 對自定義其內存管理語義的collection使用無縫橋接
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
轉換操作的__bridge告訴ARC如何處理轉換所涉及的OC對象。__bridge的意思是:ARC仍然具備這個OC對象的所有權。而__bridge_retained則與之相反,意味著ARC將交出對象的所有權。若是前面那段代碼改用它來實現,那么用完數組之后就要加上CFRelease以釋放其內存。與之相似,反向轉換可以通過__bridge_transfer來實現,例如,想把CFArrayRef轉換為NSArray*,并且想令ARC獲得對象所有權,那么就可以采用這種轉換方式。這三種轉換方式稱為“橋式轉換”。
要點
通過無縫橋接技術,可以在Foundation框架中的OC對象與CoreFoundation框架中的C語言數據結構之間來回轉換。
在CoreFoundation層面創建collection時,可以指定許多回調函數,這些函數表示此collercion應如何處理其元素。然后,可運用無縫橋接技術,將其轉換成劇本特殊內存管理語義的OC collection.
第五十條 構建緩存時選用NSCache而非NSDictionary
NSCache勝過NSDictionary之處在于,當系統資源將要耗盡時,它可以自動刪減緩存。此外,NSCache還會先行刪減“最久未使用的”對象。
NSCache并不會copy鍵,而是會retain它。
原因:很多時候,鍵都時由不支持拷貝操作的對象來充當的。所以說,在鍵不支持拷貝操作的情況下,該類用起來必字典更方便。
另外,NSCache是線程安全的。在開發者自己不編寫加鎖代碼前提下,多個線程便可以同時訪問NSCache.
開發者可以操控緩存刪減其內容的時機。其一是緩存中的對象總數,其二是所有對象的“總開銷”。
#import <Foundation/Foundation.h>
//Network fetcher class
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
//Class that use the network fetcher and caches results
@interface EOCClass : NSobject
@end
@implementation EOCClass {
NSCache *_cache;
}
- (id)init {
if (self = [super init]) {
_cache = [NSCache new];
//Cache a maximum of 100 URLs
_cache.countLimit = 100;
//**
*The Size in bytes of data is used as the cost,
*so this set a cost limit of 5MB
*/
_cache.totalCostLimt = 5 * 1024 *1024;
}
return self;
}
- (void)downloadDataFromURL:(NSURL *)url {
NSData *cachedData = [_cache objectForKey:url];
if (cachedData) {
//Cache hit
[self useData:cachedData];
}
else {
//cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
[_cache setObject:data forKey:url cost data.length];
[self useData:data];
}];
}
}
使用NSPurgeableData
- (void)downloadDataFroURL:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) {
//Cache hit
[cachedData beginContentAccess];
[self useData:cachedData];
[cachedData endContentAccess];
}
else {
//cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost: purgeableData.length];
[self useData:data];
[purgeableData endContentAccess];
}];
}
}
要點
實現緩存時應選用NSCache,因為NSCache可以提供優雅的自動刪減功能,而且是“線程安全的”,此外,它與字典不同,不會拷貝鍵。
可以給NSCache對象設置上限,用以限制緩存中的對象總個數及“總成本”,而這些尺度則定義了緩存刪減其中對象的時機。但是絕對不要把這些尺度當成可靠的“硬限制”,它們僅對NSCache起指導作用。
將NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能,也就是說,當NSPurgeableData對象所占用內存為系統所丟棄時,該對象也會從緩存中移除。
第五十一條 精簡initialize與load的實現代碼
+ (void)load
對于加入運行期系統中的每個類及分類來說,必定會調用此方法,而且僅調用一次。當包含類或分類的程序庫載入系統時,就會執行此方法,而這通常就是指應用程序啟動的時候,若程序是為iOS平臺設計的,則肯定會在此時執行。Mac OS X應用程序更自由一些,它們可以使用“動態加載”之類的特性,等應用程序啟動好之后再去加載程序庫。如果分類和其所屬的類都定義了load,則先調用類里的,再調用分類里的。
load方法的問題在于,執行該方法時,運行期系統處于“脆弱狀態”。在執行子類的load方法之前,必定會先執行所有超類的load方法,而如果代碼還依賴其他程序庫,那么程序庫里相關類的load方法也必定會先執行。然而,根據某個給定的程序庫,卻無法判斷出其中各個類的載入順序。因此,在load方法中使用其他類是不安全的。
分類和其所屬的類里,都可能出現load方法。此時兩種實現代碼都會調用,類的實現要比分類的實現先執行。
想執行與類相關的初始化操作,還有個辦法,就是復寫下列方法:
+ (void)initialize
該方法會在程序首次用該類之前調用,且只調用一次。它是由運行期系統來調用的,絕不應該通過代碼直接調用。其雖與load相似,但卻有幾個非常重要的微妙區別。
1、它是“惰性調用”,只有當程序用到了相關的類時,才會調用。因此,如果某個類一直都沒有使用,那么其initialize方法就一直不會運行。也就是說,應用程序無須先把每個類的initialize都執行一遍,這與load方法不同,對于load來說,應用程序必須阻塞并等著所有類的load都執行完,才能繼續
2、運行期系統在執行該方法時,是處于正常狀態的,此時可以安全使用并調用任意類中的任意方法。而且,運行期系統也能確保initialize方法一定會在“線程安全的環境”中執行,只有執行initialize那個線程可以操作類或類實例。其他線程都要先阻塞,等著initialize執行完。
3、initialize方法與其他消息一樣,如果某個類未實現它,而其超類實現了,那么就會運行超類的實現代碼。
要點
在加載階段,如果類實現了load方法,那么系統就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。與其他方法不同,load方法不參與覆寫機制。
首次使用某個類之前,系統會向其發送initialize消息。由于此方法尊從普通的覆寫規則,所以通常應該在里面判斷當前要初始化的是哪個類。
load與initialize方法都應該實現得精簡一些,有助于保持應用程序的響應能力,也能減少引入“依賴環”的幾率。
無法在編譯期設定的全局常量,可以放在initialize方法里初始化。
第五十二條 別忘了NSTimer會保留其目標對象
計時器會保留其目標對象,等到自身“失效”時再釋放此對象。調用invalidate方法可令計時器失效;執行完相關任務后,一次性的計時器也會失效。
要點
NSTimer對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計時器失效,另外,一次性的計時器在觸發完任務之后也會失效。
反復執行任務的計時器,很容易引起保留環,如果這種計時器的目標對象又保留了計時器本身,那肯定會導致保留環。這種環狀保留關系,可能是直接發生的,也可能是通過對象圖里的其他對象間接發生的。
可以擴充NSTimer的功能,用“塊”來打破保留環。不過,除非NSTimer將來在公共接口里提供此功能,否則必須創建分類,將相關實現代碼加入其中。