第五章 內(nèi)存管理

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

Objective-C語(yǔ)言使用引用計(jì)數(shù)來(lái)管理內(nèi)存,也就是說(shuō),每個(gè)對(duì)象都有個(gè)可以遞增或遞減的計(jì)數(shù)器。如果想使某個(gè)對(duì)象繼續(xù)存活,那就遞增其引用計(jì)數(shù);用完了之后,就遞減其計(jì)數(shù)。計(jì)數(shù)變?yōu)?,就表示沒(méi)人關(guān)注此對(duì)象了,于是,就可以把它銷毀。

從Mac OS X 10.8開(kāi)始,“垃圾回收集器”(garbage collector)已經(jīng)正式廢棄了,以O(shè)bjective-C代碼編寫Mac OS X程序時(shí)不應(yīng)再使用它,而iOS則從未支持過(guò)垃圾收集。

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

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

  • retain:遞增保留計(jì)數(shù)。
  • release:遞減保留計(jì)數(shù)。
  • autorelease:待稍后清理“自動(dòng)釋放池”(autorelease pool)時(shí),再遞減保留計(jì)數(shù)。

查看保留計(jì)數(shù)的方法叫做retainCount,此方法不太有用,即便在調(diào)試時(shí)也如此,所以筆者(與蘋果公司)并不推薦大家使用這個(gè)方法。

對(duì)象創(chuàng)建出來(lái)時(shí),其保留計(jì)數(shù)至少為1。若想令其繼續(xù)存活,則調(diào)用retain方法。要是某部分代碼不再使用此對(duì)象,不想令其繼續(xù)存活,那就調(diào)用release或autorelease方法。最終當(dāng)保留計(jì)數(shù)歸零時(shí),對(duì)象就回收了(deallocated),也就是說(shuō),系統(tǒng)會(huì)將其占用的內(nèi)存標(biāo)記為“可重用”(reuse)。此時(shí),所有指向該對(duì)象的引用也都變得無(wú)效了。

下圖演示了對(duì)象自創(chuàng)建出來(lái)之后歷經(jīng)一次“保留”及兩次“釋放”操作的過(guò)程。

在對(duì)象聲明周期中,其保留計(jì)數(shù)時(shí)而遞增,時(shí)而遞減,最終歸零

應(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ì)象圖”(object graph)。對(duì)象如果持有指向其他對(duì)象的強(qiáng)引用,那么前者就“擁有”后者。也就是說(shuō),對(duì)象想令其所引用的那些對(duì)象繼續(xù)存活,就可將其“保留”。等用完了之后,再釋放。

下圖中,ObjectB與ObjectC都引用了ObjectA。若ObjectB與ObjectC都不再使用ObjectA,則其保留計(jì)數(shù)降為0,于是便可摧毀了。還有其他對(duì)象想令ObjectB與ObjectC繼續(xù)存活,而應(yīng)用程序里又有另外一些對(duì)象想令那些對(duì)象繼續(xù)存活。如果按“引用樹(shù)”回溯,那么最終會(huì)發(fā)現(xiàn)一個(gè)“根對(duì)象”(root object)。在Mac OS X應(yīng)用程序中,此對(duì)象就是NSApplication對(duì)象;而在iOS應(yīng)用程序中,則是UIApplication對(duì)象。兩者都是應(yīng)用程序啟動(dòng)時(shí)創(chuàng)建的單例。

對(duì)象圖

下面這段代碼有助于理解這些方法的用法:

NSMutableArray *array = [[NSMutableArray alloc]init];
    
NSNumber *number = [[NSNumber alloc]initWithInt:111];
[array addObject:number];
[number release];
    
//do something with 'array'
    
[array release];

由于代碼中直接調(diào)用了release方法,所以在ARC下無(wú)法編譯。在Objective-C中,調(diào)用alloc方法所返回的對(duì)象由調(diào)用者所擁有。也就是說(shuō),調(diào)用者已通過(guò)alloc方法表達(dá)了想令該對(duì)象繼續(xù)存活下去的意愿。不過(guò)請(qǐng)注意,這并不是說(shuō)對(duì)象此時(shí)的保留計(jì)數(shù)必定是1。在alloc或“initWithInt:”方法的實(shí)現(xiàn)代碼中,也許還有其他對(duì)象也保留了此對(duì)象,所以,其保留計(jì)數(shù)可能會(huì)大于1。能夠肯定的是:保留計(jì)數(shù)至少為1。保留計(jì)數(shù)這個(gè)概念就應(yīng)該這樣理解才對(duì)。絕不應(yīng)該說(shuō)保留計(jì)數(shù)一定是某個(gè)值,只能說(shuō)所執(zhí)行的操作是遞增了該計(jì)數(shù)還是遞減了該計(jì)數(shù)。

創(chuàng)建完數(shù)組后,把number對(duì)象加入其中。調(diào)用數(shù)組的“addObject:”方法時(shí),數(shù)組也會(huì)在number上調(diào)用retain方法,以期繼續(xù)保留此對(duì)象。這時(shí),保留計(jì)數(shù)至少為2。接下來(lái),代碼不再需要number對(duì)象了,于是將其釋放?,F(xiàn)在的保留計(jì)數(shù)至少為1。這樣就不能照常使用number變量了。調(diào)用release之后,已經(jīng)無(wú)法保證所指的對(duì)象仍然存活。當(dāng)然,根據(jù)本例中的代碼,我們顯然知道number對(duì)象在調(diào)用了release之后仍然存活,因?yàn)閿?shù)組還在引用著它。然而絕不應(yīng)假設(shè)此對(duì)象一定存活,也就是說(shuō),不要像下面這樣編寫代碼:

NSNumber *number = [[NSNumber alloc]initWithInt:111];
[array addObject:number];
[number release];
NSLog(@"number=5@",number);

即便上述代碼在本例中可以正常執(zhí)行,也仍然不是個(gè)好辦法。如果調(diào)用release之后,基于某些原因,其保留計(jì)數(shù)降至0,那么number對(duì)象所占內(nèi)存也許會(huì)回收,這樣的話,再調(diào)用NSLog可能就將使應(yīng)用程序崩潰了。這里說(shuō)“可能”,是因?yàn)閷?duì)象所占的內(nèi)存在“解除分配”(deallocated)之后,只是放回“可用內(nèi)存池”(avaliable pool)。如果執(zhí)行NSLog時(shí)尚未覆寫對(duì)象內(nèi)存,那么該對(duì)象仍然有效,這時(shí)程序不會(huì)崩潰。由此可見(jiàn):因過(guò)早釋放對(duì)象而導(dǎo)致的bug很難調(diào)試。

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

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

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

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

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

此方法將保留新值并釋放舊值,然后更新實(shí)例變量,令其指向新值。順序很重要。假如還未保留新值就先把舊值釋放了,而且兩個(gè)值又指向同一個(gè)對(duì)象,那么,先執(zhí)行的release操作就可能導(dǎo)致系統(tǒng)將此對(duì)象永久回收。而后續(xù)的retain操作則無(wú)法令這個(gè)已經(jīng)徹底回收的對(duì)象復(fù)生,于是實(shí)例變量就成了懸掛指針。

3. 自動(dòng)釋放池

調(diào)用release會(huì)立刻遞減對(duì)象的保留計(jì)數(shù)(而且還有可能令系統(tǒng)回收此對(duì)象),然后有時(shí)候可以不調(diào)用它,改為調(diào)用autorelease,此方法會(huì)在稍后遞減計(jì)數(shù),通常是在下一次“事件循環(huán)”(event loop)時(shí)遞減,不過(guò)也可能執(zhí)行得更早些。

