国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Effective Objective-C 學(xué)習(xí)(三)

這篇具有很好參考價(jià)值的文章主要介紹了Effective Objective-C 學(xué)習(xí)(三)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

理解引用計(jì)數(shù)

Objective-C 使用引用計(jì)數(shù)來管理內(nèi)存:每個(gè)對(duì)象都有個(gè)可以遞增或遞減的計(jì)數(shù)器。如果想使某個(gè)對(duì)象繼續(xù)存活,那就遞增其引用計(jì)數(shù):用完了之后,就遞減其計(jì)數(shù)。計(jì)數(shù)變?yōu)?0時(shí),就可以把它銷毀。
在ARC中,所有與引用計(jì)數(shù)有關(guān)的方法都無法編譯(由于 ARC 會(huì)在編譯時(shí)自動(dòng)插入內(nèi)存管理代碼,因此在編譯時(shí),所有與引用計(jì)數(shù)相關(guān)的方法都會(huì)被 ARC 替換為適當(dāng)?shù)拇a)。

引用計(jì)數(shù)的工作原理

在引用計(jì)數(shù)架構(gòu)下,對(duì)象有個(gè)計(jì)數(shù)器,用以表示當(dāng)前有多少個(gè)事物想令此對(duì)象繼續(xù)存活下去。這在 Objective-C 中叫做 “保留計(jì)數(shù)”也可以叫 “引用計(jì)數(shù)”。NSObject 協(xié)議聲明了下面三個(gè)方法用于操作計(jì)數(shù)器,以遞增或遞減其值:

  • Retain 遞增保留計(jì)數(shù)。
  • release 遞減保留計(jì)數(shù)。
  • autorelease 待稍后清理 “自動(dòng)釋放池”時(shí),再遞減保留計(jì)數(shù)。
    查看保留計(jì)數(shù)的方法叫做 retainCount,不推薦使用。

對(duì)象創(chuàng)建出來時(shí),其保留計(jì)數(shù)至少為 1。若想令其繼續(xù)存活,則調(diào)用 retain 方法。要是某部分代碼不再使用此對(duì)象,不想令其繼續(xù)存活,那就調(diào)用 release 或 autorelease 方法。最終當(dāng)保留計(jì)數(shù)歸零時(shí),對(duì)象就回收了,也就是說系統(tǒng)會(huì)將其占用的內(nèi)存標(biāo)記為 “可重用”。此時(shí),所有指向該對(duì)象的引用也都變得無效。
應(yīng)用程序在其生命周期中會(huì)創(chuàng)建很多對(duì)象,這些對(duì)象都相互聯(lián)系著。例如,表示個(gè)人信息的對(duì)象會(huì)引用另一個(gè)表示人名的字符串對(duì)象,還可能會(huì)引用其他個(gè)人信息對(duì)象,比如在存放朋友的 set 中就是如此。這些相互關(guān)聯(lián)的對(duì)象就構(gòu)成了一張 “對(duì)象圖”。對(duì)象如果持有指向其他對(duì)象的強(qiáng)引用,那么前者就 “擁有” 后者。對(duì)象想令其所引用的那些對(duì)象繼續(xù)存活,就可將其 “保留”。等用完了之后,再釋放。
按 “引用樹” 回溯,那么最終會(huì)發(fā)現(xiàn)一個(gè) “根對(duì)象”。在 iOS 應(yīng)用程序中,它是 UIApplication 對(duì)象。是應(yīng)用程序啟動(dòng)時(shí)創(chuàng)建的單例。
如下面這段代碼:

NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];

//不能假設(shè)number對(duì)象一定存活
NSLog(@"number = %@", number);//***
//因?yàn)閷?duì)象所占的內(nèi)存在 “解除分配”之后,只是放回 “可用內(nèi)存池”。如果執(zhí)行 NSLog 時(shí)尚未覆寫對(duì)象內(nèi)存,那么該對(duì)象仍然有效,這時(shí)程序不會(huì)崩潰。
//因過早釋放對(duì)象而導(dǎo)致的 bug 很難調(diào)試。

[array release];

在上面這段代碼中:創(chuàng)建了一個(gè)可變數(shù)組 array,然后創(chuàng)建了一個(gè) NSNumber 對(duì)象 number,并將其添加到數(shù)組中,數(shù)組也會(huì)在 number 上調(diào)用retain 方法,以期繼續(xù)保留此對(duì)象,這時(shí)number的引用計(jì)數(shù)至少為2。接著,我們?cè)囍ㄟ^ release 方法釋放了 number 對(duì)象,因?yàn)閿?shù)組對(duì)象還在引用著number對(duì)象,因此它仍然存活,但是不應(yīng)該假設(shè)它一定存活。最后,通過調(diào)用 release 方法釋放了數(shù)組 array,確保了內(nèi)存的正確管理。上面這段代碼在ARC中是無法編譯的,因?yàn)檎{(diào)用了release方法。
調(diào)用者通過 alloc 方法表達(dá)了想令該對(duì)象繼續(xù)存活下去的意愿,不過并不是說對(duì)象此時(shí)的保留計(jì)數(shù)必定是1。在 alloc 或 “initWithInt:” 方法的實(shí)現(xiàn)代碼中,也許還有其他對(duì)象也保留了此對(duì)象(如初始化過程中的委托調(diào)用、通知其他對(duì)等),所以,其保留計(jì)數(shù)可能會(huì)大于 1。

為避免在不經(jīng)意間使用了無效對(duì)象,一般調(diào)用完 release 之后都會(huì)清空指針。這就能保證不會(huì)出現(xiàn)可能指向無效對(duì)象的指針,這種指針通常稱為 “懸掛指針”。
可以這樣編寫代碼來防止此情況發(fā)生:


NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;

屬性存取方法中的內(nèi)存管理

剛才那個(gè)例子中的數(shù)組通過在其元素上調(diào)用 retain 方法來保留那些對(duì)象。不光是數(shù)組,其他對(duì)象也可以保留別的對(duì)象,這一般通過訪問 “屬性”來實(shí)現(xiàn),而訪問屬性時(shí),會(huì)用到相關(guān)實(shí)例變量的獲取方法及設(shè)置方法。若屬性為 “strong 關(guān)系”,則設(shè)置的屬性值會(huì)保留。假設(shè)有個(gè)名叫 foo 的屬性由名為 _foo 的實(shí)例變量所實(shí)現(xiàn),那么,該屬性的設(shè)置方法會(huì)是這樣:

- (void)setFoo:(id)foo {
  [foo retain];
  [_foo release];
  _foo = foo;
 }

當(dāng)設(shè)置屬性 foo 的新值時(shí),屬性的設(shè)置方法會(huì)依次執(zhí)行以下操作:

  1. 保留新值:使用 retain 方法保留新值,確保其在設(shè)置方法之外仍然可用。
  2. 釋放舊值:使用 release 方法釋放舊值,以減少其保留計(jì)數(shù),并在不再需要時(shí)釋放內(nèi)存。
  3. 更新實(shí)例變量:將實(shí)例變量 _foo 的引用指向新值。
    上面這些操作的順序很重要。如果在保留新值之前就釋放了舊值,并且舊值和新值指向同一個(gè)對(duì)象,那么在釋放舊值時(shí)可能會(huì)導(dǎo)致對(duì)象被系統(tǒng)回收,而在后續(xù)保留新值時(shí),該對(duì)象已經(jīng)不存在了。這就會(huì)導(dǎo)致 _foo 成為一個(gè)懸掛指針,即指向已經(jīng)釋放的內(nèi)存空間,這樣的指針是無效的,并且可能導(dǎo)致應(yīng)用程序崩潰或出現(xiàn)其他問題。

自動(dòng)釋放池

自動(dòng)釋放池(autorelease)是 Objective-C 內(nèi)存管理的重要特性之一。
簡(jiǎn)單來說它允許開發(fā)者推遲對(duì)象的釋放時(shí)間,通常在下一個(gè)事件循環(huán)中才執(zhí)行釋放操作。
比如說有這個(gè)代碼:

- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return str;
}

在上面這段代碼中,str對(duì)象在stringValue方法的作用域中被alloc出來,因此它的引用計(jì)數(shù)為1,但是因?yàn)樗淖饔糜蚴莝tringValue方法,因此我們期望的是它在該方法結(jié)束前引用計(jì)數(shù)應(yīng)為0,但是因?yàn)槿鄙倭酸尫挪僮?,因此該str對(duì)象的引用計(jì)數(shù)為1比期望值多1。
但是我們又不能直接在stringValue方法中將str對(duì)象釋放,否則還沒等方法返回,系統(tǒng)就把該對(duì)象回收了。這里應(yīng)該用 autorelease,它會(huì)在稍后釋放對(duì)象,從而給調(diào)用者留下了足夠長(zhǎng)的時(shí)間,使其可以在需要時(shí)先保留返回值。換句話說,此方法可以保證對(duì)象在跨越 “方法調(diào)用邊界”(method callboundary)后一定存活。

  • 因此我們需要改寫 stringValue 方法,使用 autorelease 來釋放對(duì)象:
- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return [str autorelease];
}

改寫后使用了 autorelease 方法將str對(duì)象放入自動(dòng)釋放池中,以延遲其釋放時(shí)間。這樣,在方法返回時(shí),對(duì)象的引用計(jì)數(shù)會(huì)保持預(yù)期的值,而不會(huì)多出 1。
可以像下面這樣使用 stringValue 方法返回的字符串對(duì)象:

NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);

在第一段代碼中,NSString *str = [self stringValue]; 返回的字符串對(duì)象 str 是一個(gè)被放入自動(dòng)釋放池的對(duì)象。因此,盡管沒有顯式地調(diào)用 retain 方法,但在 NSLog(@“The string is: %@”, str); 之后,str 對(duì)象的引用計(jì)數(shù)不會(huì)被減少。這是因?yàn)樵搶?duì)象在自動(dòng)釋放池中,直到下一個(gè)事件循環(huán)才會(huì)被釋放。
但是,如果你需要在稍后持有這個(gè)對(duì)象,比如將它設(shè)置給一個(gè)實(shí)例變量,那么你需要手動(dòng)增加其引用計(jì)數(shù),以防止在自動(dòng)釋放池釋放時(shí)對(duì)象被釋放,比如像這樣,假設(shè)在另一個(gè)地方創(chuàng)建了一個(gè) ExampleClass 的實(shí)例,并希望將stringValue返回的對(duì)象設(shè)置為該實(shí)例的 instanceVariable:

ExampleClass *exampleObject = [[ExampleClass alloc] init];
NSString *str = [exampleObject stringValue];

exampleObject.instanceVariable = str;

NSLog(@"The instance variable is: %@", exampleObject.instanceVariable);

則需要確保 str 對(duì)象不會(huì)在自動(dòng)釋放池被釋放時(shí)被釋放。因此,我們需要手動(dòng)增加 str 對(duì)象的引用計(jì)數(shù),以確保它不會(huì)被過早釋放:

//手動(dòng)增加引用計(jì)數(shù)
exampleObject.instanceVariable = [str retain];
//......

//并且在稍后手動(dòng)釋放
[exampleObject.instanceVariable release];

保留環(huán)

用引用計(jì)數(shù)機(jī)制時(shí),經(jīng)常要注意的一個(gè)問題就是 “保留環(huán)”,也就是呈環(huán)狀相互引用的多個(gè)對(duì)象。這將導(dǎo)致內(nèi)存泄漏,因?yàn)檠h(huán)中的對(duì)象其保留計(jì)數(shù)不會(huì)降為 0。對(duì)于循環(huán)中的每個(gè)對(duì)象來說,至少還有另外一個(gè)對(duì)象引用著它。圖里的每個(gè)對(duì)象都引用了另外兩個(gè)對(duì)象之中的一個(gè)。在這個(gè)循環(huán)里,所有對(duì)象的保留計(jì)數(shù)都是 1。
在垃圾收集環(huán)境中,通常將這種情況認(rèn)定為 “孤島”。此時(shí),垃圾收集器會(huì)把三個(gè)對(duì)象全都回收走。而在 Objective-C 的引用計(jì)數(shù)架構(gòu)中,則享受不到這一便利。通常采用 “弱引用”來解決此問題,或是從外界命令循環(huán)中的某個(gè)對(duì)象不再保留另外一個(gè)對(duì)象。這兩種辦法都能打破保留環(huán),從而避免內(nèi)存泄漏。

  • 引用計(jì)數(shù)機(jī)制通過可以遞增遞減的計(jì)數(shù)器來管理內(nèi)存。對(duì)象創(chuàng)建好之后,其保留計(jì)數(shù)至少為 1。若保留計(jì)數(shù)為正,則對(duì)象繼續(xù)存活。當(dāng)保留計(jì)數(shù)降為 0 時(shí),對(duì)象就被銷毀了。
  • 在對(duì)象生命期中,其余對(duì)象通過引用來保留或釋放對(duì)象。保留于釋放操作分別會(huì)遞增及遞減保留計(jì)數(shù)。

以ARC簡(jiǎn)化引用計(jì)數(shù)

