想必每一個初學者在接觸到iOS時對于他們內(nèi)存管理方式肯定有些不太習慣,即使是如今ARC已經(jīng)流行開來的情況下,為了更懂內(nèi)存,我們還是要和這個討厭的家伙——“引用計數(shù)”打打交道。
對于已經(jīng)接觸過引用計數(shù)的朋友可以先看看以下代碼:
“靈異事件”
//class1 僅僅是一個自定義類 沒有任何方法和屬性
class1 * c1 = [[class1 alloc]init];
NSLog(@"第一次:%lu",(unsigned long)[c1 retainCount]);
//引用計數(shù)減1
[c1 release];
//輸出減1之后的結果
NSLog(@"第二次:%lu",(unsigned long)[c1 retainCount]);
我們知道,在給一個對象執(zhí)行alloc方法之后,系統(tǒng)會在堆區(qū)為這個對象分配存儲空間,引用計數(shù)初始為1,在執(zhí)行release方法之后,引用計數(shù)減1,當一個對象的引用計數(shù)為0時,系統(tǒng)執(zhí)行dealloc方法回收釋放內(nèi)存。
那么上述執(zhí)行結果“按道理來說”應該是:
第一次:1
第二次:0
或者直接崩潰,因為引用計數(shù)為0時該對象已經(jīng)被回收,因為c1此時已經(jīng)成為了一個野指針。
當我在編譯器中執(zhí)行的時候出現(xiàn)了意想不到的答案,沒有并崩潰,正常運行出的結果是:
第一次:1
第二次:1
那么我們接著看下面這個例子:
NSString *str = @"abcdefg";
NSLog(@"Str = %lu",(unsigned long)[str retainCount]);
NSNumber *num = @11;
NSLog(@"Str = %lu",(unsigned long)[str retainCount]);
NSNumber *longNum = @222222222222222222;
NSLog(@"longNum = %lu",(unsigned long)[longNum retainCount]);
NSMutableString *str1 = [[NSMutableString alloc]initWithString:@"abcdefg"];
NSLog(@"Str1 = %lu",(unsigned long)[str1 retainCount]);
結果是:
Str = 18446744073709551615
num = 9223372036854775807
longNum = 1
Str1 = 1
真相大白
在Effective Objective 2.0的第36條這樣解釋道:
對象的引用計數(shù)非常大是因為,這些對象都是“單例對象”,所以其引用計數(shù)都很大。系統(tǒng)會盡可能把NSString實現(xiàn)成單例對象。如果字符串像例子上出現(xiàn)的情況這樣,是個編譯器常量(compile-time constant),那么就可以這樣來實現(xiàn)了。在這種情況下,編譯器會把NSString對象所表示的數(shù)據(jù)放到應用程序的二進制文件里,這樣的話,運行程序時就可以直接使用了,無需再創(chuàng)建NSString對象。NSNumber也類似,他使用了一種叫做“標簽指針”(tagged pointer)的概念來標注特定類型的數(shù)值。這種做法不適用NSNumber對象,而是把與數(shù)值有關的全部消息都放在指針值里面。運行時系統(tǒng)會在消息派發(fā)期間檢測這種標簽指針,并對它執(zhí)行相應操作,使其行為看上去和真正的NSNumber對象一樣。這種優(yōu)化只在某些場合使用。并且這種單例對象的引用計數(shù)也不會改變。
也就是說,系統(tǒng)為了優(yōu)化,將一些字符串作為一個“常量”直接放進二進制文件中,并不會為它分配相應的堆區(qū)內(nèi)存。下次使用時也可以直接使用。這樣就會減少很多分配內(nèi)存,回收內(nèi)存,以及管理一個對象的引用計數(shù)等復雜的操作。從而優(yōu)化性能。
對于NSNumber,它是一個對象類型,所以按道理來說在NSNumber *中應當是例子中存在11這個數(shù)值內(nèi)存的地址。那么我們?yōu)榱舜娣?1這個簡單的數(shù)字,起碼要有2塊內(nèi)存:一個內(nèi)存存放11這個數(shù)(堆區(qū)),一個存放前面那塊內(nèi)存的地址(棧區(qū))。所以蘋果引入了tagged pointer這個概念,將一些一些比較小的數(shù)值,也就是放指針的那塊棧區(qū)放的下的數(shù)(32位系統(tǒng)小于4字節(jié)的數(shù)值,64位小于8字節(jié)的數(shù)值)。將11直接放在棧區(qū)那個本來放指針的地方,這樣堆區(qū)就不用再分配內(nèi)存給它了。但是作為我們來說,這些都是系統(tǒng)自動優(yōu)化的。我們在使用上不會有任何的問題。關于NSNumber這個有意思的問題,可以參看巧爺?shù)倪@篇文章深入理解Tagged Pointer
而且我查閱了retainCount的官方文檔,原文是這樣的:
描述: 不要使用該方法。
返回值: 接收機的引用計數(shù)。
特殊注意事項:
這種方法在調(diào)試內(nèi)存管理問題上都是沒有價值的。因為任意數(shù)量的框架對象可能保留一個對象,以持有對它的引用。而在同一時間自動釋放池可持有任意數(shù)量的延遲釋放的對象。所以你從這個方法并不太可能得到有用的信息。
要了解內(nèi)存管理,你必須遵守的基本法則,閱讀內(nèi)存管理策略。要診斷內(nèi)存管理的問題,使用合適的工具:
- Clang 靜態(tài)分析器通常你運行程序之前,發(fā)現(xiàn)內(nèi)存管理的問題。
- Object Alloc instrument(參看Instrument用戶指南)可以跟蹤對象分配和銷毀。
可用性
iOS 2.0及其之后版本
所以我們產(chǎn)生這個問題的根源就是retainCount可能對于我們來說只是一個“標識”性質(zhì)的方法,我們并不能從中的得到有用的信息。
將錯就錯
實質(zhì)上拋開retainCount的限制來說,其實上面兩個例子還是有很多可以深究的地方,以下是我的一些理解:
- 對象的引用計數(shù)可能永遠不會為0,對對象的引用計數(shù)為1,執(zhí)行引用計數(shù)減1的操作后,系統(tǒng)明白這個對象已經(jīng)不被任何對象所持有,那么應該釋放他了,所以他并不會再執(zhí)行將引用計數(shù)減為0的操作了,因為沒有必要,系統(tǒng)已經(jīng)知道該如何處理這個對象了。多余的操作只會增加系統(tǒng)的負擔。
啰嗦幾句
其他的一些相關問題:
問題1:什么對象才會有引用計數(shù)?
答:需要回收內(nèi)存的對象。換種說法就是內(nèi)存分配到了堆區(qū)的對象(雖然這樣說并不準確),我們使用類方法初始化一個對象不需要關心它的內(nèi)存,是因為它一開始就被系統(tǒng)自動分配到了常量區(qū),系統(tǒng)會幫我們搞定。而通過new,alloc,copy之類的都會在堆區(qū)為他們分配了新的內(nèi)存,引用計數(shù)也加1,這些就是需要回收的內(nèi)存,為了知道何時回收他們的內(nèi)存,便有了引用計數(shù)這樣的標記。就如同借錢一樣,你借了系統(tǒng)的錢(內(nèi)存),他就給你拿個小本本記著(引用計數(shù)),最后等你離開之前把錢收回去(回收釋放內(nèi)存)。
問題2:什么會影響引用計數(shù)?
答:
使引用計數(shù)加1的操作有:
創(chuàng)建并持有對象:alloc/new/copy/mutableCopy
持有對象:retain
使引用計數(shù)減1的操作有:
釋放持有權: release
問題3:引用計數(shù)的內(nèi)在含義是什么?
答:對象的標記,標記你對這個對象的被需求度,如果有人要用得上它,那么他就不應該被釋放掉。我自己創(chuàng)建了一個對象,說明我肯定要用它,所以這個步驟叫做“創(chuàng)建并持有對象”,而不是我自己創(chuàng)建的對象,但我也要用到它,所以我就要“借用一下(retain)”,這個步驟被稱為“持有對象”。不論是不是我創(chuàng)建的,只要我不用它了,我就應該釋放我對這個對象的持有權,即“釋放持有權”,當所有人都不要這個對象了(沒有人再有該對象持有權),我們就要釋放掉它。
文章水平有限,如有錯誤還請大家指正。
若需轉載請注明出處。