此特性很有用,尤其是在方法中返回對(duì)象時(shí)更應(yīng)該用它。在這種情況下,我們并不總是想令方法調(diào)用者手工保留其值。比方說(shuō),有下面這個(gè)方法:

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

此時(shí)返回的str對(duì)象其保留計(jì)數(shù)比期望值要多1(+1 retain count),因?yàn)檎{(diào)用alloc會(huì)令保留計(jì)數(shù)加1,而又沒(méi)有與之對(duì)應(yīng)的釋放操作。保留計(jì)數(shù)多1,就意味著調(diào)用者要負(fù)責(zé)處理多出來(lái)的這一次保留操作。必須設(shè)法將其抵消。這并不是說(shuō)保留計(jì)數(shù)本身就一定是1,它可能大于1,不過(guò)那取決于“initWithFromat:”方法內(nèi)的實(shí)現(xiàn)細(xì)節(jié)。你要考慮的是如何將多出來(lái)的這一次保留操作抵消掉。

但是,不能在方法內(nèi)釋放str,否則還沒(méi)等方法返回,系統(tǒng)就把該對(duì)象回收了。這里應(yīng)該用autorelease,它會(huì)在稍后釋放對(duì)象,從而給調(diào)用者留下了足夠長(zhǎng)的時(shí)間,使其可以在需要時(shí)先保留返回值。換句話說(shuō),此方法可以保證對(duì)象在跨越“方法調(diào)用邊界”(method call boundary)后一定存活。實(shí)際上,釋放操作會(huì)在清空最外層的自動(dòng)釋放池時(shí)執(zhí)行,除非你有自己的自動(dòng)釋放池,否則這個(gè)時(shí)機(jī)指的就是當(dāng)前線程的下一次事件循環(huán)。改寫stringValue方法,使用autorelease來(lái)釋放對(duì)象:

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

修改之后,stringValue方法把NSString對(duì)象返回給的調(diào)用者時(shí),此對(duì)象必然存活。所以我們能夠像下面這樣使用它:

NSString *str = [self stringValue];
NSLog(@“This string is: %@”,str);

由于返回的str對(duì)象將于稍后自動(dòng)釋放,所以多出來(lái)的那一次保留操作到時(shí)自然就會(huì)抵消,無(wú)須再執(zhí)行內(nèi)存管理操作。因?yàn)樽詣?dòng)釋放池中的釋放操作要等到下一次事件循環(huán)時(shí)才會(huì)執(zhí)行,所以NSLog語(yǔ)句在使用str對(duì)象前不需要手工執(zhí)行保留操作。但是,假如要持有此對(duì)象的話(比如將其設(shè)置給實(shí)例變量),那就需要保留,并于稍后釋放:

_instanceVariable = [[self stringValue]retain];
//…
[]

由此可見(jiàn),autorelease能延長(zhǎng)對(duì)象生命期,使其在跨越方法調(diào)用邊界后依然可以存活一段時(shí)間。

4. 循環(huán)引用

使用引用計(jì)數(shù)機(jī)制時(shí),經(jīng)常要注意的一個(gè)問(wèn)題就是“循環(huán)引用”(retain cycle),也就是呈環(huán)狀相互引用的多個(gè)對(duì)象。這將導(dǎo)致內(nèi)存泄露,因?yàn)檠h(huán)中的對(duì)象其保留計(jì)數(shù)不會(huì)降為0。對(duì)于循環(huán)中的每個(gè)對(duì)象來(lái)說(shuō),至少還有另外一個(gè)對(duì)象引用著它。下圖中的每個(gè)對(duì)象都引用了另外兩個(gè)對(duì)象之中的一個(gè)。在這個(gè)循環(huán)里,所有對(duì)象的保留計(jì)數(shù)都是1。

循環(huán)引用

在垃圾收集環(huán)境中,通常將這種情況認(rèn)定為“孤島”(island of isoland)。此時(shí),垃圾收集器會(huì)把三個(gè)對(duì)象全部回收走。而在Objective-C的引用計(jì)數(shù)架構(gòu)中,則享受不到這一便利。通常采用“弱引用”來(lái)解決此問(wèn)題,或是從外界命令循環(huán)中的某個(gè)對(duì)象不再保留另外一個(gè)對(duì)象。這兩種辦法都能打破循環(huán)引用,從而避免內(nèi)存泄露。

要點(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ù)

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

retain
release
autorelease
dealloc

實(shí)際上,ARC在調(diào)用這些方法事,并不通過(guò)普通的Objective-C消息派發(fā)機(jī)制,而是直接調(diào)用其底層C語(yǔ)言版本。這樣做性能更好,因?yàn)楸A艏搬尫挪僮餍枰l繁執(zhí)行,所以直接調(diào)用底層函數(shù)能節(jié)省很多CPU周期。比方說(shuō),ARC會(huì)調(diào)用與retain等價(jià)的底層函數(shù)objc_retain。這也是不能覆寫retain、release或autorelease的緣由,因?yàn)檫@些方法從來(lái)不會(huì)直接被調(diào)用。

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

將內(nèi)存管理語(yǔ)義在方法名中表示出來(lái)早已成為Objective-C的慣例,而ARC則將之確立為硬性規(guī)定。這些規(guī)則簡(jiǎn)單地體現(xiàn)在方法名上。若方法名以下列詞語(yǔ)開(kāi)頭,則其返回的對(duì)象歸調(diào)用者所有:
alloc
new
copy
mutableCopy

歸調(diào)用者所有的意思是:調(diào)用上述四種方法的那段代碼要負(fù)責(zé)釋放方法所返回的對(duì)象。

若方法名不以上述四個(gè)詞語(yǔ)開(kāi)頭,則表示其所返回的對(duì)象并不歸調(diào)用者所有。在這種情況下,返回的對(duì)象會(huì)自動(dòng)釋放,所以其值在跨越方法調(diào)用邊界后依然有效。要想使對(duì)象多存活一段時(shí)間,必須令調(diào)用者保留它才行。

維系這些規(guī)則所需的全部?jī)?nèi)存管理事宜均有ARC自動(dòng)處理,其中也包括在將其返回的對(duì)象上調(diào)用autorelease,下列代碼演示了ARC的用法:

+(EOCPerson *)newPerson{
    EOCPerson *person = [[EOCPerson alloc]init];
    return person;
    //這個(gè)方法用new開(kāi)頭的,不需要在返回的時(shí)候retain、release或autorelease
}

+(EOCPerson *)somePerson{
    EOCPerson *person = [[EOCPerson alloc]init];
    return person;
    //這個(gè)方法不是以擁有關(guān)系關(guān)鍵字開(kāi)頭的,所以ARC會(huì)自動(dòng)在返回的時(shí)候加上autorelease
}

-(void)doSomething{
    EOCPerson *personOne = [EOCPerson newPerson];
    EOCPerson *personTwo = [EOCPerson somePerson];
    //personOne是作為被這段代碼擁有的關(guān)系返回的,所以需要release,
    //personTwo不被這段代碼擁有,不需要release
}

除了會(huì)自動(dòng)調(diào)用“保留”與“釋放”方法外,使用ARC還可以執(zhí)行一些手工操作很難甚至無(wú)法完成的優(yōu)化。例如,在編譯器,ARC會(huì)把能夠互相抵消的retain、release、autorelease操作約簡(jiǎn)。如果發(fā)現(xiàn)在同一個(gè)對(duì)象上執(zhí)行多次“保留”與“釋放”操作,那么ARC有時(shí)可以成對(duì)地移除這兩個(gè)操作。