引用計(jì)數(shù)這個(gè)概念相當(dāng)容易理解。需要執(zhí)行保留與釋放操作的地方也很容易就能看出來。所以 Clang 編譯器項(xiàng)目帶有一個(gè) “靜態(tài)分析器”。用于指明程序里引用計(jì)數(shù)出問題的地方。
比如再拿上一條中的這段代碼舉例子:

  if ([self shouldLogMessage]) {
      NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
      NSLog(@“str = %@, str);
  }

這段代碼中,alloc增加了str對(duì)象的引用計(jì)數(shù),但是在這個(gè)if塊的作用域中,它卻缺少了釋放操作,因此str對(duì)象的引用計(jì)數(shù)比預(yù)期值多1,導(dǎo)致了內(nèi)存泄漏。因?yàn)樯鲜鲞@些規(guī)則很容易表述,所以計(jì)算機(jī)可以簡(jiǎn)單地將其套用在程序上,從而分析出有內(nèi)存泄漏問題的對(duì)象。這正是 “靜態(tài)分析器” 要做的事。

靜態(tài)分析器還有更為深入的用途。既然可以查明內(nèi)存管理問題,那么應(yīng)該也可以根據(jù)需要,預(yù)先加入適當(dāng)?shù)谋A艋蜥尫挪僮饕员苊膺@些問題。自動(dòng)引用計(jì)數(shù)的思路就是源于此。
因此假如使用了ARC,它就會(huì)自動(dòng)將代碼改寫為這樣:

  if ([self shouldLogMessage]) {
      NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
      NSLog(@“str = %@, str);
      [message release];
  }

使用 ARC 時(shí)一定要記住,引用計(jì)數(shù)實(shí)際上還是要執(zhí)行的,只不過保留與釋放操作現(xiàn)在是由 ARC 自動(dòng)為你添加。
由于 ARC 會(huì)自動(dòng)執(zhí)行 retain、release 、autorelease 等操作,所以直接在 ARC 下調(diào)用這些內(nèi)存管理方法是非法的。具體來說,不能調(diào)用下列方法:

  • retain
  • release
  • autorelease
  • dealloc
    直接調(diào)用上述任何方法都會(huì)產(chǎn)生編譯錯(cuò)誤,因?yàn)?ARC 要分析何處應(yīng)該自動(dòng)調(diào)用內(nèi)存管理方法,所以如果手工調(diào)用的話,就會(huì)干擾其工作。
    實(shí)際上,ARC 在調(diào)用這些方法時(shí),并不通過普通的 Objective-C 消息派發(fā)機(jī)制,而是直接調(diào)用其底層 C 語(yǔ)言版本。這樣做性能更好,因?yàn)楸A艏搬尫挪僮餍枰l繁執(zhí)行,所以直接調(diào)用底層函數(shù)能節(jié)省很多 CPU 周期。

使用 ARC 時(shí)必須遵循的方法命名規(guī)則

ARC 將內(nèi)存管理語(yǔ)義在方法名中表示出來確立為硬性規(guī)定。
簡(jiǎn)單地體現(xiàn)在方法名上。若方法名以下列詞語(yǔ)開頭,則其調(diào)用上述四種方法的那段代碼要負(fù)責(zé)釋放方法所返回的對(duì)象:

  • alloc
  • new
  • copy
  • mutableCopy
    若方法名不以上述四個(gè)詞語(yǔ)開頭,則表示其所返回的對(duì)象并不歸調(diào)用者所有。 在這種情況下,返回的對(duì)象會(huì)自動(dòng)釋放,所以其值在跨越方法調(diào)用邊界后依然有效。要想使對(duì)象多存活一段時(shí)間,必須令調(diào)用者保留它才行。
    (我自己簡(jiǎn)單理解就是使用 “alloc”、“new”、“copy” 或者 “mutableCopy” 開頭的方法,方法內(nèi)部的引用計(jì)數(shù)要自己手動(dòng)管理(release或者autorelease等),而不使用這四個(gè)開頭的方法的引用計(jì)數(shù)就是自動(dòng)管理的)。

除了會(huì)自動(dòng)調(diào)用 “保留” 與 “釋放” 方法外,使用 ARC 還有其他好處,它可以執(zhí)行一些手工操作很難甚至無法完成的優(yōu)化,例如,在編譯器,ARC 會(huì)把能夠互相抵消的retain、release、autorelease 操作約簡(jiǎn)。如果發(fā)現(xiàn)在同一對(duì)象上執(zhí)行了多次 “保留” 與 “釋放” 操作,那么 ARC 有時(shí)可以成對(duì)地移除這兩個(gè)操作。
ARC 也包含運(yùn)行期組件。此時(shí)所執(zhí)行的優(yōu)化很有意義,大家看到之后就會(huì)明白為何以后的代碼都應(yīng)該用 ARC 來寫了。前面講到,某些方法在返回對(duì)象前,為其執(zhí)行了 autorelease 操作,而調(diào)用方法的代碼可能需要將返回的對(duì)象保留,比如像下面這種情況就是如此:

_myPerson = [EOCPerson personWithName:@"Bob smith"];

調(diào)用 “personWithName:” 方法會(huì)返回新的 EOCPerson 對(duì)象,而此方法在返回對(duì)象之前,為其調(diào)用了 autorelease 方法。由于實(shí)例變量是個(gè)強(qiáng)引用,所以編譯器在設(shè)置其值的時(shí)候還需要執(zhí)行一次保留操作。因此,前面那段代碼與下面這段手工管理引用計(jì)數(shù)的代碼等效:

EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];

此時(shí)應(yīng)該能看出來, “personWithName:” 方法里面的 autorelease 與上段代碼中的 retain 都是多余的。為提升性能,可將二者刪去。但是,在 ARC 環(huán)境下編譯代碼時(shí),必須考慮 “向后兼容性”(backward compatibility),以兼容那些不使用 ARC 的代碼。
在 ARC 環(huán)境下,編譯器會(huì)盡可能地優(yōu)化代碼,以提高性能和效率。在處理方法中返回自動(dòng)釋放的對(duì)象時(shí),編譯器可以通過一些特殊的函數(shù)來優(yōu)化代碼,從而避免不必要的 autorelease 和 retain 操作,提升代碼的執(zhí)行效率。
具體來說,在方法中返回自動(dòng)釋放的對(duì)象時(shí),編譯器會(huì)替換對(duì) autorelease 方法的調(diào)用,改為調(diào)用 objc_autoreleaseReturnValue 函數(shù)。這個(gè)函數(shù)會(huì)檢查方法返回后即將執(zhí)行的代碼,如果發(fā)現(xiàn)需要在返回的對(duì)象上執(zhí)行 retain 操作,那么就會(huì)設(shè)置一個(gè)標(biāo)志位,而不會(huì)立即執(zhí)行 autorelease 操作。類似地,如果調(diào)用方法的代碼需要保留返回的自動(dòng)釋放對(duì)象,那么編譯器會(huì)將 retain 操作替換為 objc_retainAutoreleasedReturnValue 函數(shù),該函數(shù)會(huì)檢查之前設(shè)置的標(biāo)志位,如果已經(jīng)設(shè)置,則不會(huì)執(zhí)行 retain 操作。
objc_autoreleaseReturnValue 函數(shù)檢測(cè)方法調(diào)用者是否會(huì)立刻保留對(duì)象要根據(jù)處理器來定。

變量的內(nèi)存管理語(yǔ)義

ARC 也會(huì)處理局部變量與實(shí)例變量的內(nèi)存管理。默認(rèn)情況下,每個(gè)變量都是指向?qū)ο蟮膹?qiáng)引用。
在編寫設(shè)置方法(setter)時(shí),使用 ARC 會(huì)簡(jiǎn)單一些。如果不用 ARC ,那么需要像下面這樣來寫:

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

但是在這段代碼中,如果新值和實(shí)例變量已有的值相同,那么在執(zhí)行設(shè)置方法時(shí)會(huì)出現(xiàn)問題。具體來說,當(dāng)新值和舊值相同時(shí),首先會(huì)調(diào)用 [_object release] 來釋放舊值,此時(shí)如果舊值只有當(dāng)前對(duì)象在引用,那么舊值的引用計(jì)數(shù)會(huì)減少為0。接著,會(huì)調(diào)用 [object retain] 來保留新值,但是此時(shí)舊值的內(nèi)存已經(jīng)被釋放掉了,再次對(duì)其執(zhí)行保留操作就會(huì)導(dǎo)致訪問已釋放的內(nèi)存,從而引發(fā)應(yīng)用程序崩潰。
使用 ARC 之后,就不可能發(fā)生這種疏失了。在 ARC 環(huán)境下,與剛才等效的設(shè)置函數(shù)可以這么寫:

- (void)setObject:(id)object {
    _object = object;
}

ARC 會(huì)用一種安全的方式來設(shè)置:先保留新值,再釋放舊值,最后設(shè)置實(shí)例變量。用了 ARC 之后,根本無須考慮這種 “邊界情況”。

  • 在應(yīng)用程序中,可用下列修飾符來改變局部變量與實(shí)例變量的語(yǔ)義:

__strong: 默認(rèn)語(yǔ)義,保留此值。
__unsafe_unretained: 不保留此值,這么做可能不安全,因?yàn)榈鹊皆俅问褂米兞繒r(shí),其對(duì)象可能已經(jīng)回收了。
__weak: 不保留此值,但是變量可以安全使用,因?yàn)槿绻到y(tǒng)把這個(gè)對(duì)象回收了,那么變量也會(huì)自動(dòng)清空。
__autoreleasing: 把對(duì)象 “按引用傳遞” (pass by reference)給方法時(shí),使用這個(gè)特殊的修飾符。此值在方法返回時(shí)自動(dòng)釋放。

比方說,想令實(shí)例變量的語(yǔ)義與不使用 ARC 時(shí)相同,可以運(yùn)用 __weak 或 __unsafe_unretained 修飾符:

@interface EOCClass : NSObject {
    __weak id _weakObject;
    __unsafe_unretained id _unsafeUnretainedObject;
}
@end

我們經(jīng)常會(huì)給局部變量加上修飾符,用以打破由“塊”,所引入的“保留環(huán)”。塊會(huì)自動(dòng)保留其所捕獲的全部對(duì)象,而如果這其中有某個(gè)對(duì)象又保留了塊本身,那么就可能導(dǎo)致 “保留環(huán)”??梢杂?__weak 局部變量來打破這種 “保留環(huán)”:

NSURL *url = [NSURL URLWithString:@"http://www.example.com/"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
EOCNetworkFetcher *__weak weakFetcher  = fetcher;

[fetcher startWithCompletion:^(BOOL success) {
    NSLog(@"Finished fetching from %@", weakFetcher.url);
}];

在這段代碼中,我們使用了 __weak 修飾符來聲明一個(gè)局部變量 weakFetcher,它指向 fetcher 對(duì)象。通過使用 __weak 修飾符,我們避免了塊對(duì) fetcher 對(duì)象的強(qiáng)引用,從而打破了潛在的保留環(huán)。

ARC 如何清理實(shí)例變量

ARC 也負(fù)責(zé)對(duì)實(shí)例變量進(jìn)行內(nèi)存管理。要管理其內(nèi)存,ARC 就必須在 “回收分配給對(duì)象的內(nèi)存”(deallocate)(也稱為 “釋放/回收/解除分配(內(nèi)存)”) 時(shí)生成必要的清理代碼。凡是具備強(qiáng)引用的變量,都必須釋放,ARC 會(huì)在 dealloc 方法中插入這些代碼。當(dāng)手動(dòng)管理引用計(jì)數(shù)時(shí)可以這樣自己來編寫 dealloc 方法:

- (void)dealloc {
    [_foo release];
    [_bar release];
    [super dealloc];
}

這段代碼做了以下幾件事情:

  1. 釋放 _foo 實(shí)例變量:調(diào)用 release 方法來減少對(duì) _foo 對(duì)象的引用計(jì)數(shù),如果引用計(jì)數(shù)為0,則會(huì)釋放 _foo 對(duì)象所占用的內(nèi)存。
  2. 釋放 _bar 實(shí)例變量:同樣地,調(diào)用 release 方法來減少對(duì) _bar 對(duì)象的引用計(jì)數(shù),如果引用計(jì)數(shù)為0,則會(huì)釋放 _bar 對(duì)象所占用的內(nèi)存。
  3. 調(diào)用 super 的 dealloc 方法:調(diào)用父類的 dealloc 方法來執(zhí)行一些必要的清理工作,確保對(duì)象的內(nèi)存被正確釋放。
    使用 ARC 之后,不需要再編寫這種 dealloc 方法。不過,如果有非 Objective-C 的對(duì)象,比如 CoreFoundation 中的對(duì)象或是由 malloc() 分配在堆中的內(nèi)存,那么仍然需要清理。然而不需要像原來那樣調(diào)用超類的 dealloc 方法。前文說過,在 ARC 下不能直接調(diào)用 dealloc。ARC 會(huì)自動(dòng)在 .cxx_destruct 方法中生成代碼并運(yùn)行此方法,而在生成的代碼中會(huì)自動(dòng)調(diào)用超類的 dealloc 方法。ARC 環(huán)境下,dealloc 方法可以像這樣寫:
- (void)dealloc {
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

可以使用CFRelease() 函數(shù)來釋放CoreFoundation 對(duì)象。
可以使用 free() 函數(shù)來釋放通過 malloc() 函數(shù)分配在堆上的內(nèi)存塊_heapAllocatedMemoryBlob

覆寫內(nèi)存管理方法

不使用 ARC 時(shí),可以覆寫內(nèi)存管理方法。比方說,在實(shí)現(xiàn)單例類的時(shí)候,因?yàn)閱卫豢舍尫?,所以我們?jīng)常覆寫 release 方法,將其替換為 “空操作”(no-op)。但在 ARC 環(huán)境下不能這么做,因?yàn)闀?huì)干擾到 ARC 分析對(duì)象生命期的工作。而且,由于開發(fā)者不可調(diào)用及覆寫這些方法,所以 ARC 能夠優(yōu)化 retain、release、autorelease 操作,使之不經(jīng)過 Objective-C 的消息派發(fā)機(jī)制。優(yōu)化后的操作,直接調(diào)用隱藏在運(yùn)行期程序中的 C 函數(shù)。

  • 有 ARC 之后,程序員就無須擔(dān)心內(nèi)存管理問題了。使用 ARC 來編程,可省去類中的許多 “樣板代碼”。
  • ARC 管理對(duì)象生命期的辦法基本上就是:在合適的地方插入 “保留” 及 “釋放”操作。
    在 ARC 環(huán)境下,變量的內(nèi)存管理語(yǔ)義可以通過修飾符指明,而原來需要手工執(zhí)行 “保留” 及 “釋放”操作。
  • 由方法所返回的對(duì)象,其內(nèi)存管理語(yǔ)義總是通過方法名來體現(xiàn)。ARC 將此確定為開發(fā)者必須遵守的規(guī)則。
  • ARC 只負(fù)責(zé)管理 Objective-C 對(duì)象的內(nèi)存。尤其要注意: CoreFoundation 對(duì)象不歸 ARC 管理,開發(fā)者必須適時(shí)調(diào)用 CFRetain/CFRelease。

