第五章 內存管理

29.理解引用計數

Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數器。如果想使某個對象繼續存活,那就遞增其引用計數;用完了之后,就遞減其計數。計數變為0,就表示沒人關注此對象了,于是,就可以把它銷毀。

從Mac OS X 10.8開始,“垃圾回收集器”(garbage collector)已經正式廢棄了,以Objective-C代碼編寫Mac OS X程序時不應再使用它,而iOS則從未支持過垃圾收集。

1. 引用計數工作原理

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

  • retain:遞增保留計數。
  • release:遞減保留計數。
  • autorelease:待稍后清理“自動釋放池”(autorelease pool)時,再遞減保留計數。

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

對象創建出來時,其保留計數至少為1。若想令其繼續存活,則調用retain方法。要是某部分代碼不再使用此對象,不想令其繼續存活,那就調用release或autorelease方法。最終當保留計數歸零時,對象就回收了(deallocated),也就是說,系統會將其占用的內存標記為“可重用”(reuse)。此時,所有指向該對象的引用也都變得無效了。

下圖演示了對象自創建出來之后歷經一次“保留”及兩次“釋放”操作的過程。

在對象聲明周期中,其保留計數時而遞增,時而遞減,最終歸零

應用程序在其聲明期中會創建很多對象,這些對象都相互聯系著。例如,表示個人信息的對象會引用另一個表示人名的字符串對象,而且可能還會引用其他個人信息對象,比如存放朋友的set中就是如此,于是,這些相互關聯的對象就構成了一張“對象圖”(object graph)。對象如果持有指向其他對象的強引用,那么前者就“擁有”后者。也就是說,對象想令其所引用的那些對象繼續存活,就可將其“保留”。等用完了之后,再釋放。

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

對象圖

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

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

由于代碼中直接調用了release方法,所以在ARC下無法編譯。在Objective-C中,調用alloc方法所返回的對象由調用者所擁有。也就是說,調用者已通過alloc方法表達了想令該對象繼續存活下去的意愿。不過請注意,這并不是說對象此時的保留計數必定是1。在alloc或“initWithInt:”方法的實現代碼中,也許還有其他對象也保留了此對象,所以,其保留計數可能會大于1。能夠肯定的是:保留計數至少為1。保留計數這個概念就應該這樣理解才對。絕不應該說保留計數一定是某個值,只能說所執行的操作是遞增了該計數還是遞減了該計數。

創建完數組后,把number對象加入其中。調用數組的“addObject:”方法時,數組也會在number上調用retain方法,以期繼續保留此對象。這時,保留計數至少為2。接下來,代碼不再需要number對象了,于是將其釋放?,F在的保留計數至少為1。這樣就不能照常使用number變量了。調用release之后,已經無法保證所指的對象仍然存活。當然,根據本例中的代碼,我們顯然知道number對象在調用了release之后仍然存活,因為數組還在引用著它。然而絕不應假設此對象一定存活,也就是說,不要像下面這樣編寫代碼:

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

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

為避免在不經意間使用了無效對象,一般調用完release之后都會清空指針。這就能保證不會出現可能指向無效對象的指針,這種指針通常稱為“懸掛指針”(dangling pointer)。比方說,可以這樣編寫代碼來防止此情況發生:

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

2. 屬性存取方法中的內存管理

數組通過在其元素上調用retain方法來保留那些對象。不光是數組,其他對象也可以保留別的對象,這一般通過訪問“屬性”來實現,而訪問屬性時,會用到相關實例變量的獲取方法及設置方法。若屬性為“strong關系”,則設置的屬性值會保留。比方說,有個名叫foo的屬性由名為_foo的實例變量所實現。那么該屬性的設置方法會是這樣:

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

此方法將保留新值并釋放舊值,然后更新實例變量,令其指向新值。順序很重要。假如還未保留新值就先把舊值釋放了,而且兩個值又指向同一個對象,那么,先執行的release操作就可能導致系統將此對象永久回收。而后續的retain操作則無法令這個已經徹底回收的對象復生,于是實例變量就成了懸掛指針。

3. 自動釋放池

調用release會立刻遞減對象的保留計數(而且還有可能令系統回收此對象),然后有時候可以不調用它,改為調用autorelease,此方法會在稍后遞減計數,通常是在下一次“事件循環”(event loop)時遞減,不過也可能執行得更早些。

此特性很有用,尤其是在方法中返回對象時更應該用它。在這種情況下,我們并不總是想令方法調用者手工保留其值。比方說,有下面這個方法:

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

此時返回的str對象其保留計數比期望值要多1(+1 retain count),因為調用alloc會令保留計數加1,而又沒有與之對應的釋放操作。保留計數多1,就意味著調用者要負責處理多出來的這一次保留操作。必須設法將其抵消。這并不是說保留計數本身就一定是1,它可能大于1,不過那取決于“initWithFromat:”方法內的實現細節。你要考慮的是如何將多出來的這一次保留操作抵消掉。

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

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

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

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