ARC可以在運(yùn)行期監(jiān)測(cè)到這一對(duì)多余的操作,也就是autorelease及緊跟其后的retain。為了優(yōu)化代碼,在方法中返回自動(dòng)釋放的對(duì)象時(shí),要執(zhí)行一個(gè)特殊函數(shù)。此時(shí)不直接調(diào)用對(duì)象的autorelease方法,而是改為調(diào)用objc_autoreleaseReturnValue。此函數(shù)會(huì)檢視當(dāng)前方法返回之后即將要執(zhí)行的那段代碼。若發(fā)現(xiàn)那段代碼在返回的對(duì)象上執(zhí)行retain操作,則設(shè)置全局?jǐn)?shù)據(jù)結(jié)構(gòu)(此數(shù)據(jù)結(jié)構(gòu)的具體內(nèi)容因處理器而已)中的一個(gè)標(biāo)志位而不執(zhí)行autorelease操作。與之相似,如果方法返回了一個(gè)自動(dòng)釋放的對(duì)象,而調(diào)用方法的代碼要保留此對(duì)象,那么此時(shí)不直接執(zhí)行retain,而是改為執(zhí)行objc_retainAutoreleaseReturnValue函數(shù)。此函數(shù)要檢測(cè)剛才提到的那個(gè)標(biāo)志位,若已經(jīng)置位,則不執(zhí)行retain操作。設(shè)置并檢測(cè)標(biāo)志位,要比調(diào)用autorelease和retain更快。

下面這段代碼演示了ARC是如何通過(guò)這些特殊函數(shù)來(lái)優(yōu)化程序的:

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

//Code using EOCPerson class
EOCPerson *tmp = [EOCPerson personWithName:@"Matt Galloway"];
_myPerson = objc_retainAutoreleaseReturnValue(tmp);

為了求得最佳效果,這些特殊函數(shù)的實(shí)現(xiàn)代碼都因處理器而異。下面這段偽代碼描述了其中的步驟:

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

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

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

ARC也會(huì)處理局部變量與實(shí)例變量的內(nèi)存管理。默認(rèn)情況下,每個(gè)變量都是指向?qū)ο蟮膹?qiáng)引用。

@interface EOCClass : NSObject
{
    id _object;
}

@implementation EOCClass
-(void)setup{
    _object = [EOCOtherClass new];
}
@end

在手動(dòng)管理引用計(jì)數(shù)時(shí),實(shí)例變量_object并不會(huì)自動(dòng)保留其值,而在ARC環(huán)境下則會(huì)這樣做。也就是說(shuō),若在ARC下編譯setup方法,則其代碼會(huì)變?yōu)椋?/p>

-(void)setup{
    id tmp = [EOCOtherClass new];
    _object = [tmp retain];
    [tmp release];
}

當(dāng)然,在此情況下,retain和release可以消去。所以,ARC會(huì)將這兩個(gè)操作化簡(jiǎn)掉,于是,實(shí)際執(zhí)行的代碼還是和原來(lái)一樣。不過(guò),在編寫設(shè)置方法時(shí),使用ARC會(huì)簡(jiǎn)單一些。如果不用ARC,那么需要像下面這樣來(lái)寫:

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

但是這樣寫會(huì)出問(wèn)題。加入新值和實(shí)例變量已有的值相同,如果只有當(dāng)前對(duì)象還在引用這個(gè)值,那么設(shè)置方法中的釋放操作會(huì)使該值的保留計(jì)數(shù)降為0,從而導(dǎo)致系統(tǒng)將其回收。接下來(lái)再執(zhí)行保留操作,就會(huì)令應(yīng)用程序崩潰。使用ARC之后,就不可能發(fā)生這種疏失了。在ARC環(huán)境下,與剛才等效的設(shè)置函數(shù)可以這么寫:

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

ARC會(huì)用一種安全的方式來(lái)設(shè)置:先保留新值,再釋放舊值,最后設(shè)置實(shí)例變量。

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

__stong:默認(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ì)象“按引用傳遞”給方法時(shí),使用這個(gè)特殊的修飾符。此值在方法返回時(shí)自動(dòng)釋放。

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

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

不論采用上面哪種寫法,在設(shè)置實(shí)例變量時(shí)都不會(huì)保留其值。
我們經(jīng)常用__weak局部變量來(lái)打破循環(huán)引用。

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

使用ARC之后,不需要再編寫像不使用ARC是的那種dealloc方法了,因?yàn)锳RC借用Objective-C++的一項(xiàng)特性來(lái)生成清理例程(cleanup routime)。回收Objective-C++對(duì)象時(shí),待回收的對(duì)象會(huì)調(diào)用所有C++對(duì)象的析構(gòu)函數(shù)(destructor)。編譯器如果發(fā)現(xiàn)某個(gè)對(duì)象里含有C++對(duì)象,就會(huì)生成名為.cxx_destruct的方法。而ARC則借助此特性,在該方法中生成清理內(nèi)存所需的代碼。

不過(guò),如果有非Objective-C的對(duì)象,比如CoreFoundation中的對(duì)象或是由malloc()分配在堆中的內(nèi)存,那么仍然需要清理。然而不需要像原來(lá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);
}

因?yàn)锳RC會(huì)自動(dòng)生成回收對(duì)象時(shí)所執(zhí)行的代碼,所以通常無(wú)須再編寫dealloc方法。

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

不使用ARC時(shí),可以覆寫內(nèi)存管理方法。但是在ARC環(huán)境下不能這么做,因?yàn)闀?huì)干擾到ARC分析對(duì)象生命期的工作。

要點(diǎn):

  • 有ARC之后,程序員就無(wú)須擔(dān)心內(nèi)存管理問(wèn)題了。使用ARC來(lái)編程,可省去類中的許多“樣板代碼”。
  • 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é)管理Objective-C對(duì)象的內(nèi)存。尤其要注意:CoreFoundation對(duì)象不歸ARC管理,開(kāi)發(fā)者必須實(shí)時(shí)調(diào)用CFRetain/CFRelease。
  • 不要在屬性名前面加上alloc、new、copy或mutableCopy,否則編譯器會(huì)報(bào)錯(cuò)(Property follows Cocoa naming for returning ‘owned’objects)可用此方法解決,但是強(qiáng)烈不建議這么用

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

對(duì)象在經(jīng)歷其生命期后,最終會(huì)為系統(tǒng)所回收,這時(shí)就要執(zhí)行dealloc方法了,然后具體何時(shí)執(zhí)行,則無(wú)法保證。不應(yīng)該自己調(diào)用dealloc方法,運(yùn)行期會(huì)在適當(dāng)?shù)纳钫{(diào)用它。

在dealloc方法中主要是要釋放對(duì)象所擁有的引用,也就是說(shuō)把所有Objective-C對(duì)象都釋放掉,ARC會(huì)通過(guò)自動(dòng)生成的.cxx_destruct方法,在dealloc中為你自動(dòng)添加這些釋放代碼。對(duì)象所擁有的其他非Objective-C對(duì)象也要釋放。比如CoreFoundation的對(duì)象就必須手工釋放,因?yàn)樗鼈兪怯杉僀的API所生成的。

在dealloc方法中,通常還要把原來(lái)配置過(guò)的觀測(cè)行為都清理掉。如果用NSNotificationCenter給此對(duì)象注冊(cè)過(guò)某種通知,那么一般應(yīng)該在這里注銷,這樣的話,通知系統(tǒng)就不再把通知發(fā)給回收后的對(duì)象了。