在dealloc方法中只釋放引用并解除監(jiān)聽

對(duì)象在經(jīng)歷其生命期后,最終會(huì)為系統(tǒng)所回收,這時(shí)就要執(zhí)行 dealloc 方法了。在每個(gè)對(duì)象的生命期內(nèi),此方法僅執(zhí)行一次,也就是當(dāng)保留計(jì)數(shù)降為 0 的時(shí)候。然而具體何時(shí)執(zhí)行,則無法保證。也可以理解成: 我們能夠通過人工觀察保留操作與釋放操作的位置。來預(yù)估此方法何時(shí)即將執(zhí)行。但實(shí)際上,程序庫(kù)會(huì)以開發(fā)者察覺不到的方式操作對(duì)象,從而使回收對(duì)象的真正時(shí)機(jī)和預(yù)期的不同。你決不應(yīng)該自己調(diào)用 dealloc 方法,運(yùn)行期系統(tǒng)會(huì)在適當(dāng)?shù)臅r(shí)候調(diào)用它。而且,一旦調(diào)用過 dealloc 之后,對(duì)象就不再有效了,后續(xù)方法調(diào)用均是無效的。
在 dealloc 方法中主要就是釋放對(duì)象所擁有的引用。對(duì)象所擁有的其他非 Objective-C 對(duì)象也要釋放。比如 CoreFoundation 對(duì)象就必須手工釋放,因?yàn)樗鼈兪怯杉僀 的API 所生成的。
在 dealloc 方法中,通常還要做一件事,那就是把原來配置過低觀測(cè)行為都清理掉,比如消息通知的回收。
delloc應(yīng)該這樣寫:

- (void)dealloc {
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

如果手動(dòng)管理引用計(jì)數(shù)而不使用 ARC 的話,那么最后還需調(diào)用 “[super dealloc]”。ARC 會(huì)自動(dòng)執(zhí)行此操作。
開銷較大或系統(tǒng)內(nèi)部稀缺的資源不應(yīng)該于 dealloc 中釋放引用。像是文件描述符、套接字、大塊內(nèi)存等,都屬于這種資源。不能指望 dealloc 方法必定會(huì)在某個(gè)特定的時(shí)機(jī)調(diào)用,因?yàn)橛幸恍o法預(yù)料的東西可能也持有此對(duì)象。
比方說,如果某對(duì)象管理著連接服務(wù)器所用的套接字,那么也許就需要這種 “清理方法”。此對(duì)象可能要通過套接字連接到數(shù)據(jù)庫(kù)。對(duì)于對(duì)象所屬的類,其接口可以這樣寫:

#import <Foundation/Foundation.h>

@interface EOCServerConnection : NSObject

- (void)open:(NSString *)address;

- (void)close;

@end

這段代碼提供了兩個(gè)方法:

  • open: 方法:用于打開連接到服務(wù)器的套接字。它需要一個(gè)字符串類型的參數(shù) address,表示服務(wù)器的地址。
  • close 方法:用于關(guān)閉當(dāng)前打開的連接。
    這個(gè)類的設(shè)計(jì)允許用戶通過 open: 方法打開連接,然后使用完成后通過 close 方法關(guān)閉連接
    在清理方法而非 dealloc 方法中清理資源還有個(gè)原因,就是系統(tǒng)并不保證每個(gè)創(chuàng)建出來的對(duì)象的 dealloc 都會(huì)執(zhí)行。極個(gè)別情況下,當(dāng)應(yīng)用程序終止時(shí),仍有對(duì)象處于存活狀態(tài),這些對(duì)象沒有收到 dealloc 消息。由于應(yīng)用程序終止之后,其占用的資源也會(huì)返還給操作系統(tǒng),所以實(shí)際上這些對(duì)象也就等于是消亡了。不調(diào)用 dealloc 方法是為了優(yōu)化程序效率。而這也說明系統(tǒng)未必會(huì)在每個(gè)對(duì)象上調(diào)用其 dealloc 方法。
    如果對(duì)象管理著某些資源,那么在 dealloc 中也要調(diào)用 “清理方法”,以防止開發(fā)者忘了清理這些資源。
    在系統(tǒng)回收對(duì)象之前,必須調(diào)用 close 以釋放其資源,否則 close 方法就失去了意義了,因此,沒有適時(shí)調(diào)用 close 方法就是編程錯(cuò)誤,我們應(yīng)該在 dealloc 中補(bǔ)上這次調(diào)用,以防泄漏內(nèi)存。下面舉例說明 close 與 dealloc 方法如何來寫:
- (void)close {
    /*clean up resources*/
    _closed = YES;
}

- (void)dealloc {
    if (!_closed) {
        NSLog(@"ERROR: close was not called before dealloc!");
        [self close];
    }
}

編寫 dealloc 方法時(shí)還需要注意,不要在里面隨便調(diào)用其他方法。
調(diào)用dealloc 方法的那個(gè)線程會(huì)執(zhí)行“最終的釋放操作”,令對(duì)象的保留計(jì)數(shù)降為 0,而某些方法必須在特定的線程里(比如主線程里)調(diào)用才行。若在 dealloc 里調(diào)用了那些方法,則無法保證當(dāng)前這個(gè)線程就是那些方法所需的線程。通過編寫常規(guī)代碼的方式,無論如何都沒辦法保證其會(huì)安全運(yùn)行在正確的線程上,因?yàn)閷?duì)象處于 “正在回收的狀態(tài)”,為了指明此狀況,運(yùn)行期系統(tǒng)已經(jīng)改動(dòng)了對(duì)象內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。
在 dealloc 里也不要調(diào)用屬性的存取方法,因?yàn)橛腥丝赡軙?huì)覆寫這些方法,并與其中做一些無法在回收階段安全執(zhí)行的操作。此外,屬性可能正處于 “鍵值觀測(cè)” (KVO) 機(jī)制的監(jiān)控之下,該屬性的觀察者(observer) 可能會(huì)在屬性值改變時(shí) “保留” 或使用這個(gè)即將回收的對(duì)象。這種做法會(huì)令運(yùn)行期系統(tǒng)的狀態(tài)完全失調(diào),從而導(dǎo)致一些莫名其妙的錯(cuò)誤。

  • 在 dealloc 方法里,應(yīng)該做的事情就是釋放指向其他對(duì)象的引用,并取消原來訂閱的“鍵值觀測(cè)”(KVO)或 NSNOtificationCenter 等通知,不要做其他事情。
  • 如果對(duì)象持有文件描述符等系統(tǒng)資源,那么應(yīng)該專門編寫一個(gè)方法來釋放此種資源。這樣的類要和其使用者約定: 用完資源后必須調(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)了。

編寫“異常安全代碼” 時(shí)留意內(nèi)存管理問題

許多時(shí)下流行的編程語(yǔ)言都提供了 “異?!边@一特性。在當(dāng)前的運(yùn)行期系統(tǒng)中,C++ 與 Objective-C 的異常相互兼容,也就是說,從其中一門語(yǔ)言里拋出的異常能用另外一門語(yǔ)言所編寫的 “異常處理程序”來捕獲。
Objective-C 的錯(cuò)誤模型表明,異常只應(yīng)在發(fā)生嚴(yán)重錯(cuò)誤后拋出,不過有時(shí)仍然需要編寫代碼來捕獲并處理異常。比如使用 Objective-C++ 來編碼時(shí),或是編碼中用到了第三方程序庫(kù)而此程序庫(kù)所拋出的異常又不受你控制時(shí),就需要捕獲及處理異常了。此外,有些系統(tǒng)庫(kù)也會(huì)用到異常,比如,在使用 “鍵值觀測(cè)”(KVO)功能時(shí),若想注銷一個(gè)尚未注冊(cè)的“觀察者”,便會(huì)拋出異常。
在 try 塊中,如果先保留了某個(gè)對(duì)象,然后在釋放它之前又拋出了異常,那么,除非 catch 塊能處理此問題,否則對(duì)象所占內(nèi)存就將泄漏。
異常處理例程將自動(dòng)銷毀對(duì)象,然而在手動(dòng)管理引用計(jì)數(shù)時(shí),銷毀工作有些麻煩。以下面這段使用手工引用計(jì)數(shù)的 Objective-C 代碼為例:

@try {
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
    [object release];
}
@catch (...) {
    NSLog(@"Whoops, there was an error. Oh well...");
}

這段代碼使用了 Objective-C 中的異常處理機(jī)制,嘗試執(zhí)行一些可能會(huì)拋出異常的代碼,并在發(fā)生異常時(shí)捕獲并處理它。具體來說:
@try { … } @catch (…) { … } 是 Objective-C 中的異常處理語(yǔ)法。@try 塊用于包含可能會(huì)拋出異常的代碼,而 @catch 塊則用于捕獲異常并進(jìn)行處理。
在 @try 塊中,首先創(chuàng)建了一個(gè) EOCSomeClass 類的對(duì)象 object,然后調(diào)用了 doSomethingThatMayThrow 方法。這個(gè)方法可能會(huì)拋出異常。
在 @try 塊的最后,調(diào)用了 [object release] 方法來釋放 object 對(duì)象。這表明代碼的編寫者使用了手動(dòng)內(nèi)存管理。
如果在 @try 塊中的代碼拋出了異常,那么異常處理流程會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的 @catch 塊中。在這個(gè)例子中,@catch 塊中的代碼會(huì)執(zhí)行,它打印了一條錯(cuò)誤日志。由于 @catch 塊的參數(shù)是 …,表示捕獲所有類型的異常,因此無論什么類型的異常都會(huì)被捕獲并處理。
但如果 doSomethingThatMayThrow 拋出異常了呢?由于異常會(huì)令執(zhí)行過程終止并跳至catch 塊,因而其后的那行 release 代碼不會(huì)運(yùn)行。在這種情況下,如果代碼拋出異常,那么對(duì)象就泄漏了。這么做不好。解決方法是使用 @finally 塊,無論是否拋出異常,其中代碼都保證會(huì)運(yùn)行,且只運(yùn)行一次。比方說,剛才那段代碼可改寫如下:

EOCSomeClass *object;

@try {
    object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
} 
@catch (...) {
    NSLog(@"Whoops, there was an error. Oh well...");
} 
@finally {
    [object release];
}

在 ARC 環(huán)境下,問題會(huì)更嚴(yán)重。下面這段使用 ARC 的代碼與修改前的那段代碼等效:

@try {
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
}
@catch (...) {
    NSLog(@"Whoope, there was an error. Oh well...");
}

在 ARC 下,由于不能手動(dòng)調(diào)用 release 方法來釋放對(duì)象,因此無法像在手動(dòng)管理內(nèi)存時(shí)那樣將釋放操作放在 @finally 塊中。而且,ARC 不會(huì)自動(dòng)處理異常導(dǎo)致的內(nèi)存釋放,因?yàn)檫@需要添加大量的額外代碼來跟蹤待清理的對(duì)象,并在拋出異常時(shí)釋放它們,這可能會(huì)影響程序的性能并增加應(yīng)用程序的大小。

