二、對(duì)象、消息、運(yùn)行期
9、以“類族模式”隱藏實(shí)現(xiàn)細(xì)節(jié)
“類簇”(class cluster)是一種很有用的模式(pattern),可以隱藏“抽象基類”(abstract base class)背后的實(shí)現(xiàn)細(xì)節(jié)。OC的系統(tǒng)框架中普遍使用此模式,大部分的collection類都是類簇,如:UIButton、NSArray、NSMutableArray等:
// 返回的對(duì)象,其類型取決于傳入的按鈕類型(button type)
// 不管返回什么類型的對(duì)象,都是繼承自同一個(gè)基類:UIButton
// 意義:使用者無(wú)須關(guān)心屬于哪個(gè)類,和其繪制等實(shí)現(xiàn)細(xì)節(jié),只須知道如何創(chuàng)建,如何設(shè)置,如何使用就好
+ (UIButton *)buttonWithType:(UIButtonType)type;
// 如果放在同一個(gè)類里,你可能會(huì)寫出這樣的代碼:
- (void)drawRect:(CGRect)rect {
if (_type == TypeA) {
// Draw TypeA button
} else if (_type == TypeB) {
// Draw TypeB button
}
// 但是當(dāng)類型和方法越來(lái)越多時(shí),這樣做救護(hù)很麻煩
}
優(yōu)秀的程序猿會(huì)重構(gòu)為多個(gè)子類,把各個(gè)按鈕所用的繪制方法放到相關(guān)子類中去。不過(guò)需要用戶知道各種子類才行,此時(shí)應(yīng)使用“類簇模式”:可以靈活應(yīng)對(duì)多個(gè)類,講他們的實(shí)現(xiàn)細(xì)節(jié)隱藏在抽象基類后面,以保持接口簡(jiǎn)潔。用戶無(wú)須自己創(chuàng)建子類實(shí)例,只需調(diào)用基類方法來(lái)創(chuàng)建即可。
創(chuàng)建類簇:
// .h:聲明枚舉、屬性、方法。。。
// .m:實(shí)現(xiàn)初始化方法:工程模式,是創(chuàng)建類簇的方式之一
+ (MOEmployee *)employeeWithType:(MOEmployeeType)type {
swith (type) {
case MOEmployeeTypeDeveloper: return [MOEmployeeDeveloper new];
case MOEmployeeTypeDesigner: return [MOEmployeeDesigner new];
case MOEmployeeTypeFinance: return [MOEmployeeFinance new];
}
}
- (void)doWork {
// 子類們實(shí)現(xiàn)各自的工作內(nèi)容
}
// 子類繼承MOEmployee,并實(shí)現(xiàn)doWork方法。。。
// 另外注意:
[anEmployee isMemberOfClass:[MOEmployee class]] // 永遠(yuǎn)為NO!!!
Cocoa里的類簇:
你要是知道NSArray是個(gè)類簇,就不會(huì)寫出下面第一行這樣的代碼,[anArray class]所返回的類絕對(duì)不可能是NSArray類本身?。?!
if ([anArray class] == [NSArray class]) {} // 永遠(yuǎn)為NO!!!
// Array的初始化方法返回的是隱藏的某個(gè)內(nèi)部類型
// 不過(guò)可以這樣判斷是都屬于其類簇中
if ([anArray isKindOfClass:[NSArray class]]) {} // YES
想為類簇新增子類,需要遵循幾條規(guī)則:
?子類應(yīng)該繼承自類簇中的抽象基類(如:不可變數(shù)組的基類 or 可變數(shù)組的基類)
?子類應(yīng)該定義自己的數(shù)據(jù)存儲(chǔ)方式(如:Array子類,可以用Array來(lái)存儲(chǔ),Array不會(huì)自動(dòng)保存數(shù)據(jù))
?子類應(yīng)當(dāng)覆寫超類文檔中指明需要覆寫的方法(如:Array的count、objectAtIndex:)
要點(diǎn):
?類簇模式可以把實(shí)現(xiàn)細(xì)節(jié)隱藏在一套簡(jiǎn)單的公共接口后面
?系統(tǒng)框架中經(jīng)常使用類簇
?從類簇的公共抽象基類中繼承子類時(shí)要當(dāng)心,若有開(kāi)發(fā)文檔,則應(yīng)首先閱讀
10、在既有類中使用關(guān)聯(lián)對(duì)象存放自定義數(shù)據(jù)
可以給某對(duì)象關(guān)聯(lián)許多其他對(duì)象,這些對(duì)象通過(guò)“鍵”來(lái)區(qū)分。存儲(chǔ)對(duì)象值的時(shí)候,可以指明“存儲(chǔ)策略”(storage policy),用以維護(hù)相應(yīng)的“內(nèi)存管理語(yǔ)義”。策略有名為objc_AssociationPolicy的枚舉所定義:
<colgroup><col width="auto"><col width="auto"></colgroup>
| objc_AssociationPolicy關(guān)聯(lián)類型 | 等效的@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 |
以下方法可以管理關(guān)聯(lián)對(duì)象:
?void objc_setAssociatedObject(id object, void* key, id value, objc_AssociationPolicy policy)此方法以給定的鍵和策略為某對(duì)象設(shè)置關(guān)聯(lián)對(duì)象值
?id objc_getAssociatedObject(id object, void* key)此方法根據(jù)給定的鍵從某對(duì)象中獲取相應(yīng)的關(guān)聯(lián)對(duì)象值
?void objc_removeAssociatedObject(id object)此方法移除指定對(duì)象的全部關(guān)聯(lián)對(duì)象
可以把某對(duì)象想像成NSDictionary,把關(guān)聯(lián)到該對(duì)象的值理解為字典中的條目,存儲(chǔ)關(guān)聯(lián)對(duì)象的值就相當(dāng)于在字典對(duì)象調(diào)用setObject:valueforKey:和objectForKey:方法。然而兩者之間有個(gè)重要差別:設(shè)置關(guān)聯(lián)對(duì)象時(shí)用的鍵(key)時(shí)一個(gè)“不透明的指針”(opaque pointer)。如果在兩個(gè)鍵上調(diào)用isEqual:方法返回YES,那么字典人為二者相等;而在設(shè)置關(guān)聯(lián)對(duì)象值時(shí),若向令兩個(gè)鍵匹配到同一個(gè)字,則二則必須是完全相同的指針才行。鑒于此,在設(shè)置關(guān)聯(lián)對(duì)象值時(shí),通常使用靜態(tài)全局變量做鍵。
要點(diǎn):
?可以通過(guò)“關(guān)聯(lián)對(duì)象”機(jī)制來(lái)把兩個(gè)對(duì)象連起來(lái)
?定義關(guān)聯(lián)對(duì)象時(shí)可指定內(nèi)存管理語(yǔ)義,用以模仿定義屬性時(shí)所采用的“擁有關(guān)系”與“非擁有關(guān)系”
?只有在其他做法不可行時(shí)才應(yīng)選用關(guān)聯(lián)對(duì)象,因?yàn)檫@種做法通常會(huì)引入難于查找的bug
11、理解objc_msgSend的作用
上面講述的自描述了部分消息的調(diào)用過(guò)程,其他“邊界情況”(edge case)則需要交由Objective-C運(yùn)行環(huán)境中的另一些函數(shù)來(lái)處理:
?objc_msgSend_stret:消息返回結(jié)構(gòu)體時(shí)調(diào)用此方法。(當(dāng)CPU的寄存器能容納的心返回值類型時(shí),否則用的是另一個(gè)函數(shù))
?objc_msgSend_fpret:消息返回浮點(diǎn)數(shù)時(shí)調(diào)用此方法。需要對(duì)“浮點(diǎn)數(shù)寄存器”(floating-point register)做特殊處理。
?objc_msgSendSuper:消息是給超類發(fā)的。如:[super someMethod:parameter]。也有另外兩個(gè)與objc_msgSend_stret和objc_msgSend_fpret等效的函數(shù),用于處理發(fā)給super的相應(yīng)消息。
還有一個(gè)概念需要理解一下:“尾調(diào)用優(yōu)化”(tail-call optimization)技術(shù):
如果某函數(shù)的最后一項(xiàng)操作是調(diào)用另外一個(gè)函數(shù)且不會(huì)返回值另作他用時(shí),那么就可以運(yùn)用“尾調(diào)用優(yōu)化”技術(shù)。編譯器會(huì)生成調(diào)轉(zhuǎn)至另一函數(shù)所需的指令碼,且不會(huì)向調(diào)用堆棧中推入新的“棧幀”(frame stack)。(這項(xiàng)優(yōu)化對(duì)objc_msgSend非常關(guān)鍵,若不做優(yōu)化的話,每次調(diào)用OC方法前,都需要為objc_msgSend函數(shù)準(zhǔn)備“棧幀”(可以在“棧蹤跡”stack trace中看到),還會(huì)過(guò)早地發(fā)生“棧溢出”(stack overflow)現(xiàn)象)。
明白這一點(diǎn),就能理解為何在在調(diào)試的時(shí)候,?!盎厮荨保╞acktrace)信息中總是出現(xiàn)objc_msgSend了。
要點(diǎn):
?消息由接收者、選擇子、參數(shù)構(gòu)成。給某對(duì)象“發(fā)送消息”(invoke a message)也就相當(dāng)于在該對(duì)象上“調(diào)用方法”(call a method)。
?發(fā)給某對(duì)象的全母消息都要用“動(dòng)態(tài)消息派發(fā)系統(tǒng)”(dynamic message dispatch system)來(lái)處理,該系統(tǒng)會(huì)查出對(duì)應(yīng)的方法,并執(zhí)行其代碼。
12、理解消息轉(zhuǎn)發(fā)機(jī)制
要點(diǎn):
?若對(duì)象無(wú)法響應(yīng)某個(gè)選擇子,則進(jìn)入消息轉(zhuǎn)發(fā)流程
?通過(guò)運(yùn)行期的動(dòng)態(tài)方法解析功能,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中
?對(duì)象可以把其無(wú)法解讀的某些選擇子轉(zhuǎn)交給其他對(duì)象來(lái)處理
?經(jīng)過(guò)上述兩步之后,如果還是沒(méi)辦法處理選擇子,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制
13、用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
類的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)上,使得“動(dòng)態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來(lái)表示,這種指針叫IMP,其原型如下:
id (*IMP)(id, SEL, ...)
“方法調(diào)配”(method swizzling)
// 獲取方法實(shí)現(xiàn)
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換兩個(gè)方法的實(shí)現(xiàn)
void method_exchangeImplementations(Method m1, Method m2)
要點(diǎn):
?在運(yùn)行期,可以向類中新增或替換選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn)
?使用另一份實(shí)現(xiàn)來(lái)替換原有的方法實(shí)現(xiàn),這到工序叫做“方法調(diào)配”,開(kāi)發(fā)者常用此技術(shù)向原有實(shí)現(xiàn)中添加新功能
?一般來(lái)說(shuō),只有調(diào)試程序時(shí)才需要在運(yùn)行期修改方法實(shí)現(xiàn),這種做法不易濫用
14、理解“類對(duì)象”的用意
“在運(yùn)行期檢視對(duì)象類型”這一操作也叫做“類型信息查詢”(introspection,“內(nèi)省”),這一個(gè)強(qiáng)大而有用的特性內(nèi)置于Foundation框架的NSObject協(xié)議里,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來(lái)的對(duì)象都要遵從此協(xié)議。在程序中不要直接比較對(duì)象所屬的類,明智的做法是調(diào)用“類型信息查詢方法”。
OC對(duì)象的本質(zhì):
// 每一個(gè)OC對(duì)象實(shí)例都是指向某塊內(nèi)存數(shù)據(jù)的指針。所以在聲明變量時(shí),類型后面都要跟一個(gè)`*`字符
NSString *pointerVariable = @"Hello world";
// 對(duì)于通用對(duì)象類型`id`,由于其本身已經(jīng)是指針了,所以我們能夠這樣寫
id genericTypeString = @"Hello world"
上面兩種定義方式區(qū)別在于:
第一種聲明時(shí)指定了具體類型,當(dāng)調(diào)用其沒(méi)有的方法時(shí),編譯器會(huì)發(fā)出警告??。
OC對(duì)象所用的數(shù)據(jù)結(jié)構(gòu)定義在運(yùn)行期程序庫(kù)的頭文件里,id類型本身也定義在這里:
typedef struct objc_object {
Class isa,
} *id;
由此可見(jiàn),每個(gè)對(duì)象結(jié)構(gòu)體的收個(gè)成員是Class類的變量。該變量定義了對(duì)象所屬的類,通常稱為“is a”指針。(如:string對(duì)象“是一個(gè)”(is a)NSString,所以其“is a”指針就指向NSString)
Class對(duì)象也定義在運(yùn)行期程序庫(kù)的都文件中:
typedef struct objc_class *Class;
struct objc_class { // 吃結(jié)構(gòu)體存放類的“元數(shù)據(jù)”(metadata)
Class isa; // 說(shuō)明Class本身也是OC對(duì)象
Class super_class; // 本類的超類
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars; // 實(shí)例變量
struct objc_method_list **methodLists; // 方法
struct objc_cahce *cache;
struct objc_protocol_list *protocols;
}
類對(duì)象所屬的類型(即isa指針指向的類型)是另外一個(gè)類,叫“元類”(metaclass),用來(lái)表述類本身所劇本的元數(shù)據(jù)?!邦惙椒ā本投x于此處,這些方法可以理解為類對(duì)象的實(shí)例方法。每個(gè)類僅有一個(gè)“類對(duì)象”,每個(gè)“類對(duì)象”僅有一個(gè)與之相關(guān)的“元類”。
假如有一個(gè)名為SomeClass的子類繼承自NSObject,則其繼承體系如下:
[圖片上傳失敗...(image-b95f9e-1694433945327)]
super_class指針確立了繼承關(guān)系,而isa指針描述了實(shí)例所屬的類。通過(guò)這張布局關(guān)系圖即可執(zhí)行“類信息查詢”。可以查出對(duì)象是否能響應(yīng)某個(gè)選擇子,是否遵從某項(xiàng)協(xié)議,看出此對(duì)象位于“類繼承體系”(class hierarchy)的哪一部分。
在類繼承體系中查詢類型信息:
?isMemberOfClass::判斷對(duì)象是否為某個(gè)特定類的實(shí)例
?isKindOfClass::判斷對(duì)象是否為某類/其派生類的實(shí)例
使用isa指針獲取對(duì)象所屬的類,然后通過(guò)super_class指針在繼承體系中游走。由于OC對(duì)象是動(dòng)態(tài)的,所以此特性顯得極為重要。從collection中取出的對(duì)象類型通常是id的,可以使用類型信息查詢方法。如:
NSMutableString *str = [NSMutableString new];
for (id obj in array) {
if ([obj isKindOfClass:[NSString class]]) {
[str appendFormat:@"%@", obj];
} else if ([obj isKindOfClass:[NSNumber class]]) {
[str appendFormat:@"%d", [obj intValue]];
} else {
// ...
}
}
不應(yīng)該直接比較兩個(gè)類對(duì)象是否等同,如:某個(gè)對(duì)象可能會(huì)把收到的所有選擇子都轉(zhuǎn)發(fā)給另外一個(gè)對(duì)象。這樣的對(duì)象叫做“代理”(proxy),通常繼承自NSProxy。通常在此對(duì)象上調(diào)用class方法,返回的是代理對(duì)象本身(NSProxy的子類),而非接受代理的對(duì)象所屬的類。(用isKindOfClass:這樣的類型信息查詢方法,代理對(duì)象會(huì)轉(zhuǎn)發(fā)給“接受代理的對(duì)象”;而用class返回的是發(fā)起代理的對(duì)象,而非“接受代理的對(duì)象”)。
要點(diǎn):
?每個(gè)實(shí)例都有一個(gè)指向Class對(duì)象的指針,用以表明其類型,這些Class對(duì)象構(gòu)成了類的繼承體系
?如果對(duì)象類型無(wú)法在編譯期確定,那么就應(yīng)該使用類型信息查詢方法來(lái)探知
?盡量使用類型信息查詢方法來(lái)確定對(duì)象類型,而不要直接比較類對(duì)象,因?yàn)槟承?duì)象可能實(shí)現(xiàn)了消息轉(zhuǎn)發(fā)功能
三、接口與API設(shè)計(jì)
15、用前綴避免命名空間沖突
要點(diǎn):
?選擇與你的公司、應(yīng)用程序或二者皆有關(guān)聯(lián)之名稱作為類名的前綴,并在所有代碼中均使用這一前綴
?若自己所開(kāi)發(fā)的程序庫(kù)中用到了第三方庫(kù),則應(yīng)為其中的名稱加上前綴
16、提供“全能初始化方法”
全能初始化方法 or 指定初始化方法 Designated initializer
要點(diǎn):
?在類中提供一個(gè)全能初始化方法,并于文檔里指明。其他初始化方法均應(yīng)調(diào)用此方法
?若全能初始化方法與超類不同,則需覆寫超類中的對(duì)應(yīng)方法
?如果超類的初始化方法不適用于子類,那么應(yīng)該覆寫這個(gè)超類方法,并在其中拋出異常
17、實(shí)現(xiàn)description方法
首先description方法是定義在NSObject協(xié)議里的,然后NSObject和NSProxy倆“根類”都遵循了該協(xié)議,并有默認(rèn)實(shí)現(xiàn):打印類名和內(nèi)存地址(如:<Person: 0x7f9a1600600>)。
用NSLog+%@打印時(shí)調(diào)用的是description方法
程序運(yùn)行打斷點(diǎn)時(shí),在調(diào)試控制臺(tái)輸入LLDB的po命令,調(diào)用的是debugDescription方法
可以實(shí)現(xiàn)如下:
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
} // Bob Smith
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p,\" %@ %@\">", [self class], self, _firstName, _lastName];
} // (Person *) $1 = 0x07117fb0 <Person: 0x7117fb0, "Bob Smith">
要點(diǎn):
?實(shí)現(xiàn)description方法返回一個(gè)有意義的字符串,用以描述該實(shí)例
?若想在調(diào)試時(shí)打印出更詳盡的對(duì)象描述信息,則應(yīng)實(shí)現(xiàn)debugDescription方法
18、盡量使用不可變對(duì)象
如8條,若把可變對(duì)象放到collection之后又修改其內(nèi)容,那么很容易就會(huì)破壞set的內(nèi)部數(shù)據(jù)結(jié)構(gòu),使其失去固有的語(yǔ)義。因此,建議大家盡量減少對(duì)象中的可變內(nèi)容。
盡量把對(duì)外公布出來(lái)的屬性設(shè)為只讀,而且只在確有必要時(shí)才將屬性對(duì)外公布。
如:person有一個(gè)friends全部朋友的屬性,放在一個(gè)“列表”(list)里外界可以增刪。通常應(yīng)該提供一個(gè)readonly屬性返回不可變set(內(nèi)部可變set的copy)供外界使用。
// .h
@property (nonatomic, strong, readonly) NSSet *friends;
- (void)addFriend:(Person *)person;
- (void)removeFriend:(Person *)person;
// .m
@property (nonatomic, strong, readwrite) NSSet *friends;
NSMutableSet *_interalFriends;
- (NSSet)firends {
return [_interalFriends copy]; // 拷貝不可變版本
}
- (void)addFriend:(Person *)person {
[_interalFriends addObject:person];
}
- (void)removeFriend:(Person *)person {
[_interalFriends removeObject:person];
}
如果直接提供可變版本NSMutableSet供外部使用,不是借助addFriend:與removeFriend:方法,而是直接操作此屬性。這種過(guò)分解耦數(shù)據(jù)的做法很容易出bug。如:在添加或刪除朋友時(shí),Person對(duì)象可能還要執(zhí)行其他操作,此時(shí)就等于直接從底層修改了其內(nèi)部用于存放朋友對(duì)象的set。在Person對(duì)象不知情時(shí),直接從底層修改set可能會(huì)令對(duì)象內(nèi)的各個(gè)數(shù)據(jù)之間互不一致。
要點(diǎn):
?盡量創(chuàng)建不可變的對(duì)象
?若某屬性僅可于對(duì)象內(nèi)部修改,則在class-continuation分類中將其由readonly屬性擴(kuò)展為readwrite屬性
?不要把可變的collection作為屬性公開(kāi),而應(yīng)提供相關(guān)方法修改對(duì)象中的可變collection
19、使用清晰而協(xié)調(diào)的命名方式
1、方法命名:
?如果方法的返回值是新創(chuàng)建的,那么方法名的首個(gè)詞應(yīng)該是返回值的類型。也可以看情況在前面添加修飾語(yǔ),如:localizedString。屬性的存取方法不遵循這種命名方式~
?應(yīng)該把表示參數(shù)類型的名詞放在參數(shù)前面
?如果方法要在當(dāng)前對(duì)象上執(zhí)行操作,那么就應(yīng)該包含動(dòng)詞;若執(zhí)行操作時(shí)還需要參數(shù),則應(yīng)該在動(dòng)詞后面加上一個(gè)或多個(gè)名詞
?不要使用str這種簡(jiǎn)稱,應(yīng)該用string這樣的全稱
?Boolean屬性應(yīng)加is前綴。如過(guò)某個(gè)方法返回Boolean值,應(yīng)根據(jù)其功能添加has或is前綴
?將set這個(gè)前綴留給那些借由輸出參數(shù)來(lái)保存返回值的方法,比如說(shuō),把返回值填充到C言語(yǔ)式數(shù)組(C-stye array)里的那種方法就可以使用這個(gè)詞做前綴
2、類與協(xié)議的命名
應(yīng)該為類與協(xié)議的名稱加上前綴,以避免命名空間沖突,而且應(yīng)該像給方法起名時(shí)那樣把詞句組織好,使其從左至右讀起來(lái)較為通順。
要點(diǎn):
?起名時(shí)應(yīng)遵從標(biāo)準(zhǔn)的Objective-C命名規(guī)范,這樣創(chuàng)建出來(lái)的接口更容易為開(kāi)發(fā)者所理解
?方法名要言簡(jiǎn)意賅,從左至右讀起來(lái)要像個(gè)日常用語(yǔ)中的句子才好
?方法名里不要使用縮略后的類型名稱
?給方法起名時(shí)第一要?jiǎng)?wù)就是確保其風(fēng)格與你自己的代碼或所要繼承的框架相符
20、為私有方法名加前綴
要點(diǎn):
?給私有方法的名稱加上前綴,這樣可以很容易的將其同公共方法區(qū)分開(kāi)
?不要單用一個(gè)下劃線做私有方法的前綴,因?yàn)檫@種做法是預(yù)留給蘋果公司用的
21、理解Objective-C錯(cuò)誤模型
Error對(duì)象里封裝了三條信息:
?Error domain:錯(cuò)誤范圍,字符串
產(chǎn)生錯(cuò)誤的根源,通常用一個(gè)特有的全局變量來(lái)定義。如:NSURLError表示解析URL出錯(cuò)
?Error code:錯(cuò)誤碼,整數(shù)
獨(dú)有的錯(cuò)誤碼,指明在某個(gè)范圍內(nèi)具體發(fā)生了何種錯(cuò)誤,通常用enum定義。如:HTTP請(qǐng)求出錯(cuò)時(shí),可能回把HTTP的狀態(tài)碼設(shè)為錯(cuò)誤碼
?User info:用戶信息,字典
有關(guān)此錯(cuò)誤的額外信息,其中或許包含一段“本地化的描述”(localized description),或許還含有導(dǎo)致該錯(cuò)誤發(fā)生的另外一個(gè)錯(cuò)誤,經(jīng)由此種信息,可將相關(guān)錯(cuò)誤串成一條“錯(cuò)誤鏈”(chain of errors)
建議:
?為自己的程序庫(kù)中所發(fā)生的錯(cuò)誤制定一個(gè)專用的“錯(cuò)誤范圍”字符串
?用枚舉定義錯(cuò)誤碼,不僅解釋錯(cuò)誤碼的含義,還給它們起了個(gè)有意義的名字
要點(diǎn):
?只有發(fā)生了會(huì)使整個(gè)應(yīng)用程序崩潰的嚴(yán)重錯(cuò)誤時(shí),才使用異常
?在錯(cuò)誤不那么嚴(yán)重的情況下,可以指派“委托方法”(delegate method)來(lái)處理錯(cuò)誤,也可以把錯(cuò)誤信息放在NSError對(duì)象里,經(jīng)由“輸出參數(shù)”返回給調(diào)用者
22、理解NSCoping協(xié)議
Foundation框架中的所有collection類在默認(rèn)情況下都執(zhí)行淺拷貝,即只拷貝容器對(duì)象本身,而不復(fù)制其中數(shù)據(jù)。(主要是因?yàn)椋喝萜骼锏膶?duì)象未必都能拷貝,而且調(diào)用者尾部想在拷貝容器時(shí)一并拷貝其中的每個(gè)對(duì)象)
另外,不要假定遵從了NSCopying協(xié)議的對(duì)象都會(huì)執(zhí)行深拷貝。在絕大多數(shù)情況下執(zhí)行的都是淺拷貝。如果需要在某個(gè)對(duì)象上執(zhí)行深拷貝,那么除非該類的文檔說(shuō)它是用深拷貝來(lái)實(shí)現(xiàn)的NSCopying協(xié)議,否則:要么尋找能夠指向深拷貝的相關(guān)方法,要么自己編寫方法實(shí)現(xiàn)。
要點(diǎn):
?若想令自定義的對(duì)象具有拷貝功能,則需實(shí)現(xiàn)NSCopying協(xié)議
?若自定義對(duì)象分為可變版本與不可變版本,則應(yīng)同事實(shí)現(xiàn)NSCopying與NSMutableCopying協(xié)議
?復(fù)制對(duì)象時(shí)需決定采用淺拷貝還是深拷貝,一般情況下應(yīng)盡量執(zhí)行淺拷貝
?若自定義對(duì)象需要深拷貝,那么可考慮新增一個(gè)專門執(zhí)行深拷貝的方法,如deepCopy
四、協(xié)議與分類
23、通過(guò)委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信
“委托模式”/“代理模式”(Delegate pattern)主旨:
定義一套接口,其他對(duì)象若想接收當(dāng)前對(duì)象的委托,則需遵從此接口成為其“委托對(duì)象”(delegate)。而當(dāng)前對(duì)象則可以給其委托對(duì)象傳遞一些信息,也可以在放生相關(guān)事件時(shí)通知委托對(duì)象。(此模式可將數(shù)據(jù)與業(yè)務(wù)邏輯解耦)
通常情況下delegate對(duì)象會(huì)持有當(dāng)前對(duì)象,所以需要將delegate屬性定義成weak,否則會(huì)造成循環(huán)引用導(dǎo)致內(nèi)存泄露。
若要向外界公布此類實(shí)現(xiàn)了某協(xié)議,那么就在接口(.h文件)中聲明;而如果這個(gè)協(xié)議是個(gè)委托協(xié)議的話,就可以在類內(nèi)部(class-continauation分類中)聲明。
委托協(xié)議中的方法一般都是“可選的”(用@optional聲明,后面的都是可選),因?yàn)榇砦幢仃P(guān)心其中的所有方法。
在調(diào)用delegate中的方法時(shí),總是應(yīng)該把當(dāng)前對(duì)象也一并傳入方法中,這樣delegate在實(shí)現(xiàn)相關(guān)方法時(shí),就能根據(jù)傳入的實(shí)例分別執(zhí)行不同的代碼了。如:
- (void)networkFetcher:(NetworkFetcher *)fetcher
didReceiveDate:(NSData *)data {
if (fetcher == _myFetcherA) {
} else if (fetcher == _myFetcherB) {
}
}
可以在當(dāng)前對(duì)象中聲明一個(gè)含有位段的結(jié)構(gòu)體為其實(shí)例對(duì)象,結(jié)構(gòu)體中的每個(gè)位段表示delegate是否實(shí)現(xiàn)了協(xié)議中的相關(guān)方法:
@interface NetworkFetcher () {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlages; // 實(shí)例對(duì)象!!!
}
// 設(shè)置代理時(shí),就緩存當(dāng)前delegate是否能響應(yīng)協(xié)議中的相關(guān)方法
- (void)setDelegate:(id< NetworkFetcher>)delegate {
_delegate = delegate;
_delegateFlages.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
....
}
協(xié)議方法要調(diào)用很多次時(shí),值得進(jìn)行這種優(yōu)化。而是否需要優(yōu)化,則應(yīng)依照具體代碼來(lái)定。這種需要分析代碼性能,并找出瓶頸,若發(fā)現(xiàn)執(zhí)行速度需要改進(jìn),則可使用此技巧。
要點(diǎn):
?委托模式為對(duì)象提供了一套接口,使其可由此將相關(guān)事件告知其他對(duì)象
?將委托對(duì)象應(yīng)該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的時(shí)間定義成方法
?當(dāng)某對(duì)象需要從另外一個(gè)對(duì)象中獲取數(shù)據(jù)時(shí),可使用委托模式。這種情境下,該模式亦稱“數(shù)據(jù)源協(xié)議”(data source protocal)
?若有必要,可實(shí)現(xiàn)含有段位的結(jié)構(gòu)體,將委托對(duì)象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中
24、將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中
類的基本要素(諸如:屬性、初始化方法等)都聲明在“主實(shí)現(xiàn)”(main implementation)里。執(zhí)行不同類型的操作所用的另外幾套方法則歸入各個(gè)分類中。
處在分類中的所有方法,其符號(hào)中會(huì)包含分類名。如:addFriend:方法的“符號(hào)名”(symbol name)如下:
- [Person(Frendship) addFriend:]
根據(jù)回溯信息中的分類名稱,很容易就能精確定位到類中的方法所屬的功能區(qū),這對(duì)于某些應(yīng)該視為私有的方法來(lái)說(shuō)更是極為有用。
在編寫準(zhǔn)備分享給其他開(kāi)發(fā)者使用的程序庫(kù)時(shí),可以考慮創(chuàng)建Private分類。經(jīng)常會(huì)有些方法:他們不是公共API的一部分,然而確非常適合在程序庫(kù)之內(nèi)使用。將其放入Private分類中,哪里用到就引入。而分類的頭文件不隨公共API一并公開(kāi)。這樣使用者舊不知道庫(kù)里還有這些私有方法了。
要點(diǎn):
?使用分類機(jī)制把類的實(shí)現(xiàn)代碼劃分成易于管理的小塊,以便單獨(dú)檢視
?將應(yīng)該視為“私有”的方法歸入名教Private的分類中,已隱藏實(shí)現(xiàn)細(xì)節(jié)
25、總是為第三方類的分類名稱加前綴
因?yàn)镺C中沒(méi)有命名空間這一概念,所以只能用給類名/方法名添加前綴的方式實(shí)現(xiàn)。一般來(lái)說(shuō)這個(gè)前綴應(yīng)該與當(dāng)前項(xiàng)目/當(dāng)前模塊相同。如:
@interface NSString (XXX_HTTP) // 為分類名添加前綴!!!!
- (NSString *)xxx_urlEncodeString; // 為分類方法名名添加前綴!!!
- (NSString *)xxx_urlDecodeString;
要點(diǎn):
?向第三方類中添加分類時(shí),總應(yīng)給其名稱和方法名加上你專用的前綴
26、勿在分類中聲明屬性
雖然說(shuō)我們可以在分類中運(yùn)用runtime實(shí)現(xiàn)關(guān)聯(lián)對(duì)象的方式,實(shí)現(xiàn)屬性(實(shí)現(xiàn)方式之前這篇文章的@dynamic部分有寫)。但是這么做不理想。要把相似的代碼寫很多邊,而且在內(nèi)存管理問(wèn)題上容易出錯(cuò)(如:當(dāng)修改了某個(gè)屬性的特質(zhì)attrubute時(shí),還要記得修改setter方法中設(shè)置關(guān)聯(lián)對(duì)象時(shí)所用的內(nèi)存管理語(yǔ)義)。
分類的目標(biāo)在于擴(kuò)展類的功能,而非封裝數(shù)據(jù)。
但有時(shí)候只讀屬性還是可以在分類中使用的。如:為NSCalendar類創(chuàng)建分類,返回各個(gè)月份名稱數(shù)組。雖說(shuō)僅是訪問(wèn)數(shù)據(jù)不需要實(shí)例變量來(lái)實(shí)現(xiàn)。但此時(shí)最好不要用屬性,用一個(gè)方法就好。因?yàn)閷傩员磉_(dá)的意思是:類中有數(shù)據(jù)在支持著它。
@interface NSCalendar (XXX_Additions)
- (NSArray *)xxx_allMothns;
@end
@implementation NSCalendar (XXX_Additions)
- (NSArray *)xxx_allMothns {
if ([self.calendar isEqualToString:NSGegorianCalendar]) {
return @[@"January", @"February", ...];
} else if (/* other calendar identifiers */) {
/* return months for other calendars */
}
}
@end
要點(diǎn):
?把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里
?在"class-continuation分類"之外的其他分類中,可以定義存取方法,但盡量不要定義屬性
27、使用“class-continuation”分類隱藏實(shí)現(xiàn)細(xì)節(jié)
將使用到的C++文件在實(shí)現(xiàn)文件中導(dǎo)入,僅使實(shí)現(xiàn)文件擴(kuò)展名為.mm,使用OC++編譯。頭文件仍就是.h,使用OC編譯。從而實(shí)現(xiàn)隱藏C++代碼的效果。如系統(tǒng)的WebKit和CoreAnimation就用到了此模式,內(nèi)部很多都用C++寫成,但對(duì)外公布的卻是一套純OC接口
要點(diǎn):
?通過(guò)“class-continuation分類”向類中新增實(shí)例變量
?如果某屬性在主接口中什么為“只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那么就在“class-continuation分類”中將其擴(kuò)展為“可讀寫”
?把私有方法的原型聲明在“class-continuation分類”里
?若想是使類遵循的協(xié)議不為人所知,則可以于“class-continuation分類”中聲明
28、通過(guò)協(xié)議提供匿名對(duì)象
若接口背后有多個(gè)不同的實(shí)現(xiàn)類,而你有不想指明具體使用哪個(gè)類,那么可以考慮使用遵從某協(xié)議的純id類型—>因?yàn)橛袝r(shí)候這些類可能會(huì)邊,有時(shí)候它們又無(wú)法容納于標(biāo)準(zhǔn)的類繼承體系中,因而不能以某個(gè)公共基類來(lái)統(tǒng)一表示。
如:代理(類型不重要,只要遵循協(xié)議就行)
@property (nonatomic, weak) id <XXXDelegate> delegate;
1
如:NSDictionary設(shè)置鍵值對(duì)方法
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
1
如:處理數(shù)據(jù)庫(kù)連接的程序庫(kù),以匿名對(duì)象來(lái)表示從另一個(gè)庫(kù)中返回的對(duì)象
@protocol XXXDatabaseConnection
- (void)connect;
- (void)disconnect;
- (void)isConnect;
- (NSArray *)performQuery:(NSString *)query;
@end
@interface XXDataBaseManager: NSObject
+ (id)sharedInstance;
// 返回連接對(duì)象
- (id<XXXDatabaseConnection>)connectionWithIdentifier:(NSString)identifier;
@end
如此,處理數(shù)據(jù)庫(kù)連接所用的類名就不會(huì)泄露,可能來(lái)自不同款家的類型現(xiàn)在均可用同一個(gè)方法返回了。
再如:CoreData框架在負(fù)責(zé)查詢接口的NSFetchedResultsContoller中,有個(gè)sections屬性表示數(shù)據(jù)分區(qū),是個(gè)數(shù)組如下:
NSArray *sections = controller.sections;
id <NSFecthedResultsSectionInfo> sectionInfo = section[section];
NSUInteger numberOfObjects = sectionInfo.numberOfObjects;
在幕后,此對(duì)象可能使由處理結(jié)果的控制器所創(chuàng)建的內(nèi)部對(duì)象,沒(méi)必要把表示翅中數(shù)據(jù)的類對(duì)外公布,因?yàn)槭褂每刂破鞯娜私^對(duì)不關(guān)心查詢結(jié)果中的數(shù)據(jù)分區(qū)使如何保存的,他們只要知道可以在這些對(duì)象上查詢數(shù)據(jù)就行
要點(diǎn):
?協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的id類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法(如果具體類型不重要,重要的使對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來(lái)表示)
?使用匿名對(duì)象來(lái)隱藏類型名稱(或類名)
五、內(nèi)存管理
29、理解引用計(jì)數(shù)
要點(diǎn):
?引用計(jì)數(shù)機(jī)制通過(guò)可以遞增遞減的計(jì)數(shù)器來(lái)管理內(nèi)存。對(duì)象創(chuàng)建好之后,其保留計(jì)數(shù)至少為1。若保留計(jì)數(shù)為正,則對(duì)象繼續(xù)存活。當(dāng)保留計(jì)數(shù)降為0時(shí),對(duì)象就被銷毀了。
?在對(duì)象生命期中,其余對(duì)象通過(guò)引用來(lái)保留或釋放此對(duì)象。保留與釋放操作分別會(huì)遞增及遞減保留計(jì)數(shù)
30、以ARC簡(jiǎn)化引用計(jì)數(shù)
要點(diǎn):
?有ARC后,程序員無(wú)須擔(dān)心內(nèi)存管理問(wèn)題,可省去類中的許多“樣板代碼”
?ARC管理對(duì)象生命期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作。在ARC環(huán)境下,變量的內(nèi)存管理語(yǔ)義可以通過(guò)修飾符指明,而原來(lái)則需要手工執(zhí)行“保留”及“釋放”操作
?由方法返回的對(duì)象,其內(nèi)存管理語(yǔ)義是通過(guò)方法名來(lái)體現(xiàn)。ARC將此確定為開(kāi)發(fā)者必須遵守的規(guī)則
?ARC只負(fù)責(zé)管理OC對(duì)象的內(nèi)存。尤其要注意:CoreFoundation對(duì)象不歸ARC管理,開(kāi)發(fā)者必須適時(shí)調(diào)用CFRetain/CFRelease。
31、在dealloc方法中只釋放引用并解除監(jiān)聽(tīng)
要點(diǎn):
?在dealloc方法里,只應(yīng)釋放指向其他對(duì)象的引用,并取消原來(lái)訂閱的“鍵值觀察”(KVO)或NSNotificationCenter等通知,不要做其他事情
?若對(duì)象持有文件描述符等系統(tǒng)資源,那么應(yīng)專門寫方法釋放此資源。這樣的類要和其使用者約定:用完資源后必須調(diào)用close方法
?執(zhí)行異步任務(wù)的方法不應(yīng)在dealloc里調(diào)用;只能在正常狀態(tài)下執(zhí)行的那些方法也不應(yīng)在dealloc里調(diào)用,因?yàn)榇藭r(shí)對(duì)象已處于正在回收的狀態(tài)了
dealloc方法:
?不要隨便調(diào)用其他方法,在這里無(wú)論調(diào)用什么方法都不太應(yīng)該,因?yàn)閷?duì)象此時(shí)“已近尾聲”(in a winding-down state)。若所調(diào)用的方法又要異步執(zhí)行任務(wù)或又要繼續(xù)調(diào)用他們自己的某些方法,等到那些任務(wù)執(zhí)行完畢時(shí)還行通知當(dāng)前對(duì)象,而系統(tǒng)已經(jīng)把當(dāng)前待回收的對(duì)象徹底摧毀了。這會(huì)導(dǎo)致很多問(wèn)題,經(jīng)常crash。
?dealloc方法所在的線程會(huì)執(zhí)行“最終的釋放操作”(final release),令對(duì)象的保留計(jì)數(shù)降為0,而某些方法必須在特定的線程里(如:主線程)調(diào)用才行。(若在dealloc里調(diào)用了哪些方法,則無(wú)法保證當(dāng)前這個(gè)線程就是那些方法所需的線程)
?不要調(diào)用屬性的存取方法,因?yàn)橛腥丝赡軙?huì)覆寫這些方法,并于其中做一些無(wú)法在回收階段安全執(zhí)行的操作。另外屬性可能正出去“鍵值觀測(cè)”(KVO)機(jī)制的監(jiān)控之下,該屬性的觀察者(observer)可能會(huì)在屬性值改變時(shí)“保留”或使用這個(gè)即將回收的對(duì)象
32、編寫“異常安全代碼”時(shí)留意內(nèi)存管理問(wèn)題
雖然OC只有在發(fā)生嚴(yán)重錯(cuò)誤導(dǎo)致程序無(wú)法繼續(xù)運(yùn)行時(shí),才應(yīng)跑出異常;但如果使用OC++編碼或使用了第三方庫(kù)拋出的異常不受控制時(shí),就需要捕獲及處理異常了。
有些系統(tǒng)庫(kù)也會(huì)拋出異常,如:使用KVO時(shí),若注銷一個(gè)尚未注冊(cè)的“觀察者”,則會(huì)拋出異常;
發(fā)生異常時(shí)應(yīng)如何管理內(nèi)存又是個(gè)值得研究的問(wèn)題。在try塊中,如果先保留了某個(gè)對(duì)象,然后在釋放它之前又拋出了異常,此時(shí)除非catch塊能釋放對(duì)象,否則就會(huì)導(dǎo)致內(nèi)存泄露。
ARC模式下,不會(huì)在finally塊里加代碼處理內(nèi)存泄露問(wèn)題,因?yàn)檎咝枰砑哟罅康臉影宕a,會(huì)嚴(yán)重影響運(yùn)行期的性能,即便在不拋異常時(shí)也如此。(而且添加的額外代碼還會(huì)明顯增加應(yīng)用程序的大小。這些副作用都不甚理想)雖說(shuō)默認(rèn)不會(huì)添加,但可以通過(guò)-fobjc-arc-exceptions這個(gè)編譯器標(biāo)志來(lái)開(kāi)啟此功能。并且處于OC++模式時(shí)編譯器會(huì)自動(dòng)把-fobjc-are-exceptions標(biāo)志打開(kāi)。
33、以弱引用避免保留還
要點(diǎn):
?將某些引用設(shè)置為weak,可避免出現(xiàn)“保留環(huán)”
?weak引用可自動(dòng)清空,也可以不自動(dòng)清空。自動(dòng)清空(automiling)是隨著ARC而引入的新特性,由運(yùn)行期系統(tǒng)來(lái)實(shí)現(xiàn)。在具備自動(dòng)清空功能的弱引用上,可隨意讀取其數(shù)據(jù),因?yàn)檫@種引用不會(huì)指向已經(jīng)回收過(guò)的對(duì)象
34、以“自動(dòng)釋放池塊”降低內(nèi)存峰值
要點(diǎn):
?自動(dòng)釋放池排布在棧中,對(duì)象收到·消息后,系統(tǒng)將其放入最頂端的池里
?合理運(yùn)用自動(dòng)釋放池,可降低應(yīng)用程序的內(nèi)存峰值
?@autoreleasepool這種新式寫法能創(chuàng)建出更為輕便的自動(dòng)釋放池
35、用“僵尸對(duì)象”調(diào)試內(nèi)存管理問(wèn)題
調(diào)式內(nèi)存管理問(wèn)題很令人頭疼。(因?yàn)橛行﹩?wèn)題不是必現(xiàn)的?。。。?/p>
大家都知道,向已回收的對(duì)象發(fā)送消息是不安全的。這么做有時(shí)可以,有時(shí)不行。具體可行,完全取決于對(duì)象所占內(nèi)存有沒(méi)有為其他內(nèi)容所覆寫。而這塊內(nèi)存有沒(méi)有移做他用,又無(wú)法確定。因此,應(yīng)用程序只是偶爾崩潰。在沒(méi)用崩潰的情況下,那塊內(nèi)存可能只復(fù)用了其中一部分,所以對(duì)象中的某些二進(jìn)制數(shù)據(jù)依然有效。
還有一種可能,就是那塊內(nèi)存恰好為另外一個(gè)有效且存活的對(duì)象所占據(jù)。這種情況下,運(yùn)行期系統(tǒng)會(huì)吧消息發(fā)到新對(duì)象那里,而此對(duì)象也許能應(yīng)答,也許不能。如果能,那么程序就不崩潰,可你會(huì)覺(jué)得奇怪:為什么收到消息的對(duì)象不是預(yù)想的那個(gè)呢?若新對(duì)象無(wú)法響應(yīng)選擇子,則程序依然會(huì)崩潰。
所幸Cocoa提供了“僵尸對(duì)象”(Zombie Object)模式:運(yùn)行期系統(tǒng)會(huì)把所有已經(jīng)回收的實(shí)例轉(zhuǎn)化成特殊的“僵尸對(duì)象”,而不會(huì)真正回收它們。這種對(duì)象所在的核心內(nèi)存無(wú)法重用,因此不可能遭到覆寫。僵尸對(duì)象收到消息后,會(huì)拋出異常,并給出描述。
開(kāi)啟方式:Scheme -> Run -> Diagnostics -> Enable Zombie Objects (勾選)
runtime發(fā)現(xiàn)如果開(kāi)啟該模式,則NSObject的dealloc方法會(huì)被“調(diào)配”(swizzle),從而執(zhí)行將對(duì)象的類改為指向_NSZombie_OriginalClass類。NSZombie類并未實(shí)現(xiàn)任何方法,沒(méi)有超類,跟NSObject一樣是個(gè)“根類”,該類只有一個(gè)實(shí)例變量isa,所有OC的根類都必須有此變量。由于這個(gè)輕量級(jí)的類沒(méi)有實(shí)現(xiàn)任何方法,所以發(fā)給它的消息需要寄過(guò)“完整的消息轉(zhuǎn)發(fā)機(jī)制”(full forwarding mechanism)。
要點(diǎn):
?系統(tǒng)在回收對(duì)象時(shí),可以不將其真的回收,而是將其轉(zhuǎn)化為僵尸對(duì)象。通過(guò)環(huán)境變量NSZombieEnabled可開(kāi)啟此功能
?系統(tǒng)會(huì)修改對(duì)象的isa指針,令其指向特說(shuō)的僵尸類,從而使該對(duì)象變?yōu)榻┦瑢?duì)象。僵尸類能夠響應(yīng)所有的選擇子,響應(yīng)方式為:打印一條包含消息內(nèi)容及其接受者的消息,然后終止應(yīng)用程序
36、不用使用retainCount
OC通過(guò)引用計(jì)數(shù)類管理內(nèi)存。每個(gè)人對(duì)象都有一個(gè)計(jì)數(shù)器,其值表明還有多少個(gè)其他對(duì)象想令此對(duì)象繼續(xù)存活。對(duì)象創(chuàng)建好之后,其保留計(jì)數(shù)大于0。保留與釋放操作分別會(huì)使計(jì)數(shù)遞增or遞減。當(dāng)技術(shù)變?yōu)?時(shí),對(duì)象就被系統(tǒng)回收了。
retainCount無(wú)用的原因:
?它所返回的保留計(jì)數(shù)只是某個(gè)給定時(shí)間點(diǎn)上的值。該方法并未考慮到系統(tǒng)會(huì)稍后把自動(dòng)釋放池清空,因而不會(huì)將后續(xù)的釋放操作從返回值里減去,因此此值未必能真實(shí)反映實(shí)際的保留計(jì)數(shù)。
?retainCount可能永遠(yuǎn)不返回0,因?yàn)橛袝r(shí)系統(tǒng)會(huì)優(yōu)化對(duì)象的釋放行為,在保留計(jì)數(shù)還是1的時(shí)候就把它回收了。只有在系統(tǒng)不打算這么優(yōu)化時(shí),計(jì)數(shù)值才會(huì)遞減至0。
如果發(fā)現(xiàn)某個(gè)對(duì)象的內(nèi)存泄露了,應(yīng)該檢查還有誰(shuí)仍然保留這個(gè)數(shù),并查明為何沒(méi)有釋放此對(duì)象。
要點(diǎn):
?對(duì)象的保留計(jì)數(shù)看似有用,實(shí)則不然,因?yàn)槿魏谓o定時(shí)間點(diǎn)上的“絕對(duì)保留計(jì)數(shù)”(absolute retain count)都無(wú)法反映對(duì)象聲明期的全貌
?引入ARC之后,retainCount方法就正式廢止了,在ARC下調(diào)用該方法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)
六、塊與大中樞派發(fā)
37、理解“塊”這一概念
塊其實(shí)就是個(gè)值,而且自有其相關(guān)類型。塊類型的語(yǔ)法與函數(shù)指針近似。
塊的強(qiáng)大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。有些變量若需在塊內(nèi)修改,需要加上__block修飾符。
如果塊所捕獲的變量是對(duì)象類型,那么就會(huì)自動(dòng)保留它。系統(tǒng)在釋放這個(gè)塊的時(shí)候,也會(huì)將其一并釋放。
descriptor:塊對(duì)象的總體大??;聲明了copy與dispose兩個(gè)輔助函數(shù)對(duì)于的函數(shù)指針(在拷貝or丟棄塊對(duì)象時(shí)執(zhí)行)。
塊還會(huì)把它所捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后邊,捕獲了多少個(gè)變量,就要占用多少的內(nèi)存空間。
塊定義時(shí)時(shí)存儲(chǔ)在棧重的。一旦復(fù)制到堆上,塊就成了帶引用計(jì)數(shù)的對(duì)象了。后續(xù)的復(fù)制操作都不會(huì)真的執(zhí)行復(fù)制,只是遞增快對(duì)象的引用計(jì)數(shù)。
要點(diǎn):
?塊是C、C++、Objective-C中的詞法閉包
?塊可接受參數(shù),也可返回值
?塊可以分配在棧or堆上,也可以時(shí)全局的。分配在棧上的塊可以拷貝到堆里,此時(shí)就跟標(biāo)磚的OC對(duì)象一樣,具備引用計(jì)數(shù)了
38、為常用的塊類型創(chuàng)建typedef
使用塊別名,當(dāng)需要修改時(shí)只需修改塊類型即可,無(wú)須修改所有使用到的地方。
要點(diǎn):
?以typedef重新定義塊類型,可令塊變量用起來(lái)更加簡(jiǎn)單
?定義新類型時(shí)應(yīng)遵從現(xiàn)有的命名習(xí)慣,勿使用其名稱與別的類型相沖突
?不妨為同一個(gè)塊簽名定義多個(gè)類型別名,如果要重構(gòu)的代碼使用了塊類型的某個(gè)別名,那么只需修改相應(yīng)的typedef中的塊簽名即可,無(wú)須改動(dòng)其他typedef
39、用handler塊降低代碼分散程度
筆者建議使用同一個(gè)塊來(lái)處理成功與失敗的情況:
?缺點(diǎn):全部邏輯寫在一起會(huì)比較長(zhǎng)和復(fù)雜
?優(yōu)點(diǎn):處理成功響應(yīng)的過(guò)程中可能會(huì)發(fā)現(xiàn)錯(cuò)誤(更靈活)
有時(shí)需要在相關(guān)時(shí)間點(diǎn)指向會(huì)掉操作,這種情況也可以使用handler塊。
NSNotificationCenter就提供了一個(gè)參數(shù),可以讓調(diào)用這指定塊在哪個(gè)隊(duì)列里執(zhí)行。默認(rèn)是跟通知同一個(gè)線程:
- (id)addObserverForName:(NSString *)name
object:(id)object
queue:(NSOperationQueue *)queue
usingBlock:(void(^)(NSNotification *)block
要點(diǎn):
?在創(chuàng)建對(duì)象時(shí),可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務(wù)邏輯一并聲明
?在有多個(gè)實(shí)例需要監(jiān)控時(shí),如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對(duì)象來(lái)切換,而該用handler塊來(lái)實(shí)現(xiàn),則可直接將塊與相關(guān)對(duì)象放在一起
?設(shè)計(jì)API時(shí)如果用到了handler塊,那么可以增加一個(gè)參數(shù),使調(diào)用這可以通過(guò)此參數(shù)來(lái)決定應(yīng)該把塊安排在哪個(gè)隊(duì)列上執(zhí)行
40、用塊應(yīng)用其所屬對(duì)象時(shí)不要出現(xiàn)保留環(huán)
要點(diǎn):
?如果塊所捕獲的對(duì)象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問(wèn)題
?一定要找個(gè)適當(dāng)?shù)臅r(shí)機(jī)接觸保留環(huán),而不能把責(zé)任推給API的調(diào)用者
41、多用派發(fā)隊(duì)列、少用同步鎖
1、同步塊:@synchronized(self)(多個(gè)屬性時(shí)不宜這么寫)
2、使用鎖:NSLockorNSRecursiveLock遞歸鎖(線程能夠多次持有該說(shuō)鎖,而不會(huì)出現(xiàn)死鎖deadlock現(xiàn)象)
3、GCD:串行同步隊(duì)列
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name {
__block NSString *localName;
dispatch_sync(_syncQueue, ^{
localName = _name;
});
return localName;
}
- (void)setName:(NSString *)name {
dispatch_barrier_sync(_syncQueue, ^{ // sync 比 async 效率高一些
_name = name;
});
}
[圖片上傳失敗...(image-b812a9-1694433945326)]
在隊(duì)列中柵欄塊必須單獨(dú)執(zhí)行,不能與其他塊并行。這只對(duì)并發(fā)隊(duì)列有意義,因?yàn)榇嘘?duì)列中的塊總是按順序逐個(gè)執(zhí)行。并發(fā)隊(duì)列如果發(fā)現(xiàn)接下來(lái)要處理的塊使柵欄塊(barrier block),那么就一直等當(dāng)前所有并發(fā)塊都執(zhí)行完畢后才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊。待柵欄塊執(zhí)行過(guò)后,再按正常方式繼續(xù)向下處理。
最好還是測(cè)一測(cè)每種做法的性能,然后從中選出最合適當(dāng)前場(chǎng)景的方案。
要點(diǎn):
?派發(fā)隊(duì)列可以用來(lái)表述同步語(yǔ)句,這種做法要比使用@synchronized塊或NSLock對(duì)象更簡(jiǎn)單
?將同步與異步派發(fā)結(jié)合起來(lái),可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為,而這么做卻不會(huì)阻塞執(zhí)行異步派發(fā)的線程
?使用同步隊(duì)列及柵欄快,可以令同步行為更加高效
42、多用GCD,少用performSelector系列方法
NSObject定義了幾個(gè)方法,可以推遲執(zhí)行方法調(diào)用,也可以指定運(yùn)行方法所用的線程。這些功能原來(lái)很有用,但是在出現(xiàn)了大中樞派發(fā)及塊這樣的新技術(shù)之后,就顯得不那么必要了。雖說(shuō)有些代碼還是會(huì)經(jīng)常用到它們,但筆者勸你還是避開(kāi)為妙。
如果選擇子是在運(yùn)行時(shí)決定的,那么就能體現(xiàn)出此方法的強(qiáng)大之處了:
SEL selector;
if ( /* some codition */ ) {
selector = @selector(newObject); // 需要釋放
} else if ( /* some other codition */ ) {
selector = @selector(copy); // 需要釋放
} else {
selector = @selector(someProrperty); // 不需要釋放
}
id ret = [object performSelector:selector];
這種編程方式看起來(lái)比較靈活,但是在ARC模式下會(huì)發(fā)出警告:
warning: performSelector may case a leak because its selector
is unknow [-Warc-performSelector-leaks]
因?yàn)榫幾g器并不知道將要調(diào)用的選擇子是什么,不了解其方法簽名及返回值,甚至連私發(fā)歐有返回值都不清楚。所以沒(méi)辦法用ARC的內(nèi)存管理規(guī)則來(lái)判定返回值是不是應(yīng)該釋放。鑒于此,ARC采用了比較謹(jǐn)慎的做法,就是不添加釋放操作。因而可能導(dǎo)致內(nèi)存泄露。
這個(gè)問(wèn)題很容易忽視,而且就算用靜態(tài)分析器,也很難偵測(cè)到隨后的內(nèi)存泄露。(所以需謹(jǐn)慎使用performSelector方法)
大中樞GCD出現(xiàn)之后,performSelector系列方法所提供的功能,都可以用GCD實(shí)現(xiàn):
// 延遲執(zhí)行
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];
});
// 把任務(wù)放到主線程執(zhí)行
dispatch_async(dipatch_get_main_queue(), ^{
[self doSomething];
});
要點(diǎn):
?preformSelector系列方法在內(nèi)存管理方面容易有疏失。它無(wú)法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無(wú)法插入適當(dāng)?shù)膬?nèi)存管理方法
?performSelector系列方法所能處理的選擇子太過(guò)局限了,選擇子的返回值類型及發(fā)送給方法的參數(shù)個(gè)數(shù)都受到限制
?如果想把任務(wù)放在另一個(gè)線程上執(zhí)行,那么最好不要用preformSelector系列方法,而是應(yīng)該把任務(wù)封裝到塊里,然后調(diào)用大中樞派發(fā)機(jī)制的相關(guān)方法來(lái)實(shí)現(xiàn)
43、掌握GCD及操作隊(duì)列的使用時(shí)機(jī)
開(kāi)發(fā)者可以把操作以NSOperation子類的形式放在隊(duì)列中,而這些操作也能后并發(fā)執(zhí)行。雖說(shuō)“操作隊(duì)列”(operation queue)在GCD之前就有了,其中某些設(shè)計(jì)原理因操作隊(duì)列而流行,GCD就是基于這些原理構(gòu)建的。實(shí)際上,從iOS4與Mac OS10.6開(kāi)始,操作隊(duì)列在底層是用GCD來(lái)實(shí)現(xiàn)的。
GCD是純C的API,而操作隊(duì)列這是OC的對(duì)象;在GCD中,任務(wù)用塊來(lái)表示,而塊時(shí)一個(gè)輕量級(jí)數(shù)據(jù)結(jié)構(gòu),而“操作”(operation)則是個(gè)更為重量級(jí)的OC對(duì)象。
使用NSOperation及NSOperationQueue的好處:
?取消某個(gè)操作。已經(jīng)啟動(dòng)的任務(wù)無(wú)法取消(GCD隊(duì)列的任務(wù)就無(wú)法取消)
?指定操作間的依賴關(guān)系。
?通過(guò)鍵值觀察機(jī)制監(jiān)控NSOperation對(duì)象的屬性。isCancelled、isFinished…
?指定操作的優(yōu)先級(jí)。此操作與隊(duì)列中其他操作之間的優(yōu)先關(guān)系(GCD隊(duì)列確實(shí)有優(yōu)先級(jí),不過(guò)那是針對(duì)整個(gè)隊(duì)列的不是針對(duì)每個(gè)塊的)
?重用NSOperation對(duì)象。可以繼承該類,存放任何信息。對(duì)象在執(zhí)行時(shí)可以充分利用繼承而來(lái)的各種信息,還可以隨意調(diào)用其方法。
? 操作隊(duì)列有很多地方勝過(guò)派發(fā)隊(duì)列:操作隊(duì)列提供了多種執(zhí)行任務(wù)的方式,而且都是寫好的,直接就能使用。開(kāi)發(fā)者不用再編寫復(fù)雜的調(diào)度器,也不用自己來(lái)實(shí)現(xiàn)取消操作或指定操作優(yōu)先級(jí)的功能,這些事情操作隊(duì)列都已經(jīng)實(shí)現(xiàn)好了。
要點(diǎn):
?在解決多線程與任務(wù)管理問(wèn)題時(shí),派發(fā)隊(duì)列并非唯一方案
?操作隊(duì)列提供了一套高層的OC API,能實(shí)現(xiàn)純GCD所具備的絕大部分功能,而且還能完成一些更為復(fù)雜的操作,那些操作若改用GCD來(lái)實(shí)現(xiàn),則需另外編寫代碼
44、通過(guò)Dispatch Group機(jī)制,根據(jù)系統(tǒng)資源狀況來(lái)執(zhí)行任務(wù)
dispatch group 是GCD的一項(xiàng)特性,能夠把任務(wù)分組。調(diào)用者可以等待這組任務(wù)執(zhí)行完畢,也可以在提供會(huì)掉函數(shù)之后繼續(xù)往下執(zhí)行,這組任務(wù)完成時(shí),調(diào)用者會(huì)得到通知。
// 把任務(wù)編組
// 方法1:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
// 方法2:
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
// 等待group執(zhí)行完畢:
// 方法1:
long dispatch_group_wait(dispatch_geoup_t group,
dispatch_time_t timeout); // 會(huì)阻塞
// 方法2:
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
要點(diǎn):
?一系列任務(wù)可歸入一個(gè)dispatch group中。開(kāi)發(fā)者可以在這組任務(wù)執(zhí)行完畢時(shí)獲得通知
?通過(guò)dispatch group,可以在并發(fā)式派發(fā)隊(duì)列里同事執(zhí)行多項(xiàng)任務(wù),此時(shí)GCD會(huì)根據(jù)系統(tǒng)資源狀況來(lái)調(diào)度這些并發(fā)執(zhí)行的任務(wù)。開(kāi)發(fā)者若自己來(lái)實(shí)現(xiàn)此功能,則需編寫大量代碼
45、使用dispatch_once來(lái)執(zhí)行只需要運(yùn)行一次的線程安全代碼
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken; // 只初始化一次
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
dispatch_once更高效,沒(méi)有使用重量級(jí)的同步機(jī)制。
要點(diǎn):
?經(jīng)常需要編寫“只需執(zhí)行一次的線程安全代碼”(thread-safe single-code execution)。通過(guò)GCD所提供的dispatch_once函數(shù),很容易就能實(shí)現(xiàn)此功能
?標(biāo)記應(yīng)該聲明在static或global作用域中,這樣的話,在把只需執(zhí)行一次的塊傳給dispatch_once函數(shù)時(shí),傳進(jìn)去的標(biāo)記也是相同的
46、不要使用dispatch_get_current_queue
- (NSString *)name {
__block NSString *localName;
// 如果調(diào)用方法的隊(duì)列恰好使_syncQueue,則會(huì)死鎖 (同步線程里加同步事件)
dispatch_sync(_syncQueue, ^{
localName = _name;
});
return localName;
}
dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block ^{ /* ... */ };
if (dispatch_get_current_queue() == queueA) { // dispatch_get_current_queue 返回的是queueB
block();
} else {
dispatch_sync(queueA, block); // 所以還是會(huì)進(jìn)入死鎖
}
});
});
由于隊(duì)列間有層級(jí)關(guān)系,所以“檢查當(dāng)前隊(duì)列是否為執(zhí)行同步派發(fā)所用的隊(duì)列”這種辦法,并不總是湊效。
在這種情況下,正確的做法是:不要把存取方法做成可重入的,而是應(yīng)該確保同步操作所有的隊(duì)列,絕不會(huì)訪問(wèn)屬性,也就是絕對(duì)不會(huì)調(diào)用name方法。這種隊(duì)列只應(yīng)該用來(lái)同步屬性。由于派發(fā)隊(duì)列是一種極為輕量的機(jī)制,所以不妨為每個(gè)每項(xiàng)屬性創(chuàng)建專用的同步隊(duì)列。
GCD提供了一個(gè)功能,設(shè)定“隊(duì)列特有數(shù)據(jù)”,可以把任意數(shù)據(jù)以鍵值對(duì)的形式關(guān)聯(lián)到隊(duì)列里。最重要的是,若在當(dāng)前層級(jí)獲取不到關(guān)聯(lián)數(shù)據(jù)時(shí),系統(tǒng)會(huì)沿著層級(jí)體系向上查找,直到 找到數(shù)據(jù) / 到根隊(duì)列 為止。
dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
dispatch_set_target_queue(queueB, queueA); // B嵌套在A里
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
// 為queueA設(shè)置“隊(duì)列特定值”
dispatch_queue_set_specific(queueA, // 待設(shè)置數(shù)據(jù)的隊(duì)列
&kQueueSpecific, // key
(void *)queueSpecificValue, // value
(dispatch_funcion_t)CFRelease); // 析構(gòu)方法
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
NSLog(@"No deadlock!");
};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); // 獲取隊(duì)列特定值
if (retrievedValue) { // 在A隊(duì)列里
block();
} else {
dispatch_sync(queueA, block);
}
});
要點(diǎn):
?dispatch_get_current_queue函數(shù)的行為常常與開(kāi)發(fā)者所預(yù)期的不同。此函數(shù)已經(jīng)廢棄,只應(yīng)做調(diào)試之用
?由于派發(fā)隊(duì)列是按層級(jí)來(lái)組織的,所以無(wú)法單用某個(gè)隊(duì)列對(duì)象來(lái)描述“當(dāng)前隊(duì)列”這一概念
?Dispatch_get_current_queue函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖,然而能用此函數(shù)解決的問(wèn)題,通常該用“隊(duì)列特定數(shù)據(jù)”來(lái)解決
七、系統(tǒng)框架
47、熟悉系統(tǒng)框架
框架:將一些列代碼封裝為動(dòng)態(tài)庫(kù),并在其中放入描述其接口的頭文件。
主要框架:
?Foundation:NSObject、NSArray、NSDictionary。。。(用NS前綴),還提供了collection等基礎(chǔ)核心功能,還提供了字符串處理這樣的復(fù)雜功能。如:NSLinguisticTagger可以解析字符串并找到其中的全名名詞、動(dòng)詞、代詞等。
?CoreFoundation:Foundation框架中許多功能,都可以在此框架中找到對(duì)應(yīng)的C語(yǔ)言API?!盁o(wú)縫橋接”(toll-free bridging)功能可以把此框架中的C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)平滑轉(zhuǎn)換為Foundation中的OC對(duì)象,也可以方向轉(zhuǎn)換。
其他框架:
?CFNetwork:提供了C語(yǔ)言級(jí)別的網(wǎng)絡(luò)通信能力,將“BSD套接字”(BSD socket)抽象成易于使用的網(wǎng)絡(luò)接口。而Foundation則將該框架里的部分內(nèi)容封裝為OC語(yǔ)言接口,以便進(jìn)行網(wǎng)絡(luò)通信,如:NSURLConnection從URL中下載數(shù)據(jù)
?CoreAudio:提供C語(yǔ)言API可用來(lái)操作設(shè)備上的音頻硬件。此框架屬于比較難用的那種,因?yàn)橐纛l處理本身就很復(fù)雜。所幸由這套API中可以抽象除另外一套OC式API,用后者來(lái)處理音頻問(wèn)題會(huì)簡(jiǎn)單些
?AVFouncation:提供OC對(duì)象可用來(lái)回放并錄制音頻及視頻,如:在UI視圖類里播放視頻
?CoreData:提供OC接口可將對(duì)象放入數(shù)據(jù)庫(kù),便于持久保存。處理數(shù)據(jù)的獲取及存儲(chǔ)事宜,且可跨越Mac OS X及iOS平臺(tái)
?CoreText:提供C語(yǔ)言接口可以高效執(zhí)行文字排版及渲染操作
可以看出OC編程一項(xiàng)重要特點(diǎn):經(jīng)常要使用底層C語(yǔ)言API,好處是可以繞過(guò)OC的運(yùn)行期系統(tǒng),從而提升指向速度。當(dāng)然ARC只負(fù)責(zé)OC對(duì)象,所以使用這些API時(shí)尤其需要注意內(nèi)存管理問(wèn)題。
核心UI框架:Mac OS X的是AppKit、iOS的是UIKIt,都提供了構(gòu)建在Foundaton與CoreFoundation之上的OC類。
?CoreAnimation:OC寫成,提供了一些工具,UI框架用這些工具來(lái)渲染圖形并播放動(dòng)畫(huà)。CoreAnimation本身不是框架,是QuartzCore框架的一部分。
?CoreGaphics:C語(yǔ)言寫成的框架,提供了2D渲染所必備的數(shù)據(jù)結(jié)構(gòu)與函數(shù)。其中定義了:CGPoint、CGSize、CGRect等數(shù)據(jù)結(jié)構(gòu)。
要點(diǎn):
?許多系統(tǒng)框架都可以直接使用。其中最重要的是Foudation與CoreFoundation,這兩個(gè)框架提供了構(gòu)建應(yīng)用程序所需的許多核心功能
?很多常見(jiàn)任務(wù)都能用框架來(lái)做,例如音頻、視頻處理、網(wǎng)絡(luò)通信、數(shù)據(jù)管理等
?請(qǐng)記?。河眉僀寫成的框架于用OC寫成的一樣重要,若想成為優(yōu)秀的OC開(kāi)發(fā)者,應(yīng)該掌握C語(yǔ)言的核心概念
48、多用塊枚舉,少用for循環(huán)
1、for循環(huán)
// 遍歷NSArray
for (int i = 0; i < anArray.count; i++) {
id object = anArray[i];
}
// 遍歷NSDictionary
NSArray *keys = [aDic allKeys];
for (int i = 0; i < keys.count; i++) {
id key = keys[i];
id value = aDic[key];
}
// 遍歷NSSet
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
id object = objects[i];
}
創(chuàng)建附加數(shù)組會(huì)有額外開(kāi)銷,而且還會(huì)多創(chuàng)建一組對(duì)象,它會(huì)保留collection中的所有元素對(duì)象。釋放時(shí)這些附加對(duì)象也要釋放,這樣就調(diào)用了一些本來(lái)不需要執(zhí)行的方法。其他各種遍歷方式都無(wú)須創(chuàng)建這種中介數(shù)組。
2、使用OC1.0的NSEnumerator來(lái)遍歷
NSEnumerator是一個(gè)抽象基類,其中自定義了兩個(gè)方法,供子類實(shí)現(xiàn):
- (NSArray *)allObjects;
- (id)nextObjects; // 返回枚舉里的下一個(gè)對(duì)象
Foundation框架中內(nèi)建的collection類都實(shí)現(xiàn)了這種遍歷方式:`
// 遍歷NSArray
NSEunmerator *enumerator = [anArray objectEnumerator]; // reverseObjectEnumerator 方向遍歷
id object;
while ((object == [enumerator nextObject]) != nil) {/*...*/}
// 遍歷NSDictionary
NSEnumerator *enumerator = [aDic keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
id value = aDic[key];
}
// 遍歷NSSet
NSEunmerator *enumerator = [aSet objectEnumerator];
id object;
while ((object == [enumerator nextObject]) != nil) {/*...*/}
3、快速遍歷
OC2.0引入了快速遍歷,為for循環(huán)開(kāi)設(shè)了in關(guān)鍵字。從而大幅簡(jiǎn)化了遍歷collectin所需的語(yǔ)法:
// 遍歷NSArray
for (id object in anArray) {/*...*/}
// 遍歷NSDictionary
for (id key in anDic) {
id value = aDic[key];
}
// 遍歷NSSet
for (id object in aSet) {/*...*/}
支持快速遍歷,遵從NSFastEnumeration協(xié)議就好,只有一個(gè)協(xié)議方法:
- (NSinterger)countBeyEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id *)stackbuffer
count:(NSUInteger)length;
NSEnumerator對(duì)象也實(shí)現(xiàn)了NSFastEnumeration協(xié)議,所以能用來(lái)執(zhí)行方向遍歷:
for (id object in [anArray reverseObjectEnumerator]) {/*...*/}
4、基于塊的遍歷方式(遍歷時(shí)可以獲取更堵信息)
NSArray *anArray = [NSArray array];
[anArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES; // 優(yōu)雅的終止遍歷(其他的遍歷可以用break終止)
}
}];
NSDictionary *aDic = [NSDictionary dictionary];
[aDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES;
}
}];
NSSet *aSet = [NSSet set];
// 可以修改方法簽名,以免進(jìn)行類型轉(zhuǎn)換操作
[aSet enumerateObjectsUsingBlock:^(NSString *obj, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES;
}
}];
可以利用另一個(gè)版本指向方向遍歷、并發(fā)遍歷:
// NSEnumerationConcurrent 并發(fā)的方式遍歷
// NSEnumerationReverse 方向遍歷
[anArray enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
要點(diǎn):
?遍歷collection有四種方式。最基本的辦法是for循環(huán),其次是NSEnumerator遍歷法及快速遍歷法。最新、最先進(jìn)的方式則是“塊枚舉法”
?“塊枚舉法”本身就能通過(guò)GCD來(lái)并發(fā)執(zhí)行遍歷操作,無(wú)須另行編寫代碼。而采用其他遍歷方式則無(wú)法輕易實(shí)現(xiàn)這一點(diǎn)
?若提前知道代碼里的collection含有何種對(duì)象,則應(yīng)該修改塊簽名,指出對(duì)象的具體類型
49、對(duì)自定義其內(nèi)存管理語(yǔ)義的collection使用無(wú)縫橋接
NSArray *anNSArray = @[@1, @2, @3];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
CFArrayGetCount(aCFArray); // array.count
// __bridge: ARC仍擁有該OC對(duì)象的所有權(quán)
// __bridge_retained: ARC交出該OC對(duì)象的所有權(quán),之后需要 CFRealease(aCFArray) 來(lái)釋放
NSArray *anNSArray = (__bridge_transfer)aCFArray; // ARC獲得所有權(quán)
無(wú)縫橋接必要性:因?yàn)镕oundation框架中OC類所具備的某些功能,是CoreFoundation框架中的C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)所不具備的,反之亦然。
要點(diǎn):
?通過(guò)無(wú)縫橋接技術(shù),可以在Foundation框架中昂的OC對(duì)象與CoreFoundaton框架中的C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)之間來(lái)回轉(zhuǎn)換
?在CoreFoundation層面創(chuàng)建collection時(shí),可以指定許多回調(diào)函數(shù),這些函數(shù)表示此collection應(yīng)如何處理其元素。然后可運(yùn)用無(wú)縫橋接技術(shù),將其轉(zhuǎn)換成具備特殊內(nèi)存管理語(yǔ)義的OC collection
50、構(gòu)建緩存時(shí)選用NSCache而非NSDictionary
NSCache:當(dāng)系統(tǒng)資源將耗盡時(shí),自動(dòng)刪減緩存(若用普通字典,需要自己寫掛鉤,在“低內(nèi)存”警告時(shí)通知?jiǎng)h減緩存)。 還會(huì)先行刪減“最久未使用的”對(duì)象,不會(huì)“拷貝”鍵,而是會(huì)“保留”它(當(dāng)鍵不支持拷貝時(shí)很合適)。是線程安全的。可以設(shè)置緩存 對(duì)象總數(shù) 和 “總開(kāi)銷”。
NSCache搭配NSPureableData使用:
typedef void(^EOCNetworkFetcherCompletionHander)(NSData *data);
NSCache *_cache;
- (instancetype)init {
self = [super init];
if (self) {
_cache = [NSCache new];
_cache.countLimit = 100; // 100 URLs
_cache.totalCostLimit = 5 * 1024 * 1024; // 5MB
}
return self;
}
- (void)downloadDataForUrl:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) { // Cache hit
// cachedData.isContentDiscarded // 相關(guān)內(nèi)存是否已釋放
[cachedData beginContentAccess]; // 告訴它不應(yīng)丟棄自己所占用的內(nèi)存
[self useData:cachedData];
[cachedData endContentAccess]; // 告訴它在必要時(shí)可以丟棄自己所占用的內(nèi)存
} else { // Cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost:purgeableData.length];
[self useData:purgeableData];
[purgeableData endContentAccess];
}];
}
}
要點(diǎn):
?實(shí)現(xiàn)緩存時(shí)應(yīng)選用NSCache而非NSDictionary對(duì)象。因?yàn)镹SCache可以提供優(yōu)雅的自動(dòng)刪減功能,而且是“線程安全的”,此外,它與字典不同,并不會(huì)拷貝鍵值
?可以給NSCache對(duì)象設(shè)置上限,用以限制緩存中的對(duì)象總個(gè)數(shù),但是絕不要把這些設(shè)置當(dāng)成可靠的“硬限制”,他們僅對(duì)NSCache起指導(dǎo)作用
?將NSPurgeableData與NSCache搭配使用,可實(shí)現(xiàn)自動(dòng)清楚數(shù)據(jù)的功能。及當(dāng)NSPurgeableData對(duì)象所占用內(nèi)存被系統(tǒng)丟棄時(shí),該對(duì)象自身也會(huì)從緩存中移除
?如果緩存使用得當(dāng),那么應(yīng)用程序的響應(yīng)速度就能提高。只有那種“重新計(jì)算起來(lái)很費(fèi)事的”數(shù)據(jù),才值得放入緩存,如:需要從網(wǎng)絡(luò)獲取或從磁盤讀取的數(shù)據(jù)
51、精簡(jiǎn)load與initialize的實(shí)現(xiàn)代碼
load:當(dāng)包含類或分類的程序庫(kù)載入系統(tǒng)時(shí),就會(huì)執(zhí)行此方法。iOS指應(yīng)用程序啟動(dòng)時(shí)。load方法中使用其他類時(shí)不安全的(如:其他類又用到了當(dāng)前類,則無(wú)法正確加載了)。整個(gè)應(yīng)用程序在執(zhí)行l(wèi)oad方法時(shí)都會(huì)阻塞(不要在里面等待鎖/加鎖)。總之,能不做的事情就別做。
initialize:在程序首次用該類之前調(diào)用,且只調(diào)用一次。它時(shí)運(yùn)行期系統(tǒng)來(lái)調(diào)用的,絕不應(yīng)該通過(guò)代碼直接調(diào)用。運(yùn)行期系統(tǒng)在執(zhí)行該方法時(shí)處于正常狀態(tài),可以安全使用并調(diào)用任意類中的任意方法。運(yùn)行期系統(tǒng)會(huì)確保其在“線程安全的環(huán)境”中執(zhí)行。遵循繼承規(guī)則。
只應(yīng)用來(lái)設(shè)置內(nèi)部數(shù)據(jù),不應(yīng)調(diào)用其他方法,即便時(shí)本類自己的方法,也最好別調(diào)用。
initialize需要保持精簡(jiǎn)的原因:
?對(duì)某個(gè)類而言,任何線程都可能初次使用到它,若碰巧時(shí)UI線程,那么初始化期間會(huì)一直阻塞,導(dǎo)致應(yīng)用無(wú)法響應(yīng)。
?開(kāi)發(fā)者無(wú)法控制類的初始化時(shí)機(jī)。不能令代碼依賴特定的時(shí)間點(diǎn),否則會(huì)很危險(xiǎn)
?若某個(gè)了實(shí)現(xiàn)很復(fù)雜,那么其中可能直接或間接用到其他類。若那些類尚未初始化,則系統(tǒng)會(huì)迫使其初始化。其他類的初始化又可能依賴本類的某些數(shù)據(jù)。代碼就無(wú)法正常運(yùn)行了。
要點(diǎn):
?在加載階段,如果類實(shí)現(xiàn)了load方法,那么系統(tǒng)就會(huì)調(diào)用它。分類也可以定義此方法,類的load方法要比分類中的先調(diào)用。與其他方法不同,load方法不參與覆寫機(jī)制。
?首次使用某個(gè)類之前,系統(tǒng)會(huì)向其發(fā)送initialize消息。由于此方法遵從普通的覆寫規(guī)則,所以通常應(yīng)該在里面判斷當(dāng)前要初始化的是哪個(gè)類
?load與initialize方法都應(yīng)該實(shí)現(xiàn)的精簡(jiǎn)一些,這有助于保持應(yīng)用程序的響應(yīng)能力,也能減少引入“依賴環(huán)”的幾率
?無(wú)法在編譯器設(shè)定的全局常亮,可以放在initialize方法里初始化
52、別忘了NSTimer會(huì)保留其目標(biāo)對(duì)象
要點(diǎn):
?NSTimer對(duì)象會(huì)保留其目標(biāo),直到計(jì)時(shí)器本身失效為止,調(diào)用invalidate方法可令計(jì)時(shí)器失效,另外,一次性的計(jì)時(shí)器在觸發(fā)完任務(wù)之后也會(huì)失效
?反復(fù)執(zhí)行任務(wù)的計(jì)時(shí)器,很容易引入保留環(huán),如果這種計(jì)時(shí)器的目標(biāo)又保留了計(jì)時(shí)器本身,那肯定會(huì)導(dǎo)致保留環(huán)。這種關(guān)系,可能直接發(fā)送,也可能通過(guò)對(duì)象圖里的其他對(duì)象間接發(fā)生
?可以擴(kuò)充NSTimer的功能,用“塊”來(lái)打破保留環(huán)。不過(guò)除非NSTimer將來(lái)在公共接口里提供此功能,否則需創(chuàng)建分類,將相關(guān)實(shí)現(xiàn)代碼加入其中