dealloc方法可以這樣來(lái)寫:

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

如果是手動(dòng)管理引用計(jì)數(shù)的話,最后還要調(diào)用”[super dealloc]”,ARC會(huì)自動(dòng)執(zhí)行此操作。手動(dòng)管理還要將當(dāng)前對(duì)象所擁有的全部Objective-C對(duì)象逐個(gè)釋放。

雖說(shuō)應(yīng)該于dealloc中釋放引用,但是開(kāi)銷較大或系統(tǒng)內(nèi)稀缺的資源則不在此列。像是文件描述符(file descriptor)、套接字(socket)、大塊內(nèi)存等,都屬于這種資源。不能指望dealloc方法必定會(huì)在某個(gè)特定的時(shí)機(jī)調(diào)用,因?yàn)橛幸恍o(wú)法預(yù)料的東西可能也持有此對(duì)象。通常的做法是,實(shí)現(xiàn)另外一個(gè)方法,當(dāng)應(yīng)用程序用完資源對(duì)象后,就調(diào)用此方法。

比方說(shuō),如果某個(gè)對(duì)象管理著連接服務(wù)器所用的套接字,那么也許就需要這種“清理方法”。此對(duì)象可能要通過(guò)套接字連接到數(shù)據(jù)庫(kù)。

對(duì)于對(duì)象所屬的類,其接口可以這樣寫:

#import <Foundation/Foundation.h>

@interface EOCServerConnection : NSObject
-(void)open:(NSString *)address;
-(void)close;
@end

該類與開(kāi)發(fā)者之間的約定是:想打開(kāi)連接,就調(diào)用“open:”方法;連接使用完畢,就調(diào)用close方法?!瓣P(guān)閉”操作必須在系統(tǒng)把連接對(duì)象回收之前調(diào)用,否則就是編程錯(cuò)誤,這與通過(guò)“保留”與“釋放”操作來(lái)平衡引用計(jì)數(shù)是類似的。

在清理方法而非dealloc方法中清理資源還有個(gè)原因,就是系統(tǒng)并不保證每個(gè)創(chuàng)建出來(lái)的對(duì)象的dealloc都會(huì)執(zhí)行。極個(gè)別情況下,當(dāng)應(yīng)用程序終止時(shí),仍有對(duì)象處于存活狀態(tài),這些對(duì)象沒(méi)有收到dealloc消息。由于應(yīng)用程序終止之后,其占用資源也會(huì)返還給操作系統(tǒng),所以實(shí)際上這些對(duì)象也就等于是消亡了。不調(diào)用dealloc方法是為了優(yōu)化程序效率。在Mac OS X及iOS應(yīng)用程序所對(duì)應(yīng)的application delegate中,都含有一個(gè)會(huì)于程序終止時(shí)調(diào)用的方法。如果一定要清理某些對(duì)象,那么可在此方法中調(diào)用那些對(duì)象的“清理方法”。

在Mac OS X系統(tǒng)里,應(yīng)用程序終止時(shí)會(huì)調(diào)用NSApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(NSNotification *)notification

而在iOS系統(tǒng)里,應(yīng)用程序終止時(shí)則會(huì)調(diào)用UIApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(UIApplication *)application

如果對(duì)象管理著某些資源,那么在dealloc中也要調(diào)用“清理方法”,以防開(kāi)發(fā)者忘了清理這些資源。下面舉例說(shuō)明close與dealloc方法應(yīng)如何寫:

-(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)用其他方法。無(wú)論在這里調(diào)用什么方法都不應(yīng)該,因?yàn)閷?duì)象此時(shí)“已近尾聲”。如果在這里所調(diào)用的方法又要異步執(zhí)行某些任務(wù),或是又要繼續(xù)調(diào)用它們自己的某些方法,那么等到那些任務(wù)執(zhí)行完畢時(shí),系統(tǒng)已經(jīng)把當(dāng)前這個(gè)待回收的對(duì)象徹底摧毀了。

調(diào)用dealloc方法的那個(gè)線程會(huì)執(zhí)行“最終的釋放操作”,令對(duì)象的保留計(jì)數(shù)降為0,而某些方法必須在特定的線程里(比方說(shuō)主線程里)調(diào)用才行。若在dealloc里調(diào)用了那些方法,則無(wú)法保證當(dāng)前這個(gè)線程就是那些方法所需的線程。通過(guò)編寫常規(guī)代碼的方式,無(wú)論如何都沒(méi)辦法保證其會(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ì)覆寫這些方法,并于其中做一些無(wú)法在回收階段安全執(zhí)行的操作。此外,屬性可能正處于“鍵值觀測(cè)”機(jī)制的監(jiān)控之下,該屬性的觀察者可能會(huì)在屬性改變時(shí)“保留”或使用這個(gè)即將回收的對(duì)象。這種做法會(huì)令運(yùn)行期系統(tǒng)的狀態(tài)完全失調(diào),從而導(dǎo)致一些莫名其妙的錯(cuò)誤。

要點(diǎn):

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

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

Objective-C的錯(cuò)誤模型表明,異常只應(yīng)發(fā)生嚴(yán)重錯(cuò)誤后拋出,雖說(shuō)如此,不過(guò)有時(shí)仍然需要編寫代碼來(lái)捕獲并處理異常。比如使用Objective-C++來(lái)編碼時(shí),或是編碼中用到了第三方程序庫(kù)而此程序庫(kù)所判處的異常又不受你控制時(shí),就需要捕獲及處理異常了。
在try塊中,如果先保留了某個(gè)對(duì)象,然后在釋放它之前又拋出了異常,那么,除非catch塊能處理此問(wèn)題,否則對(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 (NSException *exception) {
        NSLog(@"Whoopse,there was an error.Oh well...");
    }

如果doSomethingThatMayThrow拋出異常,由于異常會(huì)令執(zhí)行過(guò)程終止并跳轉(zhuǎn)至catch塊,因而其后的那行release代碼不會(huì)運(yùn)行。在這種情況下,如果代碼拋出異常,那么對(duì)象就泄露了。解決辦法是使用@finally塊,無(wú)論是否拋出異常,其中的代碼都保證會(huì)運(yùn)行,且只運(yùn)行一次。代碼可改寫如下:

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

由于@finally塊也要引用object對(duì)象,所以必須把它從@try塊里移到外面去。要是所有對(duì)象都得如此釋放,那這樣做就會(huì)非常乏味。而且@try塊中的邏輯更為復(fù)雜,含有多條語(yǔ)句,那么很容易就會(huì)因?yàn)橥浤硞€(gè)對(duì)象而導(dǎo)致泄露。若泄露的對(duì)象是文件描述符或數(shù)據(jù)庫(kù)連接等稀缺資源(或是這些稀缺資源的管理者),則可能引發(fā)大問(wèn)題,因?yàn)檫@樣導(dǎo)致應(yīng)用程序把所有系統(tǒng)資源都抓在自己手里而不及時(shí)釋放。

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

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

現(xiàn)在問(wèn)題更大了:由于不能調(diào)用release,所以無(wú)法像手動(dòng)管理引用計(jì)數(shù)那樣把釋放操作移到@finally塊中。你可能認(rèn)為這種狀況ARC自然會(huì)處理的。但實(shí)際上ARC不會(huì)自動(dòng)處理,因?yàn)檫@樣做需要加入大量樣板代碼,以便跟蹤待清理的對(duì)象,從而在拋出異常時(shí)將其釋放??墒?,這段代碼會(huì)嚴(yán)重影響運(yùn)行期的性能,即便在不拋異常時(shí)也如此。而且,添加進(jìn)來(lái)的額外代碼還會(huì)明顯增加應(yīng)用程序的大小。這些副作用都不甚理想。