雖然在 Objective-C 代碼中,拋出異常通常是在應(yīng)用程序必須因異常狀況而終止時(shí)才發(fā)生的,但默認(rèn)情況下 ARC 并不會(huì)為異常處理添加額外的代碼。這是因?yàn)樵趹?yīng)用程序即將終止時(shí),是否會(huì)發(fā)生內(nèi)存泄漏已經(jīng)無關(guān)緊要了。因此,默認(rèn)情況下,ARC 不會(huì)為異常處理添加額外的代碼。如果需要在 ARC 環(huán)境下處理異常并進(jìn)行自動(dòng)內(nèi)存管理,可以通過開啟 -fobjc-arc-exceptions 編譯器標(biāo)志來實(shí)現(xiàn)。
但最重要的是:在發(fā)現(xiàn)大量異常捕獲操作時(shí),應(yīng)考慮重構(gòu)代碼。

  • 捕獲異常時(shí),一定要注意將 try 塊所創(chuàng)立的對(duì)象清理干凈。
  • 在默認(rèn)情況下,ARC 不生成安全處理異常所需的清理代碼。開啟編譯器標(biāo)志后,可以生成這種代碼,不過會(huì)導(dǎo)致應(yīng)用程序變大,而且會(huì)降低運(yùn)行效率。

以弱引用避免保留環(huán)

對(duì)象圖里經(jīng)常會(huì)出現(xiàn)一種情況,就是幾個(gè)對(duì)象都以某種方式互相引用,從而形成“環(huán)”。這種情況通常會(huì)泄漏內(nèi)存,因?yàn)樽詈鬀]有別的東西會(huì)引用環(huán)中的對(duì)象。這樣的話,環(huán)里的對(duì)象就無法為外界所訪問了,但對(duì)象之間尚有引用,這些引用使得它們都能繼續(xù)存活下去,而不會(huì)為系統(tǒng)所回收。最簡(jiǎn)單的保留環(huán)由兩個(gè)對(duì)象構(gòu)成,它們互相引用對(duì)方。
例如這里有兩個(gè)類:

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject

@property (nonatomic, strong) EOCClassB *other;

@end

@interface EOCClassB : NSObject

@property (nonatomic, strong) EOCClassA *other;

@end

保留環(huán)會(huì)導(dǎo)致內(nèi)存泄漏。如果只剩一個(gè)引用還指向保留環(huán)中的實(shí)例,而現(xiàn)在又把這個(gè)引用移除,那么整個(gè)保留環(huán)就泄漏了。
避免保留環(huán)的最佳方式就是弱引用。這種引用經(jīng)常用來表示 “非擁有關(guān)系”。將屬性聲明為 unsafe_unretained 即可。修改剛才那段范例代碼:

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject

@property (nonatomic, strong) EOCClassB *other;

@end

@interface EOCClassB : NSObject

@property (nonatomic, unsafe_unretained) EOCClassA *other;

@end

修改之后,EOCClassB 實(shí)例就不再通過 other 屬性來?yè)碛?EOCClassA 實(shí)例了。屬性特質(zhì) (attribute) 中的 unsafe_unretained 一詞表明,屬性值可能不安全,而且不歸此實(shí)例所擁有。如果系統(tǒng)已經(jīng)把屬性所指的那個(gè)對(duì)象回收了,那么在其上調(diào)用方法可能會(huì)使應(yīng)用程序崩潰。由于本對(duì)象并不保留屬性對(duì)象,因此其有可能為系統(tǒng)所回收。
還可以使用weak 屬性特質(zhì),剛剛的代碼還可以修改為:

@property (nonatomic,weak) EOCClassA *other;

unsafe_unretained 與 weak 屬性,在其所指的對(duì)象回收以后表現(xiàn)出來的行為不同。當(dāng)指向 EOCClassA 實(shí)例的引用移除后,unsafe_unretained 屬性仍然指向那個(gè)已經(jīng)回收的實(shí)例,而 weak 屬性則指向 nil。
一般來說,如果不擁有某對(duì)象,那就不要保留它,當(dāng)然 collection 例外。有時(shí),對(duì)象中的引用會(huì)指向另外一個(gè)并不歸自己擁有的對(duì)象,比如 Delegate 模式就是這樣。

  • 將某些引用設(shè)為 weak,可避免出現(xiàn) “保留環(huán)”。
  • weak 引用可以自動(dòng)清空,也可以不自動(dòng)清空。自動(dòng)清空(autonilling)是隨著 ARC 而引入的新特性,由運(yùn)行期系統(tǒng)來實(shí)現(xiàn)。在具備自動(dòng)清空功能的弱引用上,可以隨意讀取其數(shù)據(jù),因?yàn)檫@種引用不會(huì)指向已經(jīng)回收過的對(duì)象。

以“自動(dòng)釋放池塊”降低內(nèi)存峰值

由前面的內(nèi)容知道:自動(dòng)釋放池用于存放那些需要稍后某個(gè)時(shí)刻釋放的對(duì)象。
創(chuàng)建自動(dòng)釋放池所用語(yǔ)法如下:

@autorelease {
    /...
}

通常只有一個(gè)地方需要?jiǎng)?chuàng)建自動(dòng)釋放池,那就是在 main 函數(shù)里。比如說iOS程序的main函數(shù)一般這樣寫:

int main(int argc, char *argv[]) {
    @autoreleasepool {
        
        return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate");
    }
}

從技術(shù)角度看,不是非得有個(gè)“自動(dòng)釋放池塊”才行。因?yàn)閴K的末尾恰好就是應(yīng)用程序的終止處,而此時(shí)操作系統(tǒng)會(huì)把程序所占的全部?jī)?nèi)存都釋放掉。這個(gè)池可以理解成最外圍捕捉全部自動(dòng)釋放對(duì)象所用的池。
下面這段代碼中的花括號(hào)定義了自動(dòng)釋放池的范圍。自動(dòng)釋放池于左花括號(hào)處創(chuàng)建,并于對(duì)應(yīng)的右花括號(hào)處自動(dòng)清空。位于自動(dòng)釋放池范圍內(nèi)的對(duì)象,將在此范圍末尾處收到 release 消息。自動(dòng)釋放池可以嵌套。系統(tǒng)在自動(dòng)釋放對(duì)象時(shí),會(huì)把它放到最內(nèi)層的池里。比方說:

@autoreleasepool {
    NSString *string = [NSString stringWithFormat:@"1= %i", 1];
    @autoreleasepool {
        NSNumber *number = [NSNumber numberWithInt:1];
    }
}

這段代碼創(chuàng)建了兩個(gè)自動(dòng)釋放池。第一個(gè)自動(dòng)釋放池從第 1 行開始,到第 10 行結(jié)束。在此范圍內(nèi),字符串 string 被創(chuàng)建,并且在自動(dòng)釋放池的末尾被釋放。
在第 5 行到第 8 行之間的范圍內(nèi),又創(chuàng)建了一個(gè)新的自動(dòng)釋放池。在這個(gè)內(nèi)層自動(dòng)釋放池中,數(shù)字 number 被創(chuàng)建,然后在內(nèi)層自動(dòng)釋放池的末尾被釋放。
注意,內(nèi)層自動(dòng)釋放池的范圍是嵌套在外層自動(dòng)釋放池的范圍內(nèi)的。因此,number 對(duì)象的釋放操作會(huì)在外層自動(dòng)釋放池的范圍結(jié)束時(shí)執(zhí)行,而 string 對(duì)象的釋放操作則會(huì)在整個(gè)自動(dòng)釋放池的范圍結(jié)束時(shí)執(zhí)行。

有如下代碼:

for (int i = 0; i< 100000; i++) {
    [self doSomethingWithInt:i];
}

如果 “doSomethingWithInt:”方法要?jiǎng)?chuàng)建臨時(shí)對(duì)象,那么這些對(duì)象很可能會(huì)放在自動(dòng)釋放池里。即便這些對(duì)象在調(diào)用完方法之后就不再使用了,它們也依然處于存活狀態(tài),因?yàn)槟壳斑€在自動(dòng)釋放池里,等待系統(tǒng)稍后將其釋放并回收。然而,自動(dòng)釋放池要等線程執(zhí)行下一次事件循環(huán)時(shí)才會(huì)清空。即執(zhí)行 for 循環(huán)時(shí),會(huì)持續(xù)有新的對(duì)象創(chuàng)建出來,并加入自動(dòng)釋放池中。所有這種對(duì)象都要等 for 循環(huán)執(zhí)行完才會(huì)釋放。這樣一來,在執(zhí)行 for 循環(huán)時(shí),應(yīng)用程序所占內(nèi)存量就會(huì)持續(xù)上漲,而等到所有臨時(shí)對(duì)象都釋放后,內(nèi)存用量又會(huì)突然下降。
或者比如說要從數(shù)據(jù)庫(kù)中讀數(shù)據(jù):

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];

for (NSDictionary *record in databaseRecords) {
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];
}

若記錄有很多條,則內(nèi)存中也會(huì)有很多不必要的臨時(shí)對(duì)象,它們本來應(yīng)該提早回收的。增加一個(gè)自動(dòng)釋放池即可解決此問題。如果把循環(huán)內(nèi)的代碼包裹在“自動(dòng)釋放池塊”中,那么在循環(huán)中自動(dòng)釋放的對(duì)象就會(huì)放在這個(gè)池,而不是線程的主池里面。例如:

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];

for (NSDictionary *record in databaseRecords) {
    @autoreleasepool {
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

新增的自動(dòng)釋放池塊可以減少內(nèi)存峰值,因?yàn)橄到y(tǒng)會(huì)在塊的末尾把某些對(duì)象回收掉。
是否應(yīng)該用池來優(yōu)化效率,完全取決于具體的應(yīng)用程序。所以盡量不要建立額外的自動(dòng)釋放池。
有一種老式寫法,是使用 NSAutoreleasePool 對(duì)象。但是這種寫法并不會(huì)在每次執(zhí)行 for 循環(huán)時(shí)都清空池,通常用來創(chuàng)建那種偶爾要清空的池,采用隨著 ARC 所引入的新語(yǔ)法,可以創(chuàng)建出更為 “輕量級(jí)”的自動(dòng)釋放池。現(xiàn)在可以改用自動(dòng)釋放池塊把 for 循環(huán)中的語(yǔ)句包起來,這樣的話,每次執(zhí)行循環(huán)時(shí)都會(huì)建立并清空自動(dòng)釋放池。

  • 自動(dòng)釋放池排布在棧中,對(duì)象收到 autorelease 消息后,系統(tǒng)將其放入最頂端的池里。
  • 合理運(yùn)用自動(dòng)釋放池,可降低應(yīng)用程序的內(nèi)存峰值。
  • @autoreleasepool 這種新式寫法能創(chuàng)建出更為輕便的自動(dòng)釋放池。

用“僵尸對(duì)象”調(diào)試內(nèi)存管理問題

Cocoa 提供了 “僵尸對(duì)象”(Zombie Object)這個(gè)非常方便的功能。啟用這項(xiàng)調(diào)試功能之后,運(yùn)行期系統(tǒng)會(huì)把所有已經(jīng)回收的實(shí)例轉(zhuǎn)化成特殊的“僵尸對(duì)象”,而不會(huì)真正回收它們。這種對(duì)象所在的核心內(nèi)存無法重用,因此不可能遭到覆寫。僵尸對(duì)象收到消息后,會(huì)拋出異常,其中準(zhǔn)確說明了發(fā)送過來的消息,并描述了回收之前的那個(gè)對(duì)象。僵尸對(duì)象是調(diào)試內(nèi)存管理問題的最佳方式。給僵尸對(duì)象發(fā)送消息后,控制臺(tái)會(huì)打印消息,而應(yīng)用程序則會(huì)終止。
也可以在Xcode 里打開此選項(xiàng),這樣的話,Xcode 在運(yùn)行應(yīng)用程序時(shí)會(huì)自動(dòng)設(shè)置環(huán)境變量。開啟方法:編輯應(yīng)用程序的 Scheme,在對(duì)話框左側(cè)選擇 “Run”,然后切換至 “Diagnostics” 分頁(yè),最后勾選 “Enable Zombie Objects” 選項(xiàng)。
僵尸對(duì)象的工作原理涉及Objective-C的運(yùn)行時(shí)程序庫(kù)、Foundation框架和CoreFoundation框架的實(shí)現(xiàn)代碼。當(dāng)系統(tǒng)即將回收對(duì)象時(shí),如果啟用了僵尸對(duì)象功能,會(huì)執(zhí)行一個(gè)額外的步驟,即將對(duì)象轉(zhuǎn)化為僵尸對(duì)象而不是立即回收。
僵尸類是從名為 NSZombie 的模板類里復(fù)制出來的。這些僵尸類沒有多少事情可做,只是充當(dāng)一個(gè)標(biāo)記。
僵尸類的作用會(huì)在消息轉(zhuǎn)發(fā)例程中體現(xiàn)出來。 NSZombie 類(以及所有從該類拷貝出來的類)并未實(shí)現(xiàn)任何方法。此類沒有超類,因此和 NSObject 一樣,也是個(gè)“根類”,該類只有一個(gè)實(shí)例變量,叫做 isa ,所有 Objective-C 的根類都必須有此變量。由于這個(gè)輕量級(jí)的類沒有實(shí)現(xiàn)任何方法,所以發(fā)給它的全部消息都要經(jīng)過 “完整的消息轉(zhuǎn)發(fā)機(jī)制”。

  • 系統(tǒng)在回收對(duì)象時(shí),可以不將其真的回收,而是把它轉(zhuǎn)化為僵尸對(duì)象。通過環(huán)境變量 NSZombieEnable 可開啟此功能。
  • 系統(tǒng)會(huì)修改對(duì)象的 isa 指針,令其指向特殊的僵尸類,從而使該對(duì)象變?yōu)榻┦瑢?duì)象。僵尸類能夠相應(yīng)所有的選擇子,響應(yīng)方式為:打印一條包含消息內(nèi)容及其接收者的消息,然后終止應(yīng)用程序。