由于返回的str對象將于稍后自動釋放,所以多出來的那一次保留操作到時自然就會抵消,無須再執行內存管理操作。因為自動釋放池中的釋放操作要等到下一次事件循環時才會執行,所以NSLog語句在使用str對象前不需要手工執行保留操作。但是,假如要持有此對象的話(比如將其設置給實例變量),那就需要保留,并于稍后釋放:

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

由此可見,autorelease能延長對象生命期,使其在跨越方法調用邊界后依然可以存活一段時間。

4. 循環引用

使用引用計數機制時,經常要注意的一個問題就是“循環引用”(retain cycle),也就是呈環狀相互引用的多個對象。這將導致內存泄露,因為循環中的對象其保留計數不會降為0。對于循環中的每個對象來說,至少還有另外一個對象引用著它。下圖中的每個對象都引用了另外兩個對象之中的一個。在這個循環里,所有對象的保留計數都是1。

循環引用

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

要點:

  • 引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好之后,其保留計數至少為1。若保留計數為正,則對象繼續存活。當保留計數降為0時,對象就被銷毀了。
  • 在對象生命期中,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數。

30.使用ARC簡化引用計數

使用ARC時一定要記住,引用計數實際上還是要執行的,只不過保留與釋放操作現在是由ARC自動為你添加。由于ARC會自動執行retain、release、autorelease等操作,所以直接在ARC下調用這些內存管理方法是非法的。具體來說,不能調用下列方法:

retain
release
autorelease
dealloc

實際上,ARC在調用這些方法事,并不通過普通的Objective-C消息派發機制,而是直接調用其底層C語言版本。這樣做性能更好,因為保留及釋放操作需要頻繁執行,所以直接調用底層函數能節省很多CPU周期。比方說,ARC會調用與retain等價的底層函數objc_retain。這也是不能覆寫retain、release或autorelease的緣由,因為這些方法從來不會直接被調用。

1. 使用ARC時必須遵守的方法命名規則

將內存管理語義在方法名中表示出來早已成為Objective-C的慣例,而ARC則將之確立為硬性規定。這些規則簡單地體現在方法名上。若方法名以下列詞語開頭,則其返回的對象歸調用者所有:
alloc
new
copy
mutableCopy

歸調用者所有的意思是:調用上述四種方法的那段代碼要負責釋放方法所返回的對象。

若方法名不以上述四個詞語開頭,則表示其所返回的對象并不歸調用者所有。在這種情況下,返回的對象會自動釋放,所以其值在跨越方法調用邊界后依然有效。要想使對象多存活一段時間,必須令調用者保留它才行。

維系這些規則所需的全部內存管理事宜均有ARC自動處理,其中也包括在將其返回的對象上調用autorelease,下列代碼演示了ARC的用法:

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

+(EOCPerson *)somePerson{
    EOCPerson *person = [[EOCPerson alloc]init];
    return person;
    //這個方法不是以擁有關系關鍵字開頭的,所以ARC會自動在返回的時候加上autorelease
}

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

除了會自動調用“保留”與“釋放”方法外,使用ARC還可以執行一些手工操作很難甚至無法完成的優化。例如,在編譯器,ARC會把能夠互相抵消的retain、release、autorelease操作約簡。如果發現在同一個對象上執行多次“保留”與“釋放”操作,那么ARC有時可以成對地移除這兩個操作。

ARC可以在運行期監測到這一對多余的操作,也就是autorelease及緊跟其后的retain。為了優化代碼,在方法中返回自動釋放的對象時,要執行一個特殊函數。此時不直接調用對象的autorelease方法,而是改為調用objc_autoreleaseReturnValue。此函數會檢視當前方法返回之后即將要執行的那段代碼。若發現那段代碼在返回的對象上執行retain操作,則設置全局數據結構(此數據結構的具體內容因處理器而已)中的一個標志位而不執行autorelease操作。與之相似,如果方法返回了一個自動釋放的對象,而調用方法的代碼要保留此對象,那么此時不直接執行retain,而是改為執行objc_retainAutoreleaseReturnValue函數。此函數要檢測剛才提到的那個標志位,若已經置位,則不執行retain操作。設置并檢測標志位,要比調用autorelease和retain更快。

下面這段代碼演示了ARC是如何通過這些特殊函數來優化程序的:

+(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);

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

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. 變量的內存管理語義

ARC也會處理局部變量與實例變量的內存管理。默認情況下,每個變量都是指向對象的強引用。

@interface EOCClass : NSObject
{
    id _object;
}

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

在手動管理引用計數時,實例變量_object并不會自動保留其值,而在ARC環境下則會這樣做。也就是說,若在ARC下編譯setup方法,則其代碼會變為:

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