雖說(shuō)默認(rèn)狀況下未開(kāi)啟,但ARC依然能生成這種安全處理異常所用的附加代碼。-fobjc-arc-exception這個(gè)編譯器標(biāo)志用來(lái)開(kāi)啟此功能。其默認(rèn)不開(kāi)啟的原因是:Objective-C代碼中,只有當(dāng)應(yīng)用程序必須因異常狀況而終止時(shí)才拋出異常。因此,如果應(yīng)用程序即將終止,那么是否還會(huì)發(fā)生內(nèi)存泄露就已經(jīng)無(wú)關(guān)緊要了。在應(yīng)用程序必須立即終止的情況下,還去添加安全處理異常所用的附加代碼是沒(méi)有意義的。

有種情況編譯器會(huì)自動(dòng)把-fobjc-arc-exception標(biāo)志打開(kāi),就是出于Objective-C++模式時(shí)。因?yàn)镃++處理異常所用的代碼與ARC實(shí)現(xiàn)的附加代碼類似,所以令A(yù)RC加入自己的代碼以安全處理異常,其性能損失并不太大。此外,由于C++頻繁使用異常,所以O(shè)bjective-C++程序員很可能也會(huì)使用異常。

如果手工管理引用計(jì)數(shù),而且必須捕獲異常,那么要設(shè)法保證所編代碼能把對(duì)象正確清理干凈。若使用ARC且必須捕獲異常,則需打開(kāi)編譯器的-fobjc-arc-exception標(biāo)志。但最重要的是:在發(fā)現(xiàn)大量異常捕獲操作時(shí),應(yīng)考慮重構(gòu)代碼,用第21條所講的NSError式錯(cuò)誤信息傳遞法來(lái)取代異常。

要點(diǎn):

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

33.用弱引用避免循環(huán)引用

對(duì)象圖里經(jīng)常會(huì)出現(xiàn)一種情況,就是幾個(gè)對(duì)象都以某種方式互相引用,從而形成”環(huán)“。由于Objective-C內(nèi)存管理模型使用引用計(jì)數(shù)架構(gòu),所以這種情況通常會(huì)泄露內(nèi)存,因?yàn)樽詈鬀](méi)有別的東西會(huì)引用環(huán)中的對(duì)象。這樣的話,環(huán)里的對(duì)象就無(wú)法為外界所訪問(wèn)了,但對(duì)象之間尚有引用,這些引用使得他們都能繼續(xù)存活下去,而不會(huì)為系統(tǒng)所回收。

最簡(jiǎn)單的循環(huán)引用由兩個(gè)對(duì)象構(gòu)成,他們互相引用對(duì)方。如圖:

這種循環(huán)引用的產(chǎn)生原因不難理解,且很容易就能通過(guò)查看代碼而偵測(cè)出來(lái):

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,strong)EOCClassA *other;
@end

如果把EOCClassA實(shí)例的other屬性設(shè)置成某個(gè)EOCClassB實(shí)例,而把那個(gè)EOCClassB實(shí)例的other屬性又設(shè)置成這個(gè)EOCClassA實(shí)例,那么就會(huì)出現(xiàn)下圖的循環(huán)引用:

循環(huán)引用會(huì)導(dǎo)致內(nèi)存泄露。如果只剩下一個(gè)引用還指向循環(huán)引用中的實(shí)例,而現(xiàn)在又把這個(gè)引用移除,那么整個(gè)循環(huán)引用就泄露了。也就是說(shuō),沒(méi)辦法再訪問(wèn)其中的對(duì)象了。

避免循環(huán)引用的最佳方式就是弱引用。這種引用經(jīng)常用來(lái)表示“非擁有關(guān)系”。將屬性聲明為unsafe_unretained即可。修改剛才那段代碼,將其屬性聲明如下:

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,unsafe_unretained)EOCClassA *other;

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

用unsafe_unretained修飾的屬性特質(zhì),其語(yǔ)義同assign特質(zhì)等價(jià),然而assign通常只用于數(shù)值類型,unsafe_unretained則多用于對(duì)象類型。這個(gè)詞本身就表明其所修飾的屬性可能無(wú)法安全使用。

Objective-C中還有一項(xiàng)與ARC相伴的運(yùn)行期特性,可能令開(kāi)發(fā)者安全使用弱引用:這就是weak屬性特質(zhì),它與unsafe_unretained的作用完全相同。然而,只要系統(tǒng)把屬性回收,屬性值就會(huì)自動(dòng)設(shè)置為nil。在剛才那段代碼中,EOCClassB的other屬性可修改如下:

@property(nonatomic,weak)EOCClassA *other;

下圖演示了unsafe_unretained與weak屬性的區(qū)別:

當(dāng)指向EOCClassA實(shí)例的引用移除后,unsafe_unretained屬性仍然指向那個(gè)已經(jīng)回收的實(shí)例,而weak屬性則指向nil。

使用weak屬性而非unsafe_unretained引用可以令代碼更安全。應(yīng)用程序也許會(huì)顯示出錯(cuò)誤的數(shù)據(jù),但不直接崩潰。

一般來(lái)說(shuō),如果不擁有某對(duì)象,那就不要保留它。這條規(guī)則對(duì)collection例外。collection雖然并不直接擁有其內(nèi)容,但是它要代表自己所屬的那個(gè)對(duì)象來(lái)保留這些元素。有時(shí),對(duì)象中的引用會(huì)指向另外一個(gè)并不歸自己所擁有的對(duì)象,比如Delegate模式就是這樣。

要點(diǎn):

  • 將某些引用設(shè)為weak,可避免出現(xiàn)“循環(huán)引用”。
  • weak引用可以自動(dòng)清空,也可以不自動(dòng)清空。自動(dòng)清空是隨著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)存峰值

Objective-C對(duì)象的生命期取決于其引用計(jì)數(shù)。在Objective-C的引用計(jì)數(shù)架構(gòu)中,有一項(xiàng)特性叫做“自動(dòng)釋放池”(autorelease pool)。釋放對(duì)象有兩種方式:一種是調(diào)用release方法,使其保留計(jì)數(shù)立即遞減;另一種是調(diào)用autorelease方法,將其加入“自動(dòng)釋放池”中。自動(dòng)釋放池用于存放那些需要在稍后某個(gè)時(shí)刻釋放的對(duì)象。清空自動(dòng)釋放池時(shí),系統(tǒng)會(huì)向其中的對(duì)象發(fā)送release消息。

創(chuàng)建自動(dòng)釋放池所用語(yǔ)法如下:

@autoreleasepool {
        //...
    }

一般情況下無(wú)須擔(dān)心自動(dòng)釋放池的創(chuàng)建問(wèn)題。Mac OS X與iOS應(yīng)用程序分別運(yùn)行于Cocoa及Cocoa Touch環(huán)境中。系統(tǒng)會(huì)自動(dòng)創(chuàng)建一些線程,比方說(shuō)主線程或是GCD機(jī)制中的線程,這些線程默認(rèn)都有自動(dòng)釋放池,每次執(zhí)行“事件循環(huán)“(event loop)時(shí),就會(huì)將其清空。因此,不需要自己來(lái)創(chuàng)建”自動(dòng)釋放池塊“。通常只有一個(gè)地方需要?jiǎng)?chuàng)建自動(dòng)釋放池塊,那就是在mian函數(shù)里,我們用自動(dòng)釋放池來(lái)包裹應(yīng)用程序的主入口點(diǎn)。比方說(shuō),iOS程序的main函數(shù)經(jīng)常這樣寫:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

