前言
從我開始學習iOS的時候,身邊的朋友、網上的博客都告訴我iOS的內存管理是依靠引用計數的,然后說引用計數大于1則對象保存在內存的堆中而引用計數等于0則對象銷毀。然后又說在所謂的ARC時代,強指針指向一個對象,則對象不銷毀;一個對象沒有任何一個強指針指向則銷毀....,最后,我想說這些都很有道理的樣子,但是,我還是不清楚為什么引用計數器為0為什么會被銷毀,為什么一個對象沒有強指針指向就會銷毀,為什么在@property中一個OC對象要使用strong進行修飾 .... 。所以,在學習Objective-C高級編程:iOS與OS X多線程和內存管理后,讓我明白了很多事情。以下是對于這本書里面知識的總結性內容,如果要詳細了解,請閱讀該書籍。
注意:下面的內容是適合于已經對于iOS內存管理有一定了解的程序員
內存管理的思考方式
自己生成的對象,自己持有
非自己生成的對象,自己也能持有
不再需要自己持有對象時釋放
非自己持有的對象無法釋放
1) 自己生成的對象,自己持有
在iOS內存管理中有四個關鍵字,alloc、new、copy、mutableCopy,自身使用這些關鍵字產生對象,那么自身就持有了對象
// 使用了alloc分配了內存,obj指向了對象,該對象本身引用計數為1,不需要retainidobj = [[NSObjectalloc] init];// 使用了new分配了內存,objc指向了對象,該對象本身引用計數為1,不需要retainidobj = [NSObjectnew];
2) 非自己生成的對象,自己也能持有
// NSMutableArray通過類方法array產生了對象(并沒有使用alloc、new、copy、mutableCopt來產生對象),因此該對象不屬于obj自身產生的// 因此,需要使用retain方法讓對象計數器+1,從而obj可以持有該對象(盡管該對象不是他產生的)idobj = [NSMutableArrayarray];? ? [objretain];
3) 不再需要自己持有對象時釋放
idobj = [NSMutableArrayarray];? ? ? [objretain];// 當obj不在需要持有的對象,那么,obj應該發送release消息[obj release];
4) 無法釋放非自己持有的對象
// 1. 釋放一個已經釋放的對象id obj = [[NSObject alloc] init];// 已經釋放對象[objrelease];// 釋放了對象還進行釋放[objrelease];// 2. 釋放一個不屬于自己的對象id obj1 = [obj object];// obj1沒有進行retain操作而進行release操作,使得obj持有對象釋放,造成了野指針錯誤[obj1release];
如上為iOS進行內存管理的四種思考方式(記住不論是ARC還是MRC都遵循該思考方式,只是ARC時代這些工作讓編譯器做了)
引用計數器討論
蘋果對于引用計數的管理是通過一張引用計數表進行管理的
引用計數表.png
我們平常在操作對象的引用計數器時,其實就是對這個引用計數表進行操作,在獲取到該表的地址以及相應對象的內存地址,就可以通過對象的內存從該表中進行索引獲取到相應的引用計數值,然后根據用戶的操作來返回計時器、計時器加1、計時器減1,下面就深入討論retain、release、alloc、dealloc具體怎么操作該引用計數表
alloc
當我們調用alloc函數時我們進一步會調用allocWithZone方法
idobj = [[NSObjectalloc] init];? ? + (id)alloc {return[selfallocWithZone:NSDefaultMallocZone()];? ? }? ? + (id)allocWithZone:(NSZone*)z {returnNSAllocateObject(self,0,z);? ? }
調用NSAllocateObject函數對內存進行分配
retain、release、retainCount
該書籍對于這三個函數調用先是使用GNUstep(一個Cocoa框架的互換框架,功能類似)進行講解,后來又講解了蘋果對于引用計數的實現。在這里我們就討論蘋果的實現了。
調用retain、release、retainCount時函數調用順序:
retain、retainCount、release函數調用順序.png
如下所示,調用各個函數時會調用__CFDoExternRefOperation函數,該函數包含于CFRuntime.c中,該函數簡化代碼如下:
- (NSUInteger)retainCount {return(NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self);}- (id)retain{return(id)__CFDoExternRefOperation(OPERATION_retain,self);}- (void)release {return__CFDoExternRefOperation(OPERATION_release,self);}
int__CFDoExternRefOperation(uintptr_r op,id obj) {? ? ? ? CFBasicHashRef table = 取得對象對應的散列表(obj);intcount;switch(op) {caseOPERATION_retainCount:count= CFBasicHashGetCountOfKey(table,obj);returncount;caseOPERATION_retain:? ? ? ? ? ? ? ? CFBasicHashAddValue(table,obj);returnobj;caseOPERATION_release:count= CFBasicHashRemoveValue(table,obj):return0==count;? ? ? ? }? ? }
代碼如上所示,可以想象蘋果就是使用類似于上述的引用計數表來管理內存,也就是說我們在調用retain、retainCount、release時首先調用__CFDoExternRefOperation進而獲取到引用技術表的內存地址以及本對象的內存地址,然后根據對象的內存地址在表中查詢獲取到引用計數值。
若是retain就加1
若是retainCount就直接返回值,
若是release則減1而且在CFBasicHashRemoveValue中將引用計數減少到0時會調用dealloc,從而調用NDDeallocateObject函數、free函數將對象所在內存釋放
以上就是在討論蘋果對于引用計數的管理方法,對于GNUStep辦法請自行查閱書籍
autorelease
作用:將對象放入自動釋放池中,當自從釋放池銷毀時對自動釋放池中的對象都進行一次release操作
書寫形式:
NSAutoreleasePool *pool =[[NSAutoreleasePool alloc]init];? ? id obj =[[NSObject alloc]init];[obj autorelease];[pool drain];
對于autorelease的實現方式,書籍也對比了GNUSetp與蘋果實現的方式,現在通過GNUStep源代碼來理解蘋果的實現
1) GNUStep實現
id obj =[[NSObject alloc]init];[obj autorelease];
- (id)autorelease {? ? ? ? [NSAutoreleasePooladdObject:self];? ? }
+ (void)addObject:(id)anObject {NSAutoreleasePool*pool = 取得正在使用的Pool對象;if(pool !=nil) {? ? ? ? ? ? [pool addObject:anObject];? ? ? ? }else{NSLog(@"NSAutoreleasePool非存在狀態下使用Pool對象");? ? ? ? }? ? }
- (void)addObject:(id)anObject {? ? ? ? [arrayaddObject:anObject];? ? }
從上面可以看出,自動釋放池就是通過數組完成的,我們在調用autorelease時最終就是將本對象添加到當前自動釋放池的數組
而針對于自動釋放池銷毀時對數組中的進行一次release操作,見下面
NSAutoreleasePool *pool =[[NSAutoreleasePool alloc]init];? ? ...? ? // 當自動釋放池銷毀時[pool drain];
- (void)drain {? ? ? ? [self dealloc];? ? }? ? - (void)dealloc {? ? ? ? [self emptyPool];? ? ? ? [arrayrelease];? ? }? ? - (void)emptyPool {for(id obj inarray) {? ? ? ? ? ? [objrelease];? ? ? ? }? ? }
2) 蘋果的實現
classAutoreleasePoolPage? ? {staticinlinevoid*push(){? ? ? ? ? ? 相當于生成或持有NSAutoreleasePool類對象? ? ? ? }staticinlinevoid*pop(void*token){? ? ? ? ? ? 相當于廢棄NSAutoreleasePool類對象? ? ? ? ? ? releaseAll();? ? ? ? }staticinlineidautorelease(id obj){? ? ? ? ? ? 相當于NSAutoreleasePool類的addObject類方法? ? ? ? ? ? ? AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage實例;? ? ? ? ? ? autoreleasePoolPage->add(obj);? ? ? ? }id *add(id obj){? ? ? ? ? ? 將對象追加到內部數組中? ? ? ? }voidreleaseAll(){? ? ? ? ? ? 調用內部數組中對象的release實例方法? ? ? ? }? ? };void*objc_autoreleasePoolPush(void){returnAutoreleasePoolPage::push();? ? }voidobjc_autoreleasePoolPage(void*ctxt){? ? ? ? AutoreleasePoolPage::pop(ctxt);? ? }id *objc_autorelease(id obj){returnAutoreleasePoolPage::autorelease(obj);? ? }
如上所示,蘋果內部使用了類似于GNUStep中的思想,將對象添加進數組進行管理
ARC中內存管理方式
介紹:
關于這部分的內存,作者是分了兩部分進行討論,第一部分介紹ARC管理所需要的關鍵字strong 、weak、unsafe_unretained、autoreleasing的作用;第二部分介紹了ARC針對于這些關
鍵字的具體內管管理實現方式。下面我們就綜合兩部分的內容進行一次討論
蘋果官方文檔說ARC是有"編譯器自行進行管理",但事實上僅僅是編譯器是不夠,需要滿足下面啷個條件
clang(LLVM編譯器)3.0以上
objc4 Objective-C運行時庫493.9以上
__strong
作用
id__strongobj = [[NSObjectalloc]init];
如上代碼,表示obj這個強指針指向NSObject對象,且NSObject對象的引用計數為1
id__strongobj1 = obj;
如上代碼,表示obj1這個強指針與obj指針指向同一個NSObject對象,且NSObject對象的引用計數為2
id__strongobj = [NSMutableArrayarray];
如上代碼,表示obj這個強指針指向的NSMutableArray對象的引用計數為1
綜上所示,當一個對象被強指針指向則引用計數就加1,否則,該對象沒有一個強指針指向則自動釋放內存
那么問題來了,為什么一個對象被強指針指向引用計數就加1呢? 為什么分配在堆里面的對象內存能夠自動釋放內存?
原理
第一種情況: 對象是通過alloc、new、copy、multyCopy來分配內存的
id__strongobj = [[NSObjectalloc] init];
當使用alloc、new、copy、multyCopt進行對象內存分配時,強指針直接指向一個引用計數為1的對象,在編譯器作用下,上述代碼會轉換成以下代碼
id obj = objc_msgSend(NSObject,@selector(alloc));? ? objc_msgSend(obj,@selector(init));// 當讓這個代碼會在合適的時候被調用,不是馬上調用objc_release(obj);
第二種情況: 對象不是自身生成,但是自身持有(一般這樣的對象是通過除alloc、new、copy、multyCopy外方法產生的)
id__strongobj = [NSMutableArrayarray];
在這種情況下,obj也指向一個引用計數為1的對象內存,其在編譯器下轉換的代碼如下:
idobj = objc_msgSend(NSMutableArray,@selector(array));// 代替我們調用retain方法,使得obj可以持有該對象objc_retainAutoreleasedReturnValue(obj);? ? objc_release(obj);
從而使得obj指向了一個引用計數為1的對象, 不過,objc_retainAutoreleaseReturnValue有一個成對的函數objc_autoreleaseReturnValue,這兩個函數可以用于最優化程序的運行
如下代碼:
+ (id)array? ? {return[[NSMutableArrayalloc] init];? ? }
代碼轉換如下:
+ (id)array? ? {? ? ? ? id obj = objc_msgSend(NSMutableArray,@selector(alloc));? ? ? ? objc_msgSend(obj,@selector(init));// 代替我們調用了autorelease方法returnobjc_autoreleaseReturnValue(obj);? ? }
在轉換后的代碼,我們可以看見調用了objc_autoreleaseReturnValue函數且這個函數會返回注冊到自動釋放池的對象,但是,這個函數有個特點,它會查看調用方的命令執行列表,如果發現接
下來會調用objc_retainAutoreleasedReturnValue則不會返回注冊到自動釋放池的對象而僅僅返回一個對象而已。
兩者的關系圖如下:
關系圖.png
通過這些,我們就可以通知為什么強指針指向一個對象,這個對象的引用計數就加1
__weak
作用
id__weakobj = [[NSObjectalloc] init];
根據我們的知識,可以知道NSObject對象在生成之后立馬就會被釋放,其主要原因是weak修飾的指針沒有引起對象內部的引用計數器的變化
因此,weak修飾的指針常用于打破循環引用或者修飾UI控件,關于__weak修飾的指針引用場景這里不敘述,下面主要介紹其原理
原理
我們知道弱指針有兩個作用:一. 修飾的指針不會引起指向的對象的引用計數器變化 二. 當指向的對象被銷毀時,弱指針全部置為nil, 那么除了這些之外,我們還有一個要說的就是,為什么我們
在程序中不能頻繁的使用weak呢?
1) 為什么弱指針不會引起指向的對象的引用計數器發生變化
id__weakobj = [[NSObjectalloc] init];
編譯器轉換后的代碼如下:
id obj;id tmp = objc_msgSend(NSObject,@selector(alloc));objc_msgSend(tmp,@selector(init));objc_initweak(&obj,tmp);objc_release(tmp);objc_destroyWeak(&object);
對于__weak內存管理也借助了類似于引用計數表的表,它通過對象的內存地址做為key,而對應的指針作為value進行管理,在上述代碼中objc_initweak就是完成這部分操作,而objc_destroyWeak
則是銷毀該對象對應的value。所以,weak在修飾只是讓weak表增加了記錄沒有引起引用計數表的變化
2) 當弱指針指向的對象唄銷毀時,弱指針怎么才能自動置為nil? 為什么我們在程序中不能頻繁使用weak呢
對象通過objc_release釋放對象內存的動作如下:
objc_release
因為引用計數為0所以執行dealloc
_objc_rootDealloc
objc_dispose
objc_destructInstance
objc_clear_deallocating
而在對象被廢棄時最后調用了objc_clear_deallocating,該函數的動作如下:
1) 從weak表中獲取已廢棄對象內存地址對應的所有記錄
2)將已廢棄對象內存地址對應的記錄中所有以weak修飾的變量都置為nil
3)從weak表刪除已廢棄對象內存地址對應的記錄
4)根據已廢棄對象內存地址從引用計數表中找到對應記錄刪除
據此可以解釋為什么對象被銷毀時對應的weak指針變量全部都置為nil,同時,也看出來銷毀weak步驟較多,如果大量使用weak的話會增加CPU的負荷
而不建議大量使用weak,還有一個原因看下面的代碼:
id__weakobj1 = obj;NSLog(@"obj2-%@",obj1);
編譯器轉換上述代碼如下:
id obj1;? ? objc_initweak(&obj1,obj);// 從weak表中獲取附有__weak修飾符變量所引用的對象并retainid tmp = objc_loadWeakRetained(&obj1);// 將對象放入自動釋放池objc_autorelease(tmp);? ? NSLog(@"%@",tmp);? ? objc_destroyWeak(&obj1);
據此當我們訪問weak修飾指針指向的對象時,實際上是訪問注冊到自動釋放池的對象。因此,如果大量使用weak的話,在我們去訪問weak修飾的對象時,會有大量對象注冊到自動釋放池,這會影響程
序的性能。推薦方案 : 要訪問weak修飾的變量時,先將其賦給一個strong變量,然后進行訪問
最后一個問題: 為什么訪問weak修飾的對象就會訪問注冊到自動釋放池的對象呢?
因為weak不會引起對象的引用計數器變化,因此,該對象在運行過程中很有可能會被釋放。所以,需要將對象注冊到自動釋放池中并在自動釋放池銷毀時釋放對象占用的內存。
__unsafe_unretained
作用
unsafe_unretained作用需要和weak進行對比,它也不會引起對象的內部引用計數器的變化,但是,當其指向的對象被銷毀時unsafr_unretained修飾的指針不會置為nil。而且一般__unsafe_unretained就和它的名字一樣是不安全,它不納入ARC的內存管理
__autoreleasing
作用
ARC無效
NSAutoreleasePool *pool =[[NSAutoreleasePool alloc]init];? ? id obj =[[NSObject alloc]init];[obj autorelease];[pool drain];
ARC有效*
id __autoreleasing obj1 = obj;
如上所示,通過__autoreleasing修飾符就完成了ARC無效時一樣的功能
當然,在某一些情況下我們不通過顯式指定__autoreleasing關鍵字就可以完成自動注冊到自動釋放池的功能,例如以下情況
第一種:
@autoeleasepool {// 如果看了上面__strong的原理,就知道實際上對象已經注冊到自動釋放池里面了id__strongobj = [NSMutableArrayarray];? ? }
第二種:
訪問__weak修飾的對象時,對象就被注冊到了自動釋放池
第三種:
以下形式的默認修飾符是__autorelease
id *obj;
NSObject **obj;
同時,也引出一個問題: 為什么在@property中OC對象使用strong而基本數據類型使用assign?
屬性默認修飾符.png
從表中可以推斷出,在ARC在OC對象的默認修飾符是strong,因此,在@property中使用strong
而基本數據類型是不納入到ARC內存管理中的,unsafe_unretained也不歸ARC管,因此,使用assign對基本數據類型進行修飾
原理 ```objc @autoreleasepool {
id __autoreleasing obj =[[NSObject alloc]init];}
代碼轉換如下:? ```objc? ? id pool = objc_autoreleasePoolPush();id obj = objc_msgSend(NSObject,@selector(alloc));objc_msgSend(obj,@selector(init));objc_autorelease(obj);objc_autoreleasePoolPop(pool);
@autoreleasepool{id__autoreleasing obj = [NSMutableArrayarray];? ? }
代碼轉換如下:
id pool = objc_autoreleasePoolPush();id obj = objc_msgSend(NSMutableArray,@selector(array));objc_retainAutoreleasedReturnValue(obj);objc_autorelease(obk);objc_autoreleasePoolPop(pool);
上述代碼,代表的就是自身生成并持有對象、自身不生成但也持有對象的兩種__autorelease內存管理情況
ARC規則
不能使用retain、release、retainCount、autorelease方法(如果ARC下使用會出現編譯錯誤)
不能使用NSAllocateObject、NSDeallocateObject函數(如果ARC下使用會出現編譯錯誤)
不要顯式調用dealloc(ARC下,顯式調用dealloc并在代碼中書寫[super dealloc]也會出現編譯錯誤)
使用@autoreleasepool塊代替NSAutoreleasePool
@autoreleasepool{}塊相比較NSAutoreleasePool而言顯得代碼更加整潔、層次性強,而且@autoreleasepool代碼快哉ARC或者非ARC下都是可以使用的
需遵守內存管理命名規則
1) alloc、new、copy、mutableCopy等以這些名字開頭的方法都應當返回調用方能夠持有的對象2)init開頭的方法必須是實例方法并且要返回對象,返回值要是id或者該方法對應類的對象類似或者其超類或者其子類。另外,init開頭的方法也僅僅用作對對象進行初始化操作
不能使用區域(NSZone)
區域是以前為了高效利用內存的使用率而設計的,但是,目前來說ARC下的模式已經能夠有效利用內存,區域在ARC下還是非ARC下都已經被單純的忽略
對象型變量不能作為C語言結構體的成員
OC對象型變量如果成為了C語言結構體的成員,那么,ARC不能掌握該對象的生命周期從而有效管理內存,因此,不能這樣使用。
顯式轉換"id" 和 "void*"
非ARC下:idobj = [[NSObjectalloc] init];void*p = obj;? ? 這樣的代碼是可行的,id和void*可以方便得自由轉化 ,但是,在ARC下是不一樣的? ? ARC下id和void*有三個轉換的關鍵字 __bridge、__bridge_retained、__bridge_transfer:idobj = [[NSObjectalloc] init];void*p = (__bridgevoid*)obj;? ? 注意: __bridge不會引起對象的引用計數變化,因此,安全性不太好。相比較,__bridge_retained不僅僅實現了__bridge的功能而且能讓p調用retain方法使p持有對象。另外,? ? __bridge_transfer也是和release方法類似,使用__bridge_transfer進行轉化,既讓對象p調用一次retain方法,