當然,在此情況下,retain和release可以消去。所以,ARC會將這兩個操作化簡掉,于是,實際執行的代碼還是和原來一樣。不過,在編寫設置方法時,使用ARC會簡單一些。如果不用ARC,那么需要像下面這樣來寫:

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

但是這樣寫會出問題。加入新值和實例變量已有的值相同,如果只有當前對象還在引用這個值,那么設置方法中的釋放操作會使該值的保留計數降為0,從而導致系統將其回收。接下來再執行保留操作,就會令應用程序崩潰。使用ARC之后,就不可能發生這種疏失了。在ARC環境下,與剛才等效的設置函數可以這么寫:

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

ARC會用一種安全的方式來設置:先保留新值,再釋放舊值,最后設置實例變量。

在應用程序中,可用下列修飾符來改變局部變量與實例變量的語義:

__stong:默認語義,保留此值

__unsafe_unretained:不保留此值,這么做可能不安全,因為等到再次使用變量時,其對象可能已經回收了。

__weak:不保留此值,但是變量可以安全使用,因為如果系統把這個對象回收了,那么變量也會自動清空。

__autoreleasing:把對象“按引用傳遞”給方法時,使用這個特殊的修飾符。此值在方法返回時自動釋放。

比方說,想令實例變量的語義與不使用ARC時相同,可以運用__weak或__unsafe_unretained修飾符:

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

不論采用上面哪種寫法,在設置實例變量時都不會保留其值。
我們經常用__weak局部變量來打破循環引用。

3.ARC如何清理實例變量

使用ARC之后,不需要再編寫像不使用ARC是的那種dealloc方法了,因為ARC借用Objective-C++的一項特性來生成清理例程(cleanup routime)?;厥誒bjective-C++對象時,待回收的對象會調用所有C++對象的析構函數(destructor)。編譯器如果發現某個對象里含有C++對象,就會生成名為.cxx_destruct的方法。而ARC則借助此特性,在該方法中生成清理內存所需的代碼。

不過,如果有非Objective-C的對象,比如CoreFoundation中的對象或是由malloc()分配在堆中的內存,那么仍然需要清理。然而不需要像原來那樣調用超類的dealloc方法。ARC下不能直接調用dealloc方法。ARC會自動在.cxx_destruct方法中生成代碼并運行此方法。而在生成的代碼中會自動調用超類的dealloc方法。ARC環境下,dealloc方法可以像這樣寫:

-(void)dealloc{
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

因為ARC會自動生成回收對象時所執行的代碼,所以通常無須再編寫dealloc方法。

4. 覆寫內存管理方法

不使用ARC時,可以覆寫內存管理方法。但是在ARC環境下不能這么做,因為會干擾到ARC分析對象生命期的工作。

要點:

  • 有ARC之后,程序員就無須擔心內存管理問題了。使用ARC來編程,可省去類中的許多“樣板代碼”。
  • ARC管理對象聲明周期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作。在ARC環境下,變量的內存管理語義可以通過修飾符指明,而原來則需要手工執行“保留”及“釋放”操作。
  • 由方法所返回的對象,其內存管理語義總是通過方法名來提現。ARC將此確定為開發者必須遵守的規則。
  • ARC只負責管理Objective-C對象的內存。尤其要注意:CoreFoundation對象不歸ARC管理,開發者必須實時調用CFRetain/CFRelease。
  • 不要在屬性名前面加上alloc、new、copy或mutableCopy,否則編譯器會報錯(Property follows Cocoa naming for returning ‘owned’objects)可用此方法解決,但是強烈不建議這么用

31.在dealloc方法中只釋放引用并解除監聽

對象在經歷其生命期后,最終會為系統所回收,這時就要執行dealloc方法了,然后具體何時執行,則無法保證。不應該自己調用dealloc方法,運行期會在適當的生活調用它。

在dealloc方法中主要是要釋放對象所擁有的引用,也就是說把所有Objective-C對象都釋放掉,ARC會通過自動生成的.cxx_destruct方法,在dealloc中為你自動添加這些釋放代碼。對象所擁有的其他非Objective-C對象也要釋放。比如CoreFoundation的對象就必須手工釋放,因為它們是由純C的API所生成的。

在dealloc方法中,通常還要把原來配置過的觀測行為都清理掉。如果用NSNotificationCenter給此對象注冊過某種通知,那么一般應該在這里注銷,這樣的話,通知系統就不再把通知發給回收后的對象了。

dealloc方法可以這樣來寫:

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

如果是手動管理引用計數的話,最后還要調用”[super dealloc]”,ARC會自動執行此操作。手動管理還要將當前對象所擁有的全部Objective-C對象逐個釋放。

雖說應該于dealloc中釋放引用,但是開銷較大或系統內稀缺的資源則不在此列。像是文件描述符(file descriptor)、套接字(socket)、大塊內存等,都屬于這種資源。不能指望dealloc方法必定會在某個特定的時機調用,因為有一些無法預料的東西可能也持有此對象。通常的做法是,實現另外一個方法,當應用程序用完資源對象后,就調用此方法。

比方說,如果某個對象管理著連接服務器所用的套接字,那么也許就需要這種“清理方法”。此對象可能要通過套接字連接到數據庫。

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

#import <Foundation/Foundation.h>

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

該類與開發者之間的約定是:想打開連接,就調用“open:”方法;連接使用完畢,就調用close方法?!瓣P閉”操作必須在系統把連接對象回收之前調用,否則就是編程錯誤,這與通過“保留”與“釋放”操作來平衡引用計數是類似的。

在清理方法而非dealloc方法中清理資源還有個原因,就是系統并不保證每個創建出來的對象的dealloc都會執行。極個別情況下,當應用程序終止時,仍有對象處于存活狀態,這些對象沒有收到dealloc消息。由于應用程序終止之后,其占用資源也會返還給操作系統,所以實際上這些對象也就等于是消亡了。不調用dealloc方法是為了優化程序效率。在Mac OS X及iOS應用程序所對應的application delegate中,都含有一個會于程序終止時調用的方法。如果一定要清理某些對象,那么可在此方法中調用那些對象的“清理方法”。

在Mac OS X系統里,應用程序終止時會調用NSApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(NSNotification *)notification

而在iOS系統里,應用程序終止時則會調用UIApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(UIApplication *)application

如果對象管理著某些資源,那么在dealloc中也要調用“清理方法”,以防開發者忘了清理這些資源。下面舉例說明close與dealloc方法應如何寫:

-(void)close{
    //clean up resources
    _closed = YES;
}

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

編寫dealloc方法時還需注意,不要在里面隨便調用其他方法。無論在這里調用什么方法都不應該,因為對象此時“已近尾聲”。如果在這里所調用的方法又要異步執行某些任務,或是又要繼續調用它們自己的某些方法,那么等到那些任務執行完畢時,系統已經把當前這個待回收的對象徹底摧毀了。

調用dealloc方法的那個線程會執行“最終的釋放操作”,令對象的保留計數降為0,而某些方法必須在特定的線程里(比方說主線程里)調用才行。若在dealloc里調用了那些方法,則無法保證當前這個線程就是那些方法所需的線程。通過編寫常規代碼的方式,無論如何都沒辦法保證其會安全運行在正確的線程上,因為對象處在“正在回收的狀態”,為了指明此種情況,運行期系統已經改動了對象內部的數據結構。

在dealloc里也不要調用屬性的存取方法,因為有人可能會覆寫這些方法,并于其中做一些無法在回收階段安全執行的操作。此外,屬性可能正處于“鍵值觀測”機制的監控之下,該屬性的觀察者可能會在屬性改變時“保留”或使用這個即將回收的對象。這種做法會令運行期系統的狀態完全失調,從而導致一些莫名其妙的錯誤。

要點:

  • 在dealloc方法里,應該做的事情就是釋放指向其他對象的引用,并取消原來訂閱的“鍵值觀察(KVO)”或NSNotificationCenter等通知,不要做其他事情。
  • 如果對象持有文件描述符等系統資源,那么應該專門編寫一個方法來釋放此種資源。這樣的類要和其他使用者約定:用完資源后必須調用close方法。
  • 執行異步任務的方法不應該在dealloc里調用;只能在正常狀態下執行的那些方法也不應該在dealloc里調用,因為此時對象已處于正在回收的狀態了。

32.編寫“異常安全代碼”時留意內存管理問題

Objective-C的錯誤模型表明,異常只應發生嚴重錯誤后拋出,雖說如此,不過有時仍然需要編寫代碼來捕獲并處理異常。比如使用Objective-C++來編碼時,或是編碼中用到了第三方程序庫而此程序庫所判處的異常又不受你控制時,就需要捕獲及處理異常了。
在try塊中,如果先保留了某個對象,然后在釋放它之前又拋出了異常,那么,除非catch塊能處理此問題,否則對象所占內存就將泄露。
異常處理例程將自動銷毀對象,然而在手動管理引用計數時,銷毀工作有些麻煩。以下面這段代碼使用手工引用計數的Objective-C代碼為例:

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

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

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對象,所以必須把它從@try塊里移到外面去。要是所有對象都得如此釋放,那這樣做就會非常乏味。而且@try塊中的邏輯更為復雜,含有多條語句,那么很容易就會因為忘記某個對象而導致泄露。若泄露的對象是文件描述符或數據庫連接等稀缺資源(或是這些稀缺資源的管理者),則可能引發大問題,因為這樣導致應用程序把所有系統資源都抓在自己手里而不及時釋放。

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

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

現在問題更大了:由于不能調用release,所以無法像手動管理引用計數那樣把釋放操作移到@finally塊中。你可能認為這種狀況ARC自然會處理的。但實際上ARC不會自動處理,因為這樣做需要加入大量樣板代碼,以便跟蹤待清理的對象,從而在拋出異常時將其釋放。可是,這段代碼會嚴重影響運行期的性能,即便在不拋異常時也如此。而且,添加進來的額外代碼還會明顯增加應用程序的大小。這些副作用都不甚理想。

雖說默認狀況下未開啟,但ARC依然能生成這種安全處理異常所用的附加代碼。-fobjc-arc-exception這個編譯器標志用來開啟此功能。其默認不開啟的原因是:Objective-C代碼中,只有當應用程序必須因異常狀況而終止時才拋出異常。因此,如果應用程序即將終止,那么是否還會發生內存泄露就已經無關緊要了。在應用程序必須立即終止的情況下,還去添加安全處理異常所用的附加代碼是沒有意義的。

有種情況編譯器會自動把-fobjc-arc-exception標志打開,就是出于Objective-C++模式時。因為C++處理異常所用的代碼與ARC實現的附加代碼類似,所以令ARC加入自己的代碼以安全處理異常,其性能損失并不太大。此外,由于C++頻繁使用異常,所以Objective-C++程序員很可能也會使用異常。

如果手工管理引用計數,而且必須捕獲異常,那么要設法保證所編代碼能把對象正確清理干凈。若使用ARC且必須捕獲異常,則需打開編譯器的-fobjc-arc-exception標志。但最重要的是:在發現大量異常捕獲操作時,應考慮重構代碼,用第21條所講的NSError式錯誤信息傳遞法來取代異常。

要點:

  • 捕獲異常時,一定要注意將try塊內所創立的對象清理干凈。
  • 在默認情況下,ARC不生成安全處理異常所需的清理代碼。開啟編譯器標志后,可以生成這種代碼,不過會導致應用程序變大,而且會降低運行效率。

33.用弱引用避免循環引用

對象圖里經常會出現一種情況,就是幾個對象都以某種方式互相引用,從而形成”環“。由于Objective-C內存管理模型使用引用計數架構,所以這種情況通常會泄露內存,因為最后沒有別的東西會引用環中的對象。這樣的話,環里的對象就無法為外界所訪問了,但對象之間尚有引用,這些引用使得他們都能繼續存活下去,而不會為系統所回收。

最簡單的循環引用由兩個對象構成,他們互相引用對方。如圖:

這種循環引用的產生原因不難理解,且很容易就能通過查看代碼而偵測出來:

@class EOCClassA;
@class EOCClassB;

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

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

如果把EOCClassA實例的other屬性設置成某個EOCClassB實例,而把那個EOCClassB實例的other屬性又設置成這個EOCClassA實例,那么就會出現下圖的循環引用:

循環引用會導致內存泄露。如果只剩下一個引用還指向循環引用中的實例,而現在又把這個引用移除,那么整個循環引用就泄露了。也就是說,沒辦法再訪問其中的對象了。

避免循環引用的最佳方式就是弱引用。這種引用經常用來表示“非擁有關系”。將屬性聲明為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實例就不再通過other屬性來擁有EOCClassA實例了。屬性特質中的unsafe_unretained一詞表明,屬性值可能不安全,而且不歸此實例所擁有。如果系統已經把屬性所指的那個對象回收了,那么在其上調用方法可能會使應用程序崩潰。由于本對象并不保留屬性對象,因此其有可能為系統所回收。

用unsafe_unretained修飾的屬性特質,其語義同assign特質等價,然而assign通常只用于數值類型,unsafe_unretained則多用于對象類型。這個詞本身就表明其所修飾的屬性可能無法安全使用。

Objective-C中還有一項與ARC相伴的運行期特性,可能令開發者安全使用弱引用:這就是weak屬性特質,它與unsafe_unretained的作用完全相同。然而,只要系統把屬性回收,屬性值就會自動設置為nil。在剛才那段代碼中,EOCClassB的other屬性可修改如下:

@property(nonatomic,weak)EOCClassA *other;

下圖演示了unsafe_unretained與weak屬性的區別:

當指向EOCClassA實例的引用移除后,unsafe_unretained屬性仍然指向那個已經回收的實例,而weak屬性則指向nil。

使用weak屬性而非unsafe_unretained引用可以令代碼更安全。應用程序也許會顯示出錯誤的數據,但不直接崩潰。

一般來說,如果不擁有某對象,那就不要保留它。這條規則對collection例外。collection雖然并不直接擁有其內容,但是它要代表自己所屬的那個對象來保留這些元素。有時,對象中的引用會指向另外一個并不歸自己所擁有的對象,比如Delegate模式就是這樣。

要點:

  • 將某些引用設為weak,可避免出現“循環引用”。
  • weak引用可以自動清空,也可以不自動清空。自動清空是隨著ARC而引入的新特性,由運行期系統來實現。在具備自動清空功能的弱引用上,可以隨意讀寫其數據,因為這種引用不會指向已經回收過的對象。

34.以“自動釋放池塊”降低內存峰值

Objective-C對象的生命期取決于其引用計數。在Objective-C的引用計數架構中,有一項特性叫做“自動釋放池”(autorelease pool)。釋放對象有兩種方式:一種是調用release方法,使其保留計數立即遞減;另一種是調用autorelease方法,將其加入“自動釋放池”中。自動釋放池用于存放那些需要在稍后某個時刻釋放的對象。清空自動釋放池時,系統會向其中的對象發送release消息。

創建自動釋放池所用語法如下:

@autoreleasepool {
        //...
    }

一般情況下無須擔心自動釋放池的創建問題。Mac OS X與iOS應用程序分別運行于Cocoa及Cocoa Touch環境中。系統會自動創建一些線程,比方說主線程或是GCD機制中的線程,這些線程默認都有自動釋放池,每次執行“事件循環“(event loop)時,就會將其清空。因此,不需要自己來創建”自動釋放池塊“。通常只有一個地方需要創建自動釋放池塊,那就是在mian函數里,我們用自動釋放池來包裹應用程序的主入口點。比方說,iOS程序的main函數經常這樣寫:

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

從技術角度看,不是非得有個”自動釋放池塊”才行。因為塊的末尾恰好就是應用程序的終止處,而此時操作系統會把程序所占的全部內存都釋放掉。雖說如此,但是如果不寫這個塊的話,那么由UIApplicationMain函數所自動釋放的那些對象,就沒有自動釋放池可以容納了,于是系統會發出警告信息來表明這一情況。所以說,這個池可以理解成最外圍捕捉全部自動釋放對象所用的池。

位于自動釋放池范圍內的對象,將在此范圍末尾處收到release消息。自動釋放池可以嵌套。系統在自動釋放對象時,會把它放到最內層的池里。比方說:

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

在本例中有兩個對象,它們都由類的工廠方法所創建,這樣創建出來的對象會自動釋放。NSString對象放在外圍的自動釋放池中,而NSNumber對象則放在里層的自動釋放池中。將自動釋放池嵌套用的好處是,可以借此控制應用程序的內存峰值,使其不致過高。

考慮下面這段代碼:

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

如果“doSomethingWithInt:“方法要創建臨時對象,那么這些對象很可能會放在自動釋放池里。比方說,它們可能是一些臨時字符串。但是,即便這些對象在調用完方法之后就不再使用了,它們也依然處于存活狀態,因為目前還在自動釋放池里,等待系統稍后將其釋放并回收。然而,自動釋放池要等線程執行下一次事件循環時才會清空。這就意味著在執行for循環時,會持續有新對象創建出來,并加入自動釋放池中。所有這種對象都要等for循環執行完才會釋放。這樣一來,在執行for循環時,應用程序所占內存就會持續上漲,而等到所有臨時對象都釋放后,內存用量又會突然下降。

這種情況不甚理想,尤其當循環長度無法預知,必須取決于用戶輸入時更是如此。比方說,要從數據庫中讀取許多對象。代碼可能會這么寫:

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

EOCPerson的初始化函數也許會像上例那樣,再創建出一些臨時對象。若記錄有很多條,則內存中也會有很多不必要的臨時對象,它們本來應該提早回收的。增加一個自動釋放池即可解決此問題。如果把循環內的代碼包裹在”自動釋放池塊“中,那么在循環中自動釋放的對象就會放在這個池,而不是線程的的主池里面。例如:

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

加上這個自動釋放池之后,應用程序在執行循環時的內存峰值就會降低,不再像原來那么高了。內存峰值是指應用程序在某個特定時段內的最大內存用量。新增的自動釋放池塊可以減少這個峰值,因為系統會在塊的末尾把某些對象回收掉。而剛才提到的那種臨時對象,就在回收之列。

自動釋放池機制就像”?!耙粯印O到y創建好自動釋放池之后,就將其推入棧中,而清空自動釋放池,則相當于將其從棧中彈出。在對象上執行自動釋放操作,就等于將其放入棧頂的那個池里。

是否應該用池來優化效率,完全取決于具體的應用程序。首先得監控內存用量,判斷其中有沒有需要解決的問題,如果沒完成這一步,那就別急著優化。盡管自動釋放池塊的開銷不太大,但畢竟還是有的,所以盡量不要建立額外的自動釋放池。

如果在ARC出現之前就寫過Objective-C程序,那么可能還記得有種老式寫法,就是使用NSAutoreleasePool對象。這個特殊的對象與普通對象不同,它專門用來表示自動釋放池。就像新語法中的自動釋放池塊一樣。但是這種寫法并不會在每次執行for循環時都清空池,此對象更為”重量級“,通常用來創建那種偶爾需要清空的池,比方說:

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]; 
    }