從技術(shù)角度看,不是非得有個(gè)”自動(dòng)釋放池塊”才行。因?yàn)閴K的末尾恰好就是應(yīng)用程序的終止處,而此時(shí)操作系統(tǒng)會(huì)把程序所占的全部?jī)?nèi)存都釋放掉。雖說(shuō)如此,但是如果不寫這個(gè)塊的話,那么由UIApplicationMain函數(shù)所自動(dòng)釋放的那些對(duì)象,就沒(méi)有自動(dòng)釋放池可以容納了,于是系統(tǒng)會(huì)發(fā)出警告信息來(lái)表明這一情況。所以說(shuō),這個(gè)池可以理解成最外圍捕捉全部自動(dòng)釋放對(duì)象所用的池。

位于自動(dòng)釋放池范圍內(nèi)的對(duì)象,將在此范圍末尾處收到release消息。自動(dòng)釋放池可以嵌套。系統(tǒng)在自動(dòng)釋放對(duì)象時(shí),會(huì)把它放到最內(nèi)層的池里。比方說(shuō):

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

在本例中有兩個(gè)對(duì)象,它們都由類的工廠方法所創(chuàng)建,這樣創(chuàng)建出來(lái)的對(duì)象會(huì)自動(dòng)釋放。NSString對(duì)象放在外圍的自動(dòng)釋放池中,而NSNumber對(duì)象則放在里層的自動(dòng)釋放池中。將自動(dòng)釋放池嵌套用的好處是,可以借此控制應(yīng)用程序的內(nèi)存峰值,使其不致過(guò)高。

考慮下面這段代碼:

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

如果“doSomethingWithInt:“方法要?jiǎng)?chuàng)建臨時(shí)對(duì)象,那么這些對(duì)象很可能會(huì)放在自動(dòng)釋放池里。比方說(shuō),它們可能是一些臨時(shí)字符串。但是,即便這些對(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)建出來(lái),并加入自動(dòng)釋放池中。所有這種對(duì)象都要等f(wàn)or循環(huán)執(zhí)行完才會(huì)釋放。這樣一來(lái),在執(zhí)行for循環(huán)時(shí),應(yīng)用程序所占內(nèi)存就會(huì)持續(xù)上漲,而等到所有臨時(shí)對(duì)象都釋放后,內(nèi)存用量又會(huì)突然下降。

這種情況不甚理想,尤其當(dāng)循環(huán)長(zhǎng)度無(wú)法預(yù)知,必須取決于用戶輸入時(shí)更是如此。比方說(shuō),要從數(shù)據(jù)庫(kù)中讀取許多對(duì)象。代碼可能會(huì)這么寫:

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

EOCPerson的初始化函數(shù)也許會(huì)像上例那樣,再創(chuàng)建出一些臨時(shí)對(duì)象。若記錄有很多條,則內(nèi)存中也會(huì)有很多不必要的臨時(shí)對(duì)象,它們本來(lái)應(yīng)該提早回收的。增加一個(gè)自動(dòng)釋放池即可解決此問(wèn)題。如果把循環(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];
        }
    }

加上這個(gè)自動(dòng)釋放池之后,應(yīng)用程序在執(zhí)行循環(huán)時(shí)的內(nèi)存峰值就會(huì)降低,不再像原來(lái)那么高了。內(nèi)存峰值是指應(yīng)用程序在某個(gè)特定時(shí)段內(nèi)的最大內(nèi)存用量。新增的自動(dòng)釋放池塊可以減少這個(gè)峰值,因?yàn)橄到y(tǒng)會(huì)在塊的末尾把某些對(duì)象回收掉。而剛才提到的那種臨時(shí)對(duì)象,就在回收之列。

自動(dòng)釋放池機(jī)制就像”?!耙粯?。系統(tǒng)創(chuàng)建好自動(dòng)釋放池之后,就將其推入棧中,而清空自動(dòng)釋放池,則相當(dāng)于將其從棧中彈出。在對(duì)象上執(zhí)行自動(dòng)釋放操作,就等于將其放入棧頂?shù)哪莻€(gè)池里。

是否應(yīng)該用池來(lái)優(yōu)化效率,完全取決于具體的應(yīng)用程序。首先得監(jiān)控內(nèi)存用量,判斷其中有沒(méi)有需要解決的問(wèn)題,如果沒(méi)完成這一步,那就別急著優(yōu)化。盡管自動(dòng)釋放池塊的開(kāi)銷不太大,但畢竟還是有的,所以盡量不要建立額外的自動(dòng)釋放池。

如果在ARC出現(xiàn)之前就寫過(guò)Objective-C程序,那么可能還記得有種老式寫法,就是使用NSAutoreleasePool對(duì)象。這個(gè)特殊的對(duì)象與普通對(duì)象不同,它專門用來(lái)表示自動(dòng)釋放池。就像新語(yǔ)法中的自動(dòng)釋放池塊一樣。但是這種寫法并不會(huì)在每次執(zhí)行for循環(huán)時(shí)都清空池,此對(duì)象更為”重量級(jí)“,通常用來(lái)創(chuàng)建那種偶爾需要清空的池,比方說(shuō):

NSArray *databaseRecords = /*...*/;
    NSMutableArray *people = [NSMutableArray new];
    int i = 0;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
    for(NSDictionary *record in databaseRecords){
        EOCPerson *person = [[EOCPerson alloc]initWithRecord:record];
        [people addObject:person];
        //Drain the pool only every 10 circles
        if(++i == 10){
            [pool drain];
            i = 0;
        }
        //Also drain at the end in case the loop is not a multiple of 10
       [pool drain]; 
    }

現(xiàn)在不需要再這樣寫代碼了。采用隨著ARC所引入的新語(yǔ)法,可以創(chuàng)建出更為”輕量級(jí)“的自動(dòng)釋放池。原來(lái)縮寫的代碼可能會(huì)每執(zhí)行n次循環(huán)清空一次自動(dòng)釋放池,現(xiàn)在可以改用自動(dòng)釋放池塊把for循環(huán)中的語(yǔ)句包起來(lái),這樣的話,每次執(zhí)行循環(huán)時(shí)都會(huì)簡(jiǎn)歷并清空自動(dòng)釋放池。

@autoreleasepool語(yǔ)法還有個(gè)好處:每個(gè)自動(dòng)釋放池均有其范圍,可以避免無(wú)意間誤用了那些在清空池后已為系統(tǒng)所回收的對(duì)象。比方說(shuō),考慮下面這段采用舊式寫法的代碼:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id object = [self createObject];
[pool drain];
[self useObject:object];

調(diào)用”userObject:“方法時(shí)所傳入的那個(gè)對(duì)象,可能已經(jīng)為系統(tǒng)所回收了。同樣的代碼改用信使寫法就變成了:

@autoreleasepool {
        id object = [self createObject];
    }
[self useObject:object];

這次根本就無(wú)法編譯,因?yàn)閛bject變量出了自動(dòng)釋放池塊的外圍就不可用了,所以在調(diào)用”useObject:“方法時(shí)不能用它做參數(shù)。

要點(diǎn):

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

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

向業(yè)已回收的對(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)答,也許不能。