不要使用retainCount

每個(gè)對(duì)象的引用計(jì)數(shù)都有一個(gè)計(jì)數(shù)器,其值表明還有多少個(gè)其他對(duì)象想令此對(duì)象繼續(xù)存活。
NSObject 協(xié)議中定義了下列方法,用于查詢對(duì)象當(dāng)前的保留計(jì)數(shù):

  • (NSUInteger)retainCount;
    然而 ARC 已經(jīng)將此方法廢棄了。如果在 ARC 中調(diào)用,編譯器就會(huì)報(bào)錯(cuò)。但問題在于,保留計(jì)數(shù)的絕對(duì)值一般都與開發(fā)者所應(yīng)留意的事情完全無關(guān)。即便只在調(diào)試時(shí)才能調(diào)用此方法,通常也還是無所助益的。
    因?yàn)樗祷氐谋A粲?jì)數(shù)只是某個(gè)給定時(shí)間點(diǎn)上的值。該方法并未考慮到系統(tǒng)會(huì)稍后把自動(dòng)釋放池清空,因而不會(huì)將后續(xù)的釋放操作從返回值里減去,這樣的話,此值就未必能真實(shí)反映實(shí)際的保留計(jì)數(shù)了。

開發(fā)者在期望系統(tǒng)于某處回收對(duì)象時(shí),應(yīng)該確保沒有尚未抵消的保留操作,也就是不要令保留計(jì)數(shù)大于期望值。在這種情況下,如果發(fā)現(xiàn)某對(duì)象的內(nèi)存泄漏了,那么應(yīng)該檢查還有誰仍然保留這個(gè)對(duì)象,并查明其為何沒有釋放此對(duì)象。

即便只為調(diào)試,此方法也不是很有用。由于對(duì)象可能處在自動(dòng)釋放池中,所以其保留計(jì)數(shù)未必如想象般精確。那到底何時(shí)才應(yīng)該用 retainCount 呢?最佳答案是:絕對(duì)不要用,尤其考慮到蘋果公司在引入 ARC 之后已正式將其廢棄,就更不應(yīng)該用了。

  • 對(duì)象的保留計(jì)數(shù)看似有用,實(shí)則不然,因?yàn)槿魏谓o定時(shí)間點(diǎn)上的“絕對(duì)保留計(jì)數(shù)”(absolute retain count)都無法反映對(duì)象生命期的全貌。
  • 引入 ARC 之后,retainCount 方法就正式廢止了,在 ARC 下調(diào)用該方法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)。

理解“塊”這一概念

塊可以實(shí)現(xiàn)閉包,它與函數(shù)類似,只不過是直接定義在另一個(gè)函數(shù)里的,和定義它的那個(gè)函數(shù)共享同一個(gè)范圍內(nèi)的東西。塊用 “^” 符號(hào)來表示,后面跟著一對(duì)花括號(hào),括號(hào)里面是塊的實(shí)現(xiàn)代碼。
塊的強(qiáng)大之處是:在聲明它的范圍里。所有變量都可以為其所捕獲。這也就是說,那個(gè)范圍里的全部變量,在塊里依然可用。比如,下面這段代碼所定義的塊,就使用了塊以外的變量:

int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
    return a + b + addItional;
};
int add = addBlock(2, 5);

默認(rèn)情況下,為塊所捕獲的變量,是不可以在塊里修改的。聲明變量的時(shí)候可以加上 __block 修飾符,這樣就可以在塊內(nèi)修改了。例:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BooL *stop) {
    if([number compare:@2] == NSOrderedAscending) {
    count++;
    }
}];

這段范例代碼也演示了 “內(nèi)聯(lián)塊”的用法。傳給 “numerateObjectsUsingBlock:” 方法的塊并未先賦給局部變量,而是直接內(nèi)聯(lián)在函數(shù)調(diào)用里了。這樣可以把所有業(yè)務(wù)邏輯都放在一處。
如果塊所捕獲的變量是對(duì)象類型,那么就會(huì)自動(dòng)保留它。系統(tǒng)在釋放這個(gè)塊的時(shí)候,也會(huì)將其一并釋放。塊本身可視為對(duì)象。塊本身也和其他對(duì)象一樣,有引用計(jì)數(shù)。當(dāng)最后一個(gè)指向塊的引用移走之后,塊就回收了?;厥諘r(shí)也會(huì)釋放塊所捕獲的變量,以便平衡捕獲時(shí)所執(zhí)行的保留操作。
如果將塊定義在 OC 類的實(shí)例方法中,那么除了可以訪問類的所有實(shí)例變量之外,還可以使用 self 變量。塊總能修改實(shí)例變量,所以在聲明時(shí)無須加 _ _block 。不過,如果通過讀取或?qū)懭氩僮鞑东@了實(shí)例變量,那么也會(huì)自動(dòng)把 self 變量一并捕獲了,因?yàn)閷?shí)例變量是與 self 所指代的實(shí)例關(guān)聯(lián)在一起的。就是說:在OC類的實(shí)例方法中使用塊時(shí)可以輕松地訪問和操作當(dāng)前實(shí)例的變量和方法而不必過多關(guān)注__block的使用。這使得在塊內(nèi)部處理實(shí)例變量變得更加方便。
例:

@interface EOCClass
- (void)anInstanceMethod {
    void (^someBlock)() = ^{
      _anInstanceVariable = @"Something";
      NSlog(@"_anInstanceVariable = %@", _anInstanceVariable);
    };
} 
@end

如果某個(gè) EOCClass 實(shí)例正在執(zhí)行 anInstanceMethod 方法,那么 self 變量就指向此實(shí)例。由于塊里沒有明確使用 self 變量,所以很容易就會(huì)忘記 self 變量其實(shí)也為塊所捕獲了。直接訪問實(shí)例變量和通過 self 來訪問是等效的。
self 也是個(gè)對(duì)象,因而塊在捕獲它時(shí)也會(huì)將其保留。如果 self 所指代的那個(gè)對(duì)象同時(shí)保留了塊,那么這種情況通常就會(huì)導(dǎo)致 “保留環(huán)”。

塊的內(nèi)部結(jié)構(gòu)

每個(gè) OC 對(duì)象都占據(jù)著某個(gè)內(nèi)存區(qū)域。每個(gè)對(duì)象所占的內(nèi)存區(qū)域也有大有小。塊本身也是對(duì)象,在存放塊對(duì)象的內(nèi)存區(qū)域中,首個(gè)變量是指向 Class 對(duì)象的指針isa。其余內(nèi)存里含有塊對(duì)象正常運(yùn)轉(zhuǎn)所需要的各種信息。
在內(nèi)存布局中,最重要的就是 invoke 變量,這是個(gè)函數(shù)指針,指向塊的實(shí)現(xiàn)代碼。descriptor 變量是指向結(jié)構(gòu)體的指針,每個(gè)塊里都包含此結(jié)構(gòu)體,其中聲明了塊對(duì)象的總體大小,還聲明了 copy 與 dispose 這兩個(gè)輔助函數(shù)所對(duì)應(yīng)的函數(shù)指針。
塊還會(huì)把它所捕獲的所有變量都拷貝一份。這些拷貝放在 descriptor 變量后面,捕獲了多少個(gè)變量,就要占據(jù)多少內(nèi)存空間。

全局塊、棧塊及堆塊

定義塊的時(shí)候,其所占的內(nèi)存區(qū)域是分配在棧中的。這就是說,塊只在定義它的那個(gè)范圍內(nèi)有效。這里有段代碼:

void (^block)();
if () {
    block = ^{
        NSLog(@"Block A");
    };
} else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

這個(gè)代碼問題在于,塊的生命周期與其定義的范圍有關(guān)。在這里,兩個(gè)塊都分配在棧內(nèi)存中,而棧內(nèi)存的生命周期通常與包含它的作用域相關(guān)。一旦超出 if 或 else 的作用域,棧上的塊可能會(huì)被釋放,而且內(nèi)存區(qū)域可能被覆寫。
這樣的代碼在編譯時(shí)通常能夠通過,但在運(yùn)行時(shí)可能會(huì)出現(xiàn)問題,因?yàn)?block() 調(diào)用時(shí),塊的定義范圍可能已經(jīng)結(jié)束,棧上的內(nèi)存可能已經(jīng)被其他變量或數(shù)據(jù)覆寫。
為解決此問題,可以通過調(diào)用copy方法。這樣的話,就可以把塊從棧復(fù)制到堆了??截惡蟮膲K,可以在定義它的那個(gè)范圍之外使用。而且,一旦復(fù)制到堆上,塊就成了帶引用計(jì)數(shù)的對(duì)象了。后續(xù)的復(fù)制操作都不會(huì)真的執(zhí)行復(fù)制,只是遞增塊對(duì)象的引用計(jì)數(shù)。而“分配在棧上的塊”則無須明確釋放,因?yàn)闂?nèi)存本來就會(huì)自動(dòng)回收。
應(yīng)該改正為:

void (^block)();
if () {
    block = [^{
        NSLog(@"Block A");
    } copy];
} else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

還有一類塊叫做 “全局塊”。這種塊不會(huì)捕捉任何狀態(tài),運(yùn)行時(shí)也無須有狀態(tài)來參與。塊所使用的整個(gè)內(nèi)存區(qū)域在編譯期已經(jīng)完全確定了,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時(shí)候于棧中創(chuàng)建。
全局塊的拷貝操作是個(gè)空操作,因?yàn)槿謮K決不可能為系統(tǒng)所回收。這種塊實(shí)際上相當(dāng)于單例。下面是個(gè)全局塊:

void (^block)() = ^{
  NSLog(@"This is a block");
}

這完全是種優(yōu)化技術(shù)

  • 塊是C、C++、Objective-C 中的詞法閉包。
  • 塊可接受參數(shù),也可返回值。
  • 塊可以分配在?;蚨焉希部梢允侨值?。分配在棧上的塊可拷貝到堆里,這樣的話,就和標(biāo)準(zhǔn)的 Objective-C 對(duì)象一樣,具備引用計(jì)數(shù)了。

為常用的塊類型創(chuàng)建typedef

每個(gè)塊都具備其“固有類型”,因而可將其賦給適當(dāng)類型的變量。這個(gè)類型由塊所接受的參數(shù)及其返回值組成。
與其他類型的變量不同,在定義塊變量時(shí),要把變量名放在類型之中,而不要放在右側(cè)。這種語(yǔ)法非常難記,也非常難讀。鑒于此,我們應(yīng)該為常用的塊類型起個(gè)別名。
為了隱藏復(fù)雜的塊類型,需要用到C 語(yǔ)言中名為 “類型定義”的特性。typedef 關(guān)鍵字用于給類型起個(gè)易讀的別名。
舉個(gè)例子,不使用typedef的話是這樣聲明一個(gè)塊:

void (^sumBlock)(int, int) = ^(int a, int b) {
    int sum = a + b;
    NSLog(@"Sum: %d", sum);
};

使用typedef:

// 使用 typedef 創(chuàng)建塊類型別名
typedef void (^SumBlock)(int, int);
// 使用塊類型別名聲明塊變量
SumBlock sumBlock = ^(int a, int b) {
    int sum = a + b;
    NSLog(@"Sum: %d", sum);
};

這次代碼讀起來就順暢多了:與定義其他變量時(shí)一樣,變量類型在左邊,變量名在右邊??梢娛褂?typedef 可以將復(fù)雜的塊類型聲明簡(jiǎn)化為易讀的別名,使代碼更加清晰、易懂。通過項(xiàng)特性,可以把使用塊的 API 做得更為易用些。

定義方法參數(shù)所用的塊類型語(yǔ)法,又和定義變量時(shí)不同。若能把方法簽名中的參數(shù)類型寫成一個(gè)詞,那讀起來就順口多了。于是,可以給參數(shù)類型起個(gè)別名,然后使用詞名稱來定義:

//創(chuàng)建一個(gè)名為EOCCompletionHandler的塊類型別名
typedef void (^EOCCompletionHandler) (NSData *data, NSError *error);
//在方法簽名中,使用了先前創(chuàng)建的塊類型別名EOCCompletionHandler作為參數(shù)類型
//startWithCompletionHandler方法接受一個(gè)塊作為參數(shù),而這個(gè)塊的類型就是 EOCCompletionHandler
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

現(xiàn)在看上去就簡(jiǎn)單多了,而且易于理解。
使用類型定義還有個(gè)好處,就是重構(gòu)塊的類型簽名時(shí)會(huì)很方便。
比如給塊再加一個(gè)參數(shù),那么只需要修改類型定義語(yǔ)句即可:

typedef void (^EOCCompletionHandler) (NSData *data, NSTimeInterval duration, NSError *error);

修改之后,凡是使用了這個(gè)類型定義的地方都會(huì)無法編譯而且報(bào)的是同一種錯(cuò)誤,于是開發(fā)者可據(jù)此逐個(gè)修復(fù)。

最好在使用塊類型的類中定義這些 typedef,而且還應(yīng)該把這個(gè)類的名字加在由 typedef 所定義的新類型名前面,這樣可以闡明塊的用途。還可以用 typedef 給同一個(gè)塊簽名類型創(chuàng)建數(shù)個(gè)別名。
比如Accounts 框架:

typedef void (^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);

如果有好幾個(gè)類都要執(zhí)行相似但各有區(qū)別的異步任務(wù),而這幾個(gè)類又不能放入同一個(gè)繼承體系,那么,每個(gè)類就應(yīng)該有自己的 completion handler 類型。這幾個(gè) completion handler 的簽名也許完全相同,但最好還是在每個(gè)類里各自定義一個(gè)別名,而不要同一個(gè)名稱。反之,若這些類能納入同一個(gè)繼承中,則應(yīng)該將類型定義語(yǔ)句放在超類中,以供各子類使用。

  • 以 typedef 重新定義塊類型,可令塊變量用起來更加簡(jiǎn)單。
  • 定義新類型時(shí)應(yīng)遵從現(xiàn)有的命名習(xí)慣,勿使其名稱與別的類型相沖突。
  • 不妨為同一個(gè)塊簽名定義多個(gè)類型別名。如果要重構(gòu)的代碼使用了塊類型的某個(gè)別名,那么只需要修改相應(yīng) typedef 中的塊簽名即可,無須改動(dòng)其他 typedef。

用handler塊降低代碼分散程度

為用戶界面編碼時(shí),一種常用的范式就是 “異步執(zhí)行任務(wù)”。這種范式的好處在于:處理用戶界面的顯示及觸摸操作所用的線程,不會(huì)因?yàn)橐獔?zhí)行 I/O 或網(wǎng)絡(luò)通信這類耗時(shí)的任務(wù)而阻塞。這個(gè)線程通常稱為主線程。假設(shè)把執(zhí)行異步任務(wù)的方法做成同步的,那么在執(zhí)行任務(wù)時(shí),用戶界面就變得無法響應(yīng)用戶輸入了。某些情況下,如果應(yīng)用程序在一定時(shí)間內(nèi)無響應(yīng),那么就會(huì)自動(dòng)終止。iOS 系統(tǒng)上的應(yīng)用程序就是如此,“系統(tǒng)監(jiān)控器”在發(fā)現(xiàn)某個(gè)應(yīng)用程序的主線程已經(jīng)阻塞了一段時(shí)間之后,就會(huì)令其終止。

I/O 操作(輸入/輸出操作):
輸入操作(Input): 從外部設(shè)備或文件中讀取數(shù)據(jù)到計(jì)算機(jī)系統(tǒng)中。例如,從鍵盤讀取用戶輸入、從磁盤讀取文件等。
輸出操作(Output): 將計(jì)算機(jī)系統(tǒng)中的數(shù)據(jù)發(fā)送到外部設(shè)備或存儲(chǔ)到文件中。例如,將數(shù)據(jù)寫入顯示器顯示、將結(jié)果寫入磁盤文件等。
I/O 操作可能涉及到慢速的設(shè)備(如硬盤、網(wǎng)絡(luò)),因此在進(jìn)行這些操作時(shí),系統(tǒng)可能需要等待一段時(shí)間。
網(wǎng)絡(luò)通信:
指在不同計(jì)算機(jī)或設(shè)備之間傳遞數(shù)據(jù)的過程。
通過網(wǎng)絡(luò)連接,計(jì)算機(jī)系統(tǒng)可以通過一系列協(xié)議進(jìn)行數(shù)據(jù)的發(fā)送和接收,包括傳輸層協(xié)議(如TCP或UDP)和應(yīng)用層協(xié)議(如HTTP、FTP等)。
網(wǎng)絡(luò)通信包括從客戶端到服務(wù)器的請(qǐng)求和響應(yīng),文件傳輸,遠(yuǎn)程過程調(diào)用(RPC)等。
在移動(dòng)應(yīng)用或網(wǎng)絡(luò)應(yīng)用中,常見的 I/O 操作和網(wǎng)絡(luò)通信包括:
讀寫本地文件: 通過文件系統(tǒng)進(jìn)行讀寫,例如保存應(yīng)用程序數(shù)據(jù)、讀取配置文件等。
數(shù)據(jù)庫(kù)操作: 與本地或遠(yuǎn)程數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)交互,例如存儲(chǔ)和檢索用戶信息。
HTTP 請(qǐng)求和響應(yīng): 通過網(wǎng)絡(luò)協(xié)議進(jìn)行數(shù)據(jù)傳輸,例如從服務(wù)器獲取數(shù)據(jù)、上傳文件等。
Socket 編程: 直接在網(wǎng)絡(luò)上建立連接,進(jìn)行實(shí)時(shí)通信,例如聊天應(yīng)用、在線游戲等。

異步方法在執(zhí)行完任務(wù)之后,需要以某種手段通知相關(guān)代碼。實(shí)現(xiàn)此功能有很多方法。常用的技巧是設(shè)計(jì)一個(gè)委托協(xié)議,令關(guān)注此事件的對(duì)象遵從該協(xié)議。對(duì)象成為 delegate 之后,就可以在相關(guān)事件發(fā)生時(shí)(例如某個(gè)異步任務(wù)執(zhí)行完畢時(shí))得到通知了。委托模式有個(gè)缺點(diǎn):如果類要分別使用多個(gè)獲取器下載不同數(shù)據(jù),那么就得在 delegate 回調(diào)方法里根據(jù)傳入的獲取器參數(shù)來切換。這么寫代碼,不僅會(huì)令 delegate 回調(diào)方法變得很長(zhǎng),而且還要把網(wǎng)絡(luò)數(shù)據(jù)獲取器對(duì)象保存為實(shí)例變量,以便在判斷語(yǔ)句中使用。
然而如果改用塊來寫的話,代碼會(huì)更清晰。塊可以令這種 API 變得更緊致,同時(shí)令開發(fā)者調(diào)用起來更加方便。改用塊來寫的好處是:無須保存獲取器,也無須在回調(diào)方法里切換。每個(gè) completion handler 的業(yè)務(wù)邏輯,都是和相關(guān)的獲取器對(duì)象一起來定義的。
比如像這樣:

#import <Foundation/Foundation.h>
// 定義塊類型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
// 使用塊作為參數(shù)的方法
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
@implementation EOCNetworkFetcher
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.example.com"]];
        dispatch_async(dispatch_get_main_queue(), ^{
            if (handler) {
                handler(data);
            }
        });
    });
}
@end

用塊寫出來的代碼顯然更為整潔。而且,由于塊聲明在創(chuàng)建獲取器的范圍里,所以它可以訪問此范圍內(nèi)的全部變量。

這種寫法還有其他用途,比如,現(xiàn)在很多基于塊的 API 都使用塊來處理錯(cuò)誤。這又分為兩種辦法??梢苑謩e用兩個(gè)處理程序來處理操作失敗的情況和操作成功的情況。也可以把處理失敗情況所需的代碼,與處理正常情況所用的代碼,都封裝到同一個(gè) completion handler 塊里。如果想采用兩個(gè)獨(dú)立的處理程序,那么可以這樣設(shè)計(jì) API:

#import <Foundation/Foundation.h>
// 聲明塊類型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);

@interface EOCNetworkFetcher : NSObject
// 初始化方法聲明
- (instancetype)initWithURL:(NSURL *)url;
// 方法聲明,接受兩個(gè)塊作為參數(shù)
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end

調(diào)用方式如下:

EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data) {
  // Handle success
} failureHandler:^(NSError *error) {
  // Handle failure
}];

另一種風(fēng)格則像下面這樣,把處理成功情況和失敗情況所用的代碼全放在一個(gè)塊里:

#import <Foundation/Foundation.h>
// 聲明塊類型,接受兩個(gè)參數(shù):NSData 和 NSError
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
// 初始化方法聲明
- (instancetype)initWithURL:(NSURL *)url;
// 方法聲明,接受一個(gè)塊作為參數(shù)
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

調(diào)用方式如下:

// 創(chuàng)建網(wǎng)絡(luò)獲取器實(shí)例
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
    if (error) {
        // 處理失敗情況
        NSLog(@"Error occurred: %@", error);
    } else {
        // 處理成功情況
        NSLog(@"Data received: %@", data);
    }
}];

這種寫法的缺點(diǎn)是:由于全部邏輯都寫在一起,所以會(huì)令塊變得比較長(zhǎng)且比較復(fù)雜。然而也有好處,那就是更為靈活。比方說,在傳入錯(cuò)誤信息時(shí),可以把數(shù)據(jù)也傳進(jìn)來。而且還有一個(gè)優(yōu)點(diǎn)是調(diào)用 API 的代碼可能會(huì)在處理成功響應(yīng)的過程中發(fā)現(xiàn)錯(cuò)誤。

總體來說,筆者建議使用同一個(gè)塊來處理成功與失敗情況,蘋果公司似乎也是這樣設(shè)計(jì)其 API 的。

有時(shí)需要在相關(guān)時(shí)間點(diǎn)執(zhí)行回調(diào)操作,這種情況也可以使用 Handler 塊。
基于 handler 來設(shè)計(jì) API 還有個(gè)原因,就是某些代碼必須運(yùn)行在特定的線程上。例如NSNotificationCenter 就屬于這種 API,它提供了一個(gè)方法,調(diào)用者可以經(jīng)由此方法來注冊(cè)想要接收的通知,等到相關(guān)事件發(fā)生時(shí),通知中心就會(huì)執(zhí)行注冊(cè)好的那個(gè)塊。調(diào)用者可以指定某個(gè)塊應(yīng)該安排在哪個(gè)執(zhí)行隊(duì)列里。

- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;

此處傳入的 NSOperationQueue 參數(shù)就表示出觸發(fā)通知時(shí)用來執(zhí)行塊代碼的那個(gè)隊(duì)列。這是個(gè)“操作隊(duì)列”,而非“底層 GCD 隊(duì)列”,不過兩者語(yǔ)義相同。

  • 在創(chuàng)建對(duì)象時(shí),可以使用內(nèi)聯(lián)的 handler 塊將相關(guān)業(yè)務(wù)邏輯一并聲明。
  • 在有多個(gè)實(shí)例需要監(jiān)控時(shí),如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對(duì)象來切換,而若改用 handler 塊來實(shí)現(xiàn),則可直接將塊與相關(guān)對(duì)象放在一起。
  • 設(shè)計(jì) API 時(shí)如果用到了 handler 塊,那么可以增加一個(gè)參數(shù),使調(diào)用者可通過此參數(shù)來決定應(yīng)該把塊安排在哪個(gè)隊(duì)列上執(zhí)行。

用塊引用其所屬對(duì)象時(shí)不要出現(xiàn)保留環(huán)

使用塊時(shí),若不仔細(xì)思量,則很容易導(dǎo)致“保留環(huán)”。比如說下面這個(gè)例子:


// EOCNetworkFetcher.h

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

@property (nonatomic, strong, readonly) NSURL *url;

- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;- 
@end

// EOCNetworkFetcher.m

#import "EOCNetworkFetcher.h"

@interface EOCNetworkFetcher ()

@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadedData;

@end

@implementation EOCNetworkFetcher

- (instancetype)initWithURL:(NSURL *)url {
    if ((self = [super init])) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
    self.completionHandler = completion;
    // Start the request
    // Request sets downloadedData property
    // When request is finished, p_requestCompleted is called
}

- (void)p_requestCompleted {
    if (_completionHandler) {
        _completionHandler(_downloadedData);
    }
}

@end

某個(gè)類可能會(huì)創(chuàng)建這個(gè)網(wǎng)絡(luò)請(qǐng)求的實(shí)力,并用其從url中下載數(shù)據(jù):

// EOCClass.m

@implementation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}

- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
    }];
}

@end

在上面這段代碼中:當(dāng) EOCClass 類的實(shí)例調(diào)用 downloadData 方法時(shí),它會(huì)創(chuàng)建一個(gè) EOCNetworkFetcher 的實(shí)例對(duì)象 _networkFetcher。
在 _networkFetcher 的 startWithCompletionHandler: 方法中,傳入了一個(gè)塊作為參數(shù),該塊會(huì)捕獲 self(即 EOCClass 實(shí)例),因?yàn)樗枰O(shè)置 EOCClass 實(shí)例中的 _fetchedData 實(shí)例變量。
這樣,塊持有了 EOCClass 實(shí)例,EOCClass 實(shí)例持有了 _networkFetcher 實(shí)例,而 _networkFetcher 實(shí)例又持有了傳入的塊,形成了保留環(huán)。