現在不需要再這樣寫代碼了。采用隨著ARC所引入的新語法,可以創建出更為”輕量級“的自動釋放池。原來縮寫的代碼可能會每執行n次循環清空一次自動釋放池,現在可以改用自動釋放池塊把for循環中的語句包起來,這樣的話,每次執行循環時都會簡歷并清空自動釋放池。

@autoreleasepool語法還有個好處:每個自動釋放池均有其范圍,可以避免無意間誤用了那些在清空池后已為系統所回收的對象。比方說,考慮下面這段采用舊式寫法的代碼:

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

調用”userObject:“方法時所傳入的那個對象,可能已經為系統所回收了。同樣的代碼改用信使寫法就變成了:

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

這次根本就無法編譯,因為object變量出了自動釋放池塊的外圍就不可用了,所以在調用”useObject:“方法時不能用它做參數。

要點:

  • 自動釋放池排布在棧中,對象收到autorelease消息后,系統將其放入到最頂端的池里。
  • 合理運用自動釋放池,可降低應用程序的內存峰值。
  • @autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池。

35.用“僵尸對象”調試內存管理問題

向業已回收的對象發送消息是不安全的。這么做有時可以,有時不行。具體可行與否,完全取決于對象所占內存有沒有為其他內容所覆寫。而這塊內存有沒有移作他用,又無法確定,因此,應用程序只是偶爾崩潰。在沒有崩潰的情況下,那塊內存可能只復用了其中一部分,所以對象中的某些二進制數據依然有效。還有一種可能,就是那塊內存恰好為另外一個有效且存活的對象所占據。在這種情況下,運行期系統會把消息發到新對象那里,而此對象也許能應答,也許不能。