Cocoa提供了“僵尸對(duì)象”(Zombie Object)這個(gè)非常方便的功能。啟用這項(xiàng)調(diào)試功能之后,運(yùn)行期系統(tǒng)會(huì)把所有已經(jīng)回收的實(shí)例轉(zhuǎn)化成特殊的“僵尸對(duì)象”,而不會(huì)真正回收它們。這種對(duì)象所在的核心內(nèi)存無(wú)法重用,因此不可能遭到覆寫。僵尸對(duì)象收到消息后,會(huì)拋出異常,其中準(zhǔn)確說(shuō)明了發(fā)送過(guò)來(lái)的消息,并描述了回收之前的那個(gè)對(duì)象。僵尸對(duì)象是調(diào)試內(nèi)存管理問(wèn)題的最佳方式。

將NSZombieEnabled環(huán)境變量設(shè)為YES,即可開(kāi)啟此功能。在Mac OS X系統(tǒng)中用bash運(yùn)行應(yīng)用程序時(shí),可以這么做:

export NSZombieEnabled=“YES”
./app

給僵尸對(duì)象發(fā)送消息后,控制臺(tái)會(huì)打印消息,而應(yīng)用程序則會(huì)終止。打印出來(lái)的消息就像這樣:

*** -[CFString respondsToSelector:]:message sent to deallocated instance 0x7ff9e9c080e0

在Xcode中開(kāi)啟方法為:編輯應(yīng)用程序的Scheme,在對(duì)話框左側(cè)選擇”Run“,然后切換至”Diagnostics“分頁(yè),最后勾選”Enable Zombie Objects“選項(xiàng)。

僵尸對(duì)象的工作原理是什么呢?它的實(shí)現(xiàn)代碼深植與Objective-C的運(yùn)行期程序庫(kù)、Foundation框架及CoreFoundation框架中。系統(tǒng)在即將回收對(duì)象時(shí),如果發(fā)現(xiàn)通過(guò)環(huán)境變量啟用了僵尸對(duì)象功能,那么還將執(zhí)行一個(gè)附加步驟。這一步就是把對(duì)象轉(zhuǎn)化為僵尸對(duì)象,而不徹底回收。

下面代碼有助于理解這一步所執(zhí)行的操作:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface EOCClass : NSObject

@end


#import "EOCClass.h"

@implementation EOCClass

@end

void PrintClassInfo(id obj){
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===",class_getName(cls),class_getName(superCls));
}

int main(int argc, char *argv[]){
    EOCClass *obj = [[EOCClass alloc]init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);
    
    [obj release];
    NSLog(@"After release:");
    PrintClassInfo(obj);
}

本例代碼中有個(gè)函數(shù),可以根據(jù)給定的對(duì)象打印出所屬的類及其超類名稱。此函數(shù)沒(méi)有直接給對(duì)象發(fā)送Objective-C的class消息,而是調(diào)用了運(yùn)行期庫(kù)里的object_getClass()函數(shù)。因?yàn)槿绻麉?shù)已經(jīng)是僵尸對(duì)象了,那么給其發(fā)送Objective-C消息后,控制臺(tái)會(huì)打印錯(cuò)誤消息,而且應(yīng)用程序會(huì)崩潰。本例代碼將輸出下面這種消息:

Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ====

對(duì)象所屬的類已由EOCClass變成_NSZombie_EOCClass。_NSZombie_EOCClass實(shí)際上是在運(yùn)行期生成的,當(dāng)首次碰到EOCClass類的對(duì)象要變成僵尸對(duì)象時(shí),就會(huì)創(chuàng)建這么一個(gè)類。創(chuàng)建過(guò)程中用到了運(yùn)行期程序庫(kù)里的函數(shù),它們的功能很強(qiáng)大,可以操作類列表。

僵尸類是從名為NSZombie的模板類里復(fù)制出來(lái)的。這些僵尸類沒(méi)有多少事情可做,只是充當(dāng)一個(gè)標(biāo)記。接下類介紹它們是怎樣充當(dāng)標(biāo)記的。首先來(lái)看下嘛這段偽代碼,其中演示了系統(tǒng)如何根據(jù)需要?jiǎng)?chuàng)建出僵尸類,而僵尸類又如何把待回收的對(duì)象轉(zhuǎn)化成僵尸對(duì)象。

//Obtain the class of the object being deallocated
    Class cls = object_getClass(slef);
    
    //Get the class's name
    const char *clsName =  class_getName(cls);
    
    //Prepend _NSZombie_ to the class name
    const char *zombieClsName = "_NSZombie_" + clsName;
    
    //See if the specific zombie class exists
    Class zombieCls = objc_lookUpClass(zombieClsName);
    
    //If the specific zombie class doesn't exist
    //then it needs to be created
    if(!zombieCls){
        //Obtain the template zombie class called _NSZombie_
        Class baseZombieCls = objc_lookUpClass("_NSZombie_");
        
        //Duplicate the base zombie class,where the new class's
        //name is the prepended string from above
        zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
    }
    
    //Perform normal destruction of the object being deallocated
    objc_destructInstance(self);
    
    //Set the class of the object being deallocated
    //to the zombie class
    objc_setClass(self,zombieCls);
    
    //The class of 'self' is now _NSZombie_OrignalClass

這個(gè)過(guò)程其實(shí)就是NSObject的dealloc方法所做的事。運(yùn)行期系統(tǒng)如果發(fā)現(xiàn)NSZombieEnabled環(huán)境變量已設(shè)置,那么就把dealloc方法”調(diào)配“(swizzle)成一個(gè)會(huì)執(zhí)行上述代碼的版本。執(zhí)行到程序末尾時(shí),對(duì)象所屬的類已經(jīng)變?yōu)開(kāi)NSZombie_OriginalClass了,其中OriginalClass指的是原類名。

代碼中的關(guān)鍵之處在于:對(duì)象所占內(nèi)存沒(méi)有(通過(guò)調(diào)用free()方法)釋放,因此,這塊內(nèi)存不可復(fù)用。雖說(shuō)內(nèi)存泄露了,但這只是個(gè)調(diào)試手段,制作正式發(fā)行的應(yīng)用程序時(shí)不會(huì)把這項(xiàng)功能打開(kāi),所以這種泄露問(wèn)題無(wú)關(guān)緊要。

但是,系統(tǒng)為何要給每個(gè)變?yōu)榻┦念惗紕?chuàng)建一個(gè)對(duì)應(yīng)的新類呢?這是因?yàn)?,給僵尸對(duì)象發(fā)消息后,系統(tǒng)可由此知道該對(duì)象原來(lái)所屬的類。假如把所有僵尸對(duì)象都?xì)w到NSZombie類里,那原來(lái)的類名就丟了。創(chuàng)建新類的工作由運(yùn)行期函數(shù)objc_duplicateClass()來(lái)完成,它會(huì)把整個(gè)NSZombie類結(jié)構(gòu)拷貝一份,并賦予其新的名字。副本類的超類、實(shí)例變量及方法都和復(fù)制前相同。還有種做法也能保留舊類名,那就是不拷貝NSZombie,而是創(chuàng)建繼承自NSZombie的新類,但是用相應(yīng)的函數(shù)完成此功能,其效率不如直接拷貝高。