要打破保留環(huán)也很容易:要么令 _networkFetcher 實(shí)例變量不再引用獲取器,要么令獲取器的 completionHandler 屬性不在持有 handler 塊。在這個(gè)例子中,應(yīng)該等 completion handler 塊執(zhí)行完畢后,再去打破保留環(huán),以便使獲取器對(duì)象在 handler 塊執(zhí)行期間保持存活狀態(tài)。比方說,completion handler 塊的代碼可以這么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data) {
    NSLog(@"Request for URL %@ finished", _networkFetcher.url);
    _fetchedData = data;
    _networkFetcher = nil;    
];

一般來說,只要適時(shí)清理掉環(huán)中的某個(gè)引用,即可解決此問題,然而,未必總有這種機(jī)會(huì)。若是 completion handler 一直不運(yùn)行,那么保留環(huán)就無法打破,于是內(nèi)存就會(huì)泄漏。
如果 completion handler 塊所引用的對(duì)象最終又引用了這個(gè)塊本身,那么就會(huì)出現(xiàn)另一種形式的保留環(huán)。
舉個(gè)例子:

- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
}

在上面這個(gè)例子中,downloadData 方法創(chuàng)建了一個(gè) EOCNetworkFetcher 的實(shí)例 networkFetcher,并設(shè)置了其 startWithCompletionHandler 方法的 completion handler 塊。
在 completion handler 塊中,使用了 networkFetcher 實(shí)例的 url 屬性。由于塊內(nèi)部引用了 networkFetcher 實(shí)例,因此塊會(huì)保留這個(gè)實(shí)例。
反過來,networkFetcher 實(shí)例也持有了 completion handler 塊,因?yàn)?networkFetcher 的屬性 completionHandler 是一個(gè) copy 類型的塊屬性。
因此,形成了一個(gè)保留環(huán):networkFetcher 持有 completion handler 塊,而 completion handler 塊又持有 networkFetcher 實(shí)例。這會(huì)導(dǎo)致 networkFetcher 實(shí)例和其相關(guān)的對(duì)象無法被釋放,從而造成內(nèi)存泄漏。
這個(gè)問題可以這樣解決:只需要將 p_requestCompleted 方法按如下方式修改即可:

- (void)p_requestCompleted {
    if (_completionHandler) {
      _completionHandler(_downloadedData);
   }
   self.completionHandler = nil;
}

這樣一來,只要下載請(qǐng)求執(zhí)行完畢,保留環(huán)就解除了,而獲取器對(duì)象也將會(huì)在必要時(shí)為系統(tǒng)所回收。

注意,要在 start 方法中把 completion handler 作為參數(shù)傳進(jìn)去。假如把 completion handler 暴露為獲取器對(duì)象的公共屬性,那么就不便在執(zhí)行完下載請(qǐng)求之后直接將其清理掉了,因?yàn)榧热灰呀?jīng)把 handler 作為屬性公布了,那就意味著調(diào)用者可以自由使用它,若是此時(shí)又在內(nèi)部將其清理掉的話,則會(huì)破壞“封裝語(yǔ)義”。在這種情況下要想打破保留環(huán),只有一個(gè)辦法可用,那就是強(qiáng)迫調(diào)用者在 handler 代碼里自己把 compleionHandler 屬性清理干凈。

  • 如果塊所捕獲的對(duì)象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問題。
  • 一定要找個(gè)適當(dāng)?shù)臅r(shí)機(jī)解除保留環(huán),而不能把責(zé)任推給API 的調(diào)用者。

多用派發(fā)隊(duì)列,少用同步鎖

派發(fā)隊(duì)列

在 Objective-C 中,派發(fā)隊(duì)列是一種用來管理任務(wù)執(zhí)行的隊(duì)列系統(tǒng)。它基于GCD框架,用于異步執(zhí)行任務(wù),可以在串行或并發(fā)的隊(duì)列上執(zhí)行任務(wù)。派發(fā)隊(duì)列可以是串行隊(duì)列或并發(fā)隊(duì)列。

  • 串行隊(duì)列:串行隊(duì)列中的任務(wù)一個(gè)接一個(gè)按順序執(zhí)行,每個(gè)任務(wù)執(zhí)行完畢后才會(huì)執(zhí)行下一個(gè)任務(wù)。
  • 并發(fā)隊(duì)列:并發(fā)隊(duì)列中的任務(wù)可以同時(shí)執(zhí)行,不需要等待前一個(gè)任務(wù)完成。

同步鎖

同步鎖是一種用于控制并發(fā)訪問共享資源的機(jī)制。在多線程環(huán)境下,當(dāng)多個(gè)線程同時(shí)訪問某個(gè)共享資源時(shí),可能會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的情況,導(dǎo)致程序出現(xiàn)不確定的行為或錯(cuò)誤。同步鎖可以確保在某個(gè)線程修改共享資源時(shí),其他線程不會(huì)同時(shí)進(jìn)行修改,從而保證數(shù)據(jù)的一致性和正確性。
常見的同步鎖機(jī)制包括:

  • @synchronized 塊:使用 @synchronized 關(guān)鍵字創(chuàng)建臨界區(qū),確保同一時(shí)間只有一個(gè)線程能夠訪問臨界區(qū)中的代碼。
  • NSLock 類:NSLock 是 Foundation 框架中提供的一個(gè)鎖對(duì)象,可以使用 lockunlock 方法來實(shí)現(xiàn)對(duì)臨界區(qū)的加鎖和解鎖操作。
  • dispatch_semaphore 信號(hào)量:通過信號(hào)量來實(shí)現(xiàn)對(duì)臨界區(qū)的控制,可以使用 dispatch_semaphore_waitdispatch_semaphore_signal 函數(shù)來實(shí)現(xiàn)加鎖和解鎖操作。
  • NSRecursiveLock 類:NSRecursiveLockNSLock 的子類,允許同一個(gè)線程多次對(duì)鎖進(jìn)行加鎖操作,可以解決遞歸調(diào)用時(shí)可能出現(xiàn)的死鎖問題。

在 GCD 出現(xiàn)之前,如果有多個(gè)線程要執(zhí)行同一份代碼,通常要使用鎖來實(shí)現(xiàn)某種同步機(jī)制,有兩種辦法,第一種是采用內(nèi)置的 “同步塊”:

- (void)synchronizedMethod {
    @synchronized(self) {
        //。。。
    }
}

這種寫法會(huì)根據(jù)給定的對(duì)象,自動(dòng)創(chuàng)建一個(gè)鎖,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處,鎖就釋放了。但是若是在 self 對(duì)象上頻繁加鎖,那么程序可能要等另一段與此無關(guān)的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當(dāng)前代碼,這樣做其實(shí)并沒有必要。
另一個(gè)辦法是直接使用 NSLock 對(duì)象:

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock lock];
    //...
    [_lock unlock];
}

它可以創(chuàng)建一個(gè) NSLock 對(duì)象 _lock,然后在 synchronizedMethod 方法中,使用 lock 方法來獲取鎖,然后執(zhí)行安全的代碼。執(zhí)行完安全代碼后,通過調(diào)用 unlock 方法釋放鎖。這種方式提供了更多的控制能力,可以更靈活地管理鎖的獲取和釋放
也可以使用 NSRecursiveLock 這種 “遞歸鎖”(重入鎖),線程能夠多次持有該鎖,而不會(huì)出現(xiàn)死鎖現(xiàn)象。

為什么要多用派發(fā)隊(duì)列,少用同步鎖

使用同步鎖的這幾種方法有其缺陷。比方說,在極端情況下,同步塊會(huì)導(dǎo)致死鎖,另外,其效率也不見得很高,而如果直接使用鎖對(duì)象的話,一旦遇到死鎖,就會(huì)非常麻煩。
因此我們可以使用 GCD ,它能以更簡(jiǎn)單、更高效的形式為代碼加鎖。

在之前的學(xué)習(xí)中,我們學(xué)習(xí)過atomic 特質(zhì),它是用于修飾屬性的原子性,并且該特性是與鎖這個(gè)機(jī)制緊密相連的,而GCD與鎖的機(jī)制不同,它是通過隊(duì)列和調(diào)度機(jī)制來管理任務(wù)的執(zhí)行,而不需要顯式地使用鎖來保護(hù)共享數(shù)據(jù),因此使用GCD不需要依賴屬性的原子性。
而再回顧一下atomic特質(zhì),它可以指定屬性的存取方法。而開發(fā)者如果想自己來編寫訪問方法的話,那么通常會(huì)這樣寫:

- (NSString *)someString {
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

剛才說過,濫用 @synchronized(self) 會(huì)很危險(xiǎn),因?yàn)樗型綁K都會(huì)彼此搶奪同一個(gè)鎖。這么做雖然能提供某種程度的 “線程安全”(thread safety),但卻無法保證訪問該對(duì)象時(shí)絕對(duì)是線程安全的。
所以要用一種簡(jiǎn)單而高效的辦法代替同步塊或鎖對(duì)象,那就是使用 “串行同步隊(duì)列”。將讀取操作及寫入操作都安排在同一個(gè)隊(duì)列里,即可保證數(shù)據(jù)同步:

_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(_suncQueue, ^{
        _someString = someString;       
    });
}

在上面這段代碼中,創(chuàng)建了一個(gè)串行同步隊(duì)列 _syncQueue,用于處理對(duì) someString 屬性的讀取和寫入操作。
在someString 方法中,通過調(diào)用 dispatch_sync 將讀取操作安排在 _syncQueue 中執(zhí)行。在串行隊(duì)列中使用 dispatch_sync 可以確保讀取操作按順序執(zhí)行,并且在讀取操作完成之前阻塞當(dāng)前線程。讀取操作執(zhí)行完畢后,將結(jié)果賦值給 localSomeString 變量,然后返回。
setSomeString: 方法中,同樣使用 dispatch_sync 將寫入操作安排在 _syncQueue 中執(zhí)行。寫入操作也會(huì)按照順序執(zhí)行。把設(shè)置操作與獲取操作都安排在序列化的隊(duì)列里執(zhí)行,這樣的話,所有針對(duì)屬性的訪問操作就都同步了。

設(shè)置代碼也可以這樣寫:

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

上面這段代碼用異步派發(fā)替代了同步派發(fā),意味著設(shè)置方法 setSomeString: 中的實(shí)例變量 _someString 的操作會(huì)在一個(gè)后臺(tái)線程上執(zhí)行,而不會(huì)阻塞當(dāng)前線程。這可以提高設(shè)置方法的執(zhí)行速度,并使得調(diào)用者不必等待設(shè)置操作完成。
但這么改有個(gè)壞處:這種寫法可能比原來慢,因?yàn)閳?zhí)行異步派發(fā)時(shí),需要拷貝塊。若拷貝塊所用的時(shí)間明顯超過執(zhí)行塊所花的時(shí)間,則這種寫法將比原來更慢。

  • 多個(gè)獲取方法可以并發(fā)執(zhí)行,而獲取方法與設(shè)置方法之間不能并發(fā)執(zhí)行
    利用這個(gè)特點(diǎn),還能寫出更快一些的代碼來,這次不用串行隊(duì)列,而改用并發(fā)隊(duì)列:
_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_async(_syncQueue, ^{
        _someString = someString;
    });
}

在上面這段代碼中:_syncQueue 是一個(gè)并發(fā)隊(duì)列,通過 dispatch_get_global_queue 函數(shù)創(chuàng)建,它允許多個(gè)任務(wù)同時(shí)在不同的線程上執(zhí)行。
someString 方法用 dispatch_sync 函數(shù)將讀取操作添加到 _syncQueue 中,并且等待這個(gè)操作完成后再返回結(jié)果。這確保了在多個(gè)線程同時(shí)調(diào)用 someString 方法時(shí),能夠安全地讀取 _someString 的值。
setSomeString: 方法使用 dispatch_async 函數(shù)將寫入操作添加到 _syncQueue 中,這樣就可以確保多個(gè)設(shè)置方法之間不會(huì)并發(fā)執(zhí)行,保證了數(shù)據(jù)的一致性和安全性。

像現(xiàn)在這樣寫代碼,還無法正確實(shí)現(xiàn)同步。所有讀取操作與寫入操作都會(huì)在同一個(gè)隊(duì)列上執(zhí)行,不過由于是并發(fā)隊(duì)列,所以讀取與寫入操作可以隨時(shí)執(zhí)行。而我們恰恰不想讓這些操作隨意執(zhí)行。此問題用一個(gè)簡(jiǎn)單的 GCD 功能即可解決,它就是柵欄。

在并發(fā)隊(duì)列中,柵欄塊的作用是確保在其前面的任務(wù)執(zhí)行完畢后,才會(huì)執(zhí)行柵欄塊,而在其后的任務(wù)則會(huì)等待柵欄塊執(zhí)行完畢后才能繼續(xù)執(zhí)行。
下列函數(shù)可以向隊(duì)列中派發(fā)塊,將其作為柵欄使用:

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

上面這段代碼向并發(fā)隊(duì)列中添加?xùn)艡趬K,保證了柵欄塊之前的任務(wù)并發(fā)執(zhí)行,而柵欄塊本身及其后的任務(wù)則是順序執(zhí)行的。這樣,可以確保寫入操作在讀取操作之后進(jìn)行,從而避免了并發(fā)讀取與寫入操作導(dǎo)致的數(shù)據(jù)同步問題。
使用柵欄具體的實(shí)現(xiàn)代碼:

_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 函數(shù)將一個(gè)柵欄塊提交到 _syncQueue 中執(zhí)行
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

在這個(gè)并發(fā)隊(duì)列中,讀取操作是用普通的塊來實(shí)現(xiàn)的,而寫入操作則是用柵欄塊來實(shí)現(xiàn)的。讀取操作可以并行,但寫入操作必須單獨(dú)執(zhí)行,因?yàn)樗菛艡趬K。
這種做法肯定比使用串行隊(duì)列要快。注意,設(shè)置函數(shù)也可以改用同步的柵欄塊來實(shí)現(xiàn)。

  • 派發(fā)隊(duì)列可用來表述同步語(yǔ)義(synchronization semantic),這種做法要比使用 @synchronized 塊或 NSLock 對(duì)象更簡(jiǎn)單。
  • 將同步與異步派發(fā)結(jié)合起來,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為,而這么做卻不會(huì)阻塞執(zhí)行異步派發(fā)的線程。
  • 使用同步隊(duì)列及柵欄塊,可以令同步行為更加高效。

多用GCD,少用performSelector系列方法

什么是performSelector

performSelector 是 Objective-C 中的一個(gè)方法,用于在對(duì)象上調(diào)用指定的方法,并且可以延遲執(zhí)行或在指定的線程上執(zhí)行。

- (nullable id)performSelector:(SEL)aSelector;

它會(huì)在當(dāng)前線程中調(diào)用指定的方法 aSelector,如果方法有返回值,則返回該返回值;如果方法沒有返回值,則返回 nil。它相當(dāng)于直接調(diào)用選擇子:[object selectorName];
還有一個(gè)帶有 withObject: 參數(shù)的版本,可以傳遞一個(gè)參數(shù)給指定的方法:

- (nullable id)performSelector:(SEL)aSelector withObject:(nullable id)anObject;

這種編程方式極為靈活,經(jīng)??捎脕砗?jiǎn)化復(fù)雜的代碼。

為何要多用GCD

但是使用performSelector的特性的代價(jià)是,如果在 ARC 下編譯代碼,那么編譯器會(huì)發(fā)出如下警示信息:

warning: performSelector may cause a leak because its selector
is unknown [-Warc-performSelector-leaks]

原因在于,編譯器并不知道將要調(diào)用的選擇子是什么,因此,也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚。而且,由于編譯器不知道方法名,所以就沒辦法運(yùn)用 ARC 的內(nèi)存管理規(guī)則來判定返回值是不是應(yīng)該釋放。鑒于此,ARC 采用了比較謹(jǐn)慎的做法,就是不添加釋放操作。然而這么做可能導(dǎo)致內(nèi)存泄漏,因?yàn)榉椒ㄔ诜祷貙?duì)象時(shí)可能已經(jīng)將其保留了。
有如下代碼:

SEL selector;

if (/*some condition*/) {
    selector = @selector(newObject);
} else if (/*some other condition*/) {
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}

id ret = [object performSelector:selector];

if (selector == @selector(newObject) || selector == @selector(copy)) {
    [ret release]; // 手動(dòng)釋放返回的對(duì)象
}

此代碼中如果調(diào)用的是兩個(gè)選擇子之一,那么 ret 對(duì)象應(yīng)由這段代碼來釋放,而如果是第三個(gè)選擇子,則無須釋放。不僅在 ARC 環(huán)境下應(yīng)該如此,而且在非 ARC 環(huán)境下也應(yīng)該這么做,這樣才算嚴(yán)格遵循了方法的命名規(guī)范。如果不使用 ARC,那么在前兩種情況下需要手動(dòng)釋放 ret 對(duì)象,而在后一種情況下則不需要釋放。這個(gè)問題很容易忽視,而且就算用靜態(tài)分析器,也很難偵測(cè)到內(nèi)存泄漏。performSelector 系列的方法之所以要謹(jǐn)慎使用,這就是其中一個(gè)原因。
少使用performSelector系列方法的另一個(gè)原因在于:該系列方法返回值只能是 void 或?qū)ο箢愋?。盡管所要執(zhí)行的選擇子也可以返回 void,但是 performSelector 方法的返回值類型畢竟是 id。如果想返回整數(shù)或浮點(diǎn)數(shù)等類型的值,那么就需要執(zhí)行一些復(fù)雜的轉(zhuǎn)換操作了,而這種轉(zhuǎn)換很容易出錯(cuò)。
performSelector 還有如下幾個(gè)版本,可以在發(fā)消息時(shí)順便傳遞參數(shù):

- (id)performSelector:(SEL)selector withObject:(id)object;
- (id)performSelector:(SEL)selector withObject:(id)objectA withObject:(id)objectB;

比方說,可以用下面這個(gè)版本來設(shè)置對(duì)象中名為 value 的屬性值:

id object = /*an object with a property called value */;
id newValue = /*new value for the property */;
[object performSelector:@selector(setValue:) withObject:newValue];

由于參數(shù)類型是 id,所以傳入的參數(shù)必須是對(duì)象才行。如果選擇子所接受的參數(shù)是整數(shù)或浮點(diǎn)數(shù),那就不要采用這些方法了。

performSelector 系列方法還有個(gè)功能,就是可以延后執(zhí)行選擇子,或?qū)⑵浞旁诹硪粋€(gè)線程上執(zhí)行。下面列出了此方法中一些更為常用的版本:

- (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait;

但是這些方法都無法處理帶有兩個(gè)參數(shù)的選擇子。能夠指定執(zhí)行線程的那些方法也不是特別通用。如果要用這些方法,就得把許多參數(shù)都打包到字典中,然后在受調(diào)用的方法里將其提取出來,這樣會(huì)增加開銷。而且還可能出 bug。
所以就需要改用其他替代方案,使它不受這些限制。
最主要的替代方案就是使用塊,可以通過在 GCD 中使用 block 來實(shí)現(xiàn)。延后執(zhí)行可以用 dispatch_after 來實(shí)現(xiàn),在另一個(gè)線程上執(zhí)行任務(wù)則可通過 dispatch_sync 及 dispatch_async 來實(shí)現(xiàn)。
比如要要延后執(zhí)行某項(xiàng)任務(wù),我們應(yīng)該:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    [self doSomething];
});

要把任務(wù)放在主線程上執(zhí)行應(yīng)該:文章來源地址http://www.zghlxwxcb.cn/news/detail-834542.html

dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});
  • performSelector 系列方法在內(nèi)存管理方面容易有疏失。它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC 編譯器也就無法插入適當(dāng)?shù)膬?nèi)存管理方法。
  • performSelector 系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發(fā)送給方法的參數(shù)個(gè)數(shù)都受到限制。
  • 如果想把任務(wù)放在另一個(gè)線程上執(zhí)行,那么最好不要用 performSelector 系列方法,而是應(yīng)該把任務(wù)封裝到塊里,然后調(diào)用GCD 的相關(guān)方法來實(shí)現(xiàn)。

到了這里,關(guān)于Effective Objective-C 學(xué)習(xí)(三)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【Effective Objective - C】—— 熟悉Objective-C

    【Effective Objective - C】—— 熟悉Objective-C

    Objective-C通過一套全新語(yǔ)法,在C語(yǔ)言基礎(chǔ)上添加了面向?qū)ο筇匦?。Objective-C的語(yǔ)法中頻繁使用方括號(hào),而且不吝于寫出極長(zhǎng)的方法名,這通常令許多人覺得此語(yǔ)言較為冗長(zhǎng)。其實(shí)這樣寫出來的代碼十分易讀,只是C++或Java程序員不太能適應(yīng)。 Objective-C語(yǔ)言學(xué)起來很快,但有很

    2024年01月16日
    瀏覽(36)
  • 【Effective Objective-C 2.0】協(xié)議與分類

    第23條:通過委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信 在軟件開發(fā)中,對(duì)象之間的通信是不可避免的。委托模式(Delegate Pattern)是一種常用的實(shí)現(xiàn)對(duì)象間通信的方式,也被稱為代理模式。委托模式的核心思想是定義一套接口,使得一個(gè)對(duì)象可以將部分職責(zé)委托給另一個(gè)對(duì)象。在iO

    2024年02月21日
    瀏覽(32)
  • 【學(xué)習(xí)iOS高質(zhì)量開發(fā)】——熟悉Objective-C

    【學(xué)習(xí)iOS高質(zhì)量開發(fā)】——熟悉Objective-C

    Objective-C和Java、C++都是面向?qū)ο笳Z(yǔ)言但是語(yǔ)法上有些許不同。OC使用“消息結(jié)構(gòu)”而不是“函數(shù)調(diào)用”,這二者的區(qū)別主要體現(xiàn)在: 使用消息結(jié)構(gòu)的語(yǔ)言,其運(yùn)行所應(yīng)執(zhí)行的代碼由運(yùn)行環(huán)境來決定;使用函數(shù)調(diào)用的語(yǔ)言,則由編譯器決定。OC的重要工作都是由運(yùn)行期組件來完

    2024年01月19日
    瀏覽(23)
  • objective-c 基礎(chǔ)學(xué)習(xí)

    objective-c 基礎(chǔ)學(xué)習(xí)

    目錄 第一節(jié):OC 介紹 ??第二節(jié):Fundation 框架 ?第三節(jié):NSLog 相對(duì)于print 的增強(qiáng) ?第四節(jié):NSString ?第五節(jié):oc新增數(shù)據(jù)類型 第六節(jié): 類和對(duì)象 ?類的方法的聲明與實(shí)現(xiàn) ?第七節(jié):類和對(duì)象的存儲(chǔ) 第八節(jié):nil 與 NULL 第九節(jié):分組導(dǎo)航標(biāo)記#pragma mark ?第十節(jié):方法與函

    2024年02月07日
    瀏覽(33)
  • Objective-C學(xué)習(xí)筆記(block,協(xié)議)4.10

    1.block :是一個(gè)數(shù)據(jù)類型,存儲(chǔ)一段代碼,代碼可以有參數(shù)有返回值。 2.聲明block : 返回值類型 (^block變量名稱)(參數(shù)列表); ? ? ? ? ? ? ? ? ? ? ? ? int (^myblock) (int num1,int num2); ? ? ? ? ? ? ? ? ? ? ? ? 代碼段格式:^返回值類型(參數(shù)列表){ ? ? ? ? ? ? ? ? ? ? ? ? ? ?

    2024年04月17日
    瀏覽(20)
  • Objective-C學(xué)習(xí)筆記(ARC,分類,延展)4.10

    1.自動(dòng)釋放池@autoreleasepool: 存入到自動(dòng)釋放池的對(duì)象,在自動(dòng)釋放池銷毀時(shí),會(huì)自動(dòng)調(diào)用池內(nèi)所有對(duì)象的release方法。調(diào)用autorelease方法將對(duì)象放入自動(dòng)釋放池。? ? ?Person *p1 = [ [ [ Person alloc ] init ] autorelease]; 2.在類方法里寫一個(gè)同名的方法,用于創(chuàng)造對(duì)象。 (+)instancetype pers

    2024年04月17日
    瀏覽(23)
  • Objective-C學(xué)習(xí)筆記(內(nèi)存管理、property參數(shù))4.9

    1.引用計(jì)數(shù)器retainCount: 每個(gè)對(duì)象都有這個(gè)屬性,默認(rèn)值為1,記錄當(dāng)前對(duì)象有多少人用。 ? ?為對(duì)象發(fā)送一條retain/release消息,對(duì)象的引用計(jì)數(shù)器加/減1,為對(duì)象發(fā)一條retainCount,得到對(duì)象的引用計(jì)數(shù)器值,當(dāng)計(jì)數(shù)器為0時(shí)自動(dòng)調(diào)用對(duì)象的dealloc方法。 ? ?手動(dòng)發(fā)送消息:-(id)perf

    2024年04月13日
    瀏覽(20)
  • 第一章 熟悉Objective-C

    Objective—C語(yǔ)言是由Smalltalk演化而來,后者是消息型語(yǔ)言的鼻祖,所以該語(yǔ)言使用的“消息結(jié)構(gòu)”而非“函數(shù)調(diào)用”。 1. 消息和函數(shù)調(diào)用之間的區(qū)別 關(guān)鍵區(qū)別在于: 使用消息結(jié)構(gòu)的語(yǔ)言,其運(yùn)行所應(yīng)執(zhí)行的代碼由運(yùn)行環(huán)境來決定;而使用函數(shù)調(diào)用的語(yǔ)言,則由編譯器決定。

    2024年01月18日
    瀏覽(19)
  • use gnustep objective-c

    專注于概念,而不是迷失在語(yǔ)言技術(shù)細(xì)節(jié)中 編程語(yǔ)言的目的是成為一個(gè)更好的程序員; 也就是說,在設(shè)計(jì)和實(shí)現(xiàn)新系統(tǒng)以及維護(hù)舊系統(tǒng)方面變得更加有效 header preprocess interface implementation method variable declare and expression comment basic integer set and float set enum type void type derive type incl

    2024年02月14日
    瀏覽(24)
  • Objective-C日期NSDate使用

    2024年01月21日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包