Cocoa提供了“僵尸對象”(Zombie Object)這個非常方便的功能。啟用這項調試功能之后,運行期系統會把所有已經回收的實例轉化成特殊的“僵尸對象”,而不會真正回收它們。這種對象所在的核心內存無法重用,因此不可能遭到覆寫。僵尸對象收到消息后,會拋出異常,其中準確說明了發送過來的消息,并描述了回收之前的那個對象。僵尸對象是調試內存管理問題的最佳方式。

將NSZombieEnabled環境變量設為YES,即可開啟此功能。在Mac OS X系統中用bash運行應用程序時,可以這么做:

export NSZombieEnabled=“YES”
./app

給僵尸對象發送消息后,控制臺會打印消息,而應用程序則會終止。打印出來的消息就像這樣:

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

在Xcode中開啟方法為:編輯應用程序的Scheme,在對話框左側選擇”Run“,然后切換至”Diagnostics“分頁,最后勾選”Enable Zombie Objects“選項。

僵尸對象的工作原理是什么呢?它的實現代碼深植與Objective-C的運行期程序庫、Foundation框架及CoreFoundation框架中。系統在即將回收對象時,如果發現通過環境變量啟用了僵尸對象功能,那么還將執行一個附加步驟。這一步就是把對象轉化為僵尸對象,而不徹底回收。

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