僵尸類的作用會(huì)在消息轉(zhuǎn)發(fā)例程(第12條)中體現(xiàn)出來(lái)。NSZombie類(以及所有從該類拷貝出來(lái)的類)并未實(shí)現(xiàn)任何方法。此類沒(méi)有超類,因此和NSObject一樣,也是個(gè)”根類“,該類只有一個(gè)實(shí)例變量,叫做isa,所有Objective-C的根類都必須有此變量。由于這個(gè)輕量級(jí)的類沒(méi)有實(shí)現(xiàn)任何方法,所以發(fā)給它的全部消息都要經(jīng)過(guò)”完整的消息轉(zhuǎn)發(fā)機(jī)制“。

在完整的消息轉(zhuǎn)發(fā)機(jī)制中,forwarding是核心,調(diào)試程序時(shí),大家可能在?;厮菹⒗锟匆?jiàn)過(guò)這個(gè)函數(shù)。它首先要做的事情就包含檢查接收消息的對(duì)象所屬的類名。若名稱前綴為NSZombie,則表明消息接收者是僵尸對(duì)象,需要特殊處理。此時(shí)會(huì)打印一條消息,其中指明了僵尸對(duì)象所收到的消息及原來(lái)所屬的類,然后應(yīng)用程序就終止了。在僵尸類名中嵌入原來(lái)類名的好處,這時(shí)就可以看出來(lái)了。只要把NSZombie從僵尸類名的開(kāi)頭拿掉,剩下的就是原始類名。下面?zhèn)未a演示了這一過(guò)程:

//Obtain the class of the object being deallocated
    Class cls = object_getClass(slef);
    
    //Get the class's name
    const char *clsName =  class_getName(cls);
    
    //Check if the class is prefixed with _NSZombie_
    if(string_has_prefix(clsName,"_NSZombie_"){
        //If so ,this object is a zombie
        
        //Get the original class name by skipping past the
        //_NSZombie_,i.e taking the substring from character 10
        const char *originalClsName = substring_from(clsName,10);
        
        //Get the selector name of the message
        const char *selectorName = sel_getName(_cmd);
        
        //Log a message to indicate wich selector is
        //being sent to which zombie
        Log("*** -[%s %s]:message sent to deallocated instance %p",originalClsName,selectorName,self);
        
        //Kill the application
        abort();
    }

要點(diǎn):

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

36.不要使用retainCount

NSObject協(xié)議中定義了下列方法,用于查詢對(duì)象當(dāng)前的保留計(jì)數(shù):

- (NSUInteger)retainCount

這個(gè)方法看上去似乎挺合理、挺有用的。它畢竟返回了保留計(jì)數(shù),而此值對(duì)每個(gè)對(duì)象來(lái)說(shuō)顯然都很重要。但問(wèn)題在于,保留計(jì)數(shù)的絕對(duì)數(shù)值一般都與開(kāi)發(fā)者所應(yīng)留意的事情完全無(wú)關(guān)。即便只在調(diào)試時(shí)才調(diào)用此方法,通常也還是無(wú)所助益的。

此方法之所以無(wú)用,其首要原因在于:它所返回的保留計(jì)數(shù)只是某個(gè)給定時(shí)間點(diǎn)上的值。該方法并未考慮到系統(tǒng)會(huì)稍后把自動(dòng)釋放池清空,因而不會(huì)將后續(xù)的釋放操作從返回值里減去,這樣的話,此值就未必能真實(shí)反映實(shí)際的保留計(jì)數(shù)了。因此,下面這種寫法非常糟糕:

while([]){
[object release]
}

這種寫法的第一個(gè)錯(cuò)誤是:它沒(méi)考慮到后續(xù)的自動(dòng)釋放操作,只是不停地通過(guò)釋放操作來(lái)降低保留計(jì)數(shù),直至對(duì)象為系統(tǒng)所回收。假如此對(duì)象也在自動(dòng)釋放池里,那么稍后系統(tǒng)清空池子時(shí)還要把它再釋放一次,而這將導(dǎo)致程序崩潰。

第二個(gè)錯(cuò)誤在于: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。因此,保留計(jì)數(shù)可能永遠(yuǎn)都不會(huì)完全歸零。所以說(shuō),這段代碼就算有時(shí)能正常運(yùn)行,也多半是憑運(yùn)氣,而非理性判斷。對(duì)象回收之后,如果while循環(huán)仍然在運(yùn)行,那么目前的運(yùn)行期系統(tǒng)一般會(huì)直接令應(yīng)用程序崩潰。

從來(lái)都不需要編寫這種代碼。這段代碼所要實(shí)現(xiàn)的操作,應(yīng)該通過(guò)內(nèi)存管理來(lái)解決。開(kāi)發(fā)者在期望系統(tǒng)于某處回收對(duì)象時(shí),應(yīng)該確保沒(méi)有尚未抵消的保留操作,也就是不要令保留計(jì)數(shù)大于期望值。在這種情況下,如果發(fā)現(xiàn)某對(duì)象的內(nèi)存泄露了,那么應(yīng)該檢查還有誰(shuí)仍然保留這個(gè)對(duì)象,并查明為何沒(méi)有釋放此對(duì)象。

下面這段代碼:

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);
    
NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu",[numberI retainCount]);
    
NSNumber *numberF = @3.141f;
NSLog(@"numberF retainCount = %lu",[numberF retainCount]);

在64位Mac OS X系統(tǒng)中,用Clang4.1編譯后,這段代碼輸出的消息如下:

string retainCount = 18446744073709551615
numberI retainCount = 9223372036854775807
numberF retainCount = 1

第一個(gè)對(duì)象的保留計(jì)數(shù)是264-1,第二個(gè)對(duì)象的保留計(jì)數(shù)是263-1。由于二者皆為“單例對(duì)象”,所以其保留計(jì)數(shù)都很大。系統(tǒng)會(huì)盡可能把NSString實(shí)現(xiàn)成單例對(duì)象。如果字符串像本例所舉的這樣,是個(gè)編譯器常量,那么久可以這樣來(lái)實(shí)現(xiàn)了。在這種情況下,編譯器會(huì)把NSString對(duì)象所表示的數(shù)據(jù)放到應(yīng)用程序的二進(jìn)制文件里,這樣的話,運(yùn)行程序時(shí)就可以直接用了,無(wú)須再創(chuàng)建NSString對(duì)象。NSNumber也類似,它使用了一種叫做“標(biāo)簽指針”(tagged pointer)的概念來(lái)標(biāo)注特定類型的數(shù)值。這種做法不使用NSNumber對(duì)象,而是把與數(shù)值有關(guān)的全部消息都放在指針值里面。運(yùn)行期系統(tǒng)會(huì)在消息派發(fā)期檢測(cè)到這種標(biāo)簽指針,并對(duì)它執(zhí)行相應(yīng)操作,使其行為看上去和真正的NSNumber對(duì)象一樣。這種優(yōu)化只在某些場(chǎng)合使用,比如范例中的浮點(diǎn)數(shù)對(duì)象就沒(méi)有優(yōu)化,所以其保留計(jì)數(shù)就是1。

另外,像剛才所說(shuō)的那種單例對(duì)象,其保留計(jì)數(shù)絕對(duì)不會(huì)變。這種對(duì)象的保留及釋放操作都是“空操作”。可以看到,即便兩個(gè)單例對(duì)象之間,其保留計(jì)數(shù)也各不相同,系統(tǒng)對(duì)其保留計(jì)數(shù)的這種處理方式再一次表明:我們不應(yīng)該總是依賴保留計(jì)數(shù)的具體值來(lái)編碼。

要點(diǎn):

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

轉(zhuǎn)載請(qǐng)注明出處:第五章 內(nèi)存管理

參考:《Effective Objective-C 2.0》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容