#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);
}

本例代碼中有個函數,可以根據給定的對象打印出所屬的類及其超類名稱。此函數沒有直接給對象發送Objective-C的class消息,而是調用了運行期庫里的object_getClass()函數。因為如果參數已經是僵尸對象了,那么給其發送Objective-C消息后,控制臺會打印錯誤消息,而且應用程序會崩潰。本例代碼將輸出下面這種消息:

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

對象所屬的類已由EOCClass變成_NSZombie_EOCClass。_NSZombie_EOCClass實際上是在運行期生成的,當首次碰到EOCClass類的對象要變成僵尸對象時,就會創建這么一個類。創建過程中用到了運行期程序庫里的函數,它們的功能很強大,可以操作類列表。

僵尸類是從名為NSZombie的模板類里復制出來的。這些僵尸類沒有多少事情可做,只是充當一個標記。接下類介紹它們是怎樣充當標記的。首先來看下嘛這段偽代碼,其中演示了系統如何根據需要創建出僵尸類,而僵尸類又如何把待回收的對象轉化成僵尸對象。

//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

這個過程其實就是NSObject的dealloc方法所做的事。運行期系統如果發現NSZombieEnabled環境變量已設置,那么就把dealloc方法”調配“(swizzle)成一個會執行上述代碼的版本。執行到程序末尾時,對象所屬的類已經變為_NSZombie_OriginalClass了,其中OriginalClass指的是原類名。

代碼中的關鍵之處在于:對象所占內存沒有(通過調用free()方法)釋放,因此,這塊內存不可復用。雖說內存泄露了,但這只是個調試手段,制作正式發行的應用程序時不會把這項功能打開,所以這種泄露問題無關緊要。

但是,系統為何要給每個變為僵尸的類都創建一個對應的新類呢?這是因為,給僵尸對象發消息后,系統可由此知道該對象原來所屬的類。假如把所有僵尸對象都歸到NSZombie類里,那原來的類名就丟了。創建新類的工作由運行期函數objc_duplicateClass()來完成,它會把整個NSZombie類結構拷貝一份,并賦予其新的名字。副本類的超類、實例變量及方法都和復制前相同。還有種做法也能保留舊類名,那就是不拷貝NSZombie,而是創建繼承自NSZombie的新類,但是用相應的函數完成此功能,其效率不如直接拷貝高。

僵尸類的作用會在消息轉發例程(第12條)中體現出來。NSZombie類(以及所有從該類拷貝出來的類)并未實現任何方法。此類沒有超類,因此和NSObject一樣,也是個”根類“,該類只有一個實例變量,叫做isa,所有Objective-C的根類都必須有此變量。由于這個輕量級的類沒有實現任何方法,所以發給它的全部消息都要經過”完整的消息轉發機制“。

在完整的消息轉發機制中,forwarding是核心,調試程序時,大家可能在?;厮菹⒗锟匆娺^這個函數。它首先要做的事情就包含檢查接收消息的對象所屬的類名。若名稱前綴為NSZombie,則表明消息接收者是僵尸對象,需要特殊處理。此時會打印一條消息,其中指明了僵尸對象所收到的消息及原來所屬的類,然后應用程序就終止了。在僵尸類名中嵌入原來類名的好處,這時就可以看出來了。只要把NSZombie從僵尸類名的開頭拿掉,剩下的就是原始類名。下面偽代碼演示了這一過程:

//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();
    }

要點:

  • 系統在回收對象時,可以不將其真的回收,而是把它轉化成為僵尸對象。通過環境變量NSZombieEnabled可開啟此功能。
  • 系統會修改對象的isa指針,令其指向特殊的僵尸類,從而使該對象變成僵尸對象。僵尸類能夠響應所有的選擇子,響應方式為:打印一條包含消息內容及其接收者的消息,然后終止應用程序。

36.不要使用retainCount

NSObject協議中定義了下列方法,用于查詢對象當前的保留計數:

- (NSUInteger)retainCount

這個方法看上去似乎挺合理、挺有用的。它畢竟返回了保留計數,而此值對每個對象來說顯然都很重要。但問題在于,保留計數的絕對數值一般都與開發者所應留意的事情完全無關。即便只在調試時才調用此方法,通常也還是無所助益的。

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

while([]){
[object release]
}

這種寫法的第一個錯誤是:它沒考慮到后續的自動釋放操作,只是不停地通過釋放操作來降低保留計數,直至對象為系統所回收。假如此對象也在自動釋放池里,那么稍后系統清空池子時還要把它再釋放一次,而這將導致程序崩潰。

第二個錯誤在于:retainCount可能永遠不返回0,因為有時系統會優化對象的釋放行為,在保留計數還是1的時候就把它回收了。只有在系統不打算這么優化時,計數值才會遞減至0。因此,保留計數可能永遠都不會完全歸零。所以說,這段代碼就算有時能正常運行,也多半是憑運氣,而非理性判斷。對象回收之后,如果while循環仍然在運行,那么目前的運行期系統一般會直接令應用程序崩潰。

從來都不需要編寫這種代碼。這段代碼所要實現的操作,應該通過內存管理來解決。開發者在期望系統于某處回收對象時,應該確保沒有尚未抵消的保留操作,也就是不要令保留計數大于期望值。在這種情況下,如果發現某對象的內存泄露了,那么應該檢查還有誰仍然保留這個對象,并查明為何沒有釋放此對象。

下面這段代碼:

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系統中,用Clang4.1編譯后,這段代碼輸出的消息如下:

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

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

另外,像剛才所說的那種單例對象,其保留計數絕對不會變。這種對象的保留及釋放操作都是“空操作”。可以看到,即便兩個單例對象之間,其保留計數也各不相同,系統對其保留計數的這種處理方式再一次表明:我們不應該總是依賴保留計數的具體值來編碼。

要點:

  • 對象的引用計數看似有用,實則不然,因為任何給定的時間點上的“絕對引用計數”都無法反映對象生命期的全貌。
  • 引入ARC之后,retainCount方法就正式廢止了,在ARC下調用該方法會導致編譯器報錯。

轉載請注明出處:第五章 內存管理

參考:《Effective Objective-C 2.0》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容