前言
時間已經過去一年多了,每一次在地鐵上讀這本書都有新的體會和心得.所以在這做一下深層次的分享,讓大家對iOS內存管理這塊有更加深入的了解.
NSObject類解析
NSObject是Objective-C所有類的基類.這里我們就深入了解一下NSObject的內存相關知識內容.我們都知道NSObject是通過引用計數來決定對象是否需要被釋放的,在<>這本書中是通過GNUstep來闡述說明NSObject的alloc方法的內部實現的,我們都知道每一個OC對象中都有一個retainCount屬性來記錄引用計數.我們看一下簡化的NSObject的內部實現.
struct?obj_layout{
????NSUInteger?retainded;
};
+(id)alloc{
????int?size?=?sizeof(struct?obj_layout)?+?對象所占內存大小;
????struct?obj_layout?*p?=?(struct?obj_layout?*)calloc(1,?size);
????return?(id)(p+1);
}
在上面的代碼中我們可以看到alloc內部總共做了兩部分的工作,一個是先計算出頭部obj_layout以及自身所占有多少空間,然后在內存之中通過calloc函數開辟一個大小為size的連續(xù)空間.alloc返回的id值為對象本身的指針(非obj_layout的指針).整體如下圖所示.
對于retainCount引用計數這一屬性,在GNUstep是做了如下的實現的.
-(NSUInteger)retainCount{????
????return?NSExtraRefCount(self)+1;
}
inline?NSUInteger?NSExtraRefCount(id?anObject){????
????return?((struct?obj_layout?*)anObject)[-1].retainded;????
}
在圖1-8中我們知道alloc返回的指針是指向對象的頭部的,并不是指向struct obj_layout這個結構體的,所以我們想要通過對象本身的指針減去struct obj_layout結構體的大小的地址就是指向struct obj_layout的指針,如下圖所示.
接下來我們分別看一下GNUstep中的retain、release、delloc的實現由是怎么樣的.
retain的實現
-(instancetype)retain{
????NSIncrementExtraRefCount(self);
????return?self;
}
inline?void?NSIncrementExtraRefCount(id?anObject){
????if?(((struct?obj_layout?*)anObject)[-1].retainded?==?UINT_MAX?-?1)?{
????????[NSException?raise:NSInternalInconsistencyException?format:@"NSIncrementExtraRefCount()?asked?to?increment?too?far"];
????}
????((struct?obj_layout?*)anObject)[-1].retainded++;
}
其中NSIncrementExtraRefCount()函數保證了retainded變量不會超出最大值,當超出的時候就會發(fā)生異常,實際過程中很少會發(fā)生這種異常,通常我們只是執(zhí)行retainded計數加1的操作.同樣的release實現過程比較類似.
release的實現
-(void)release{
????if?(NSDecrementExtraRefCountWasZero(self))?{
????????[self?dealloc];
????}
}
BOOL?NSDecrementExtraRefCountWasZero(id?anObject){
????if?(((struct?obj_layout?*)anObject)[-1].retainded?==?0)?{
????????return?YES;
????}else{
????????((struct?obj_layout?*)anObject)[-1].retainded--;
????????return?NO;
????}
}
在NSDecrementExtraRefCountWasZero()函數中判斷struct obj_layout 結構體中的retainded變量的值是否為0,如果是0,那么在release方法中就會執(zhí)行對象的dealloc方法,釋放對象.
dealloc的實現
-?(void)dealloc{
????NSDeallocateObject(self);
}
inline?void?NSDeallocateObject(id?anObject){
????struct?obj_layout?*o?=?&((struct?obj_layout?*)anObject)[-1];
????free(o);
}
dealloc的實現就比較簡單了,通過對象指針找到有alloc分配的內存塊.然后釋放.
蘋果實現
上面都是GNUstep中對NSObject類的內存管理的實現,那么蘋果的實現和上述的實現是否一致呢?其實思路是一致的 ,但是蘋果的實現是通過散列表來管理引用計數的.如下圖所示.
我們先看一下簡化的代碼實現.
//核心方法
int?__CFDoExternRefOperation(uintptr_t?op,?id?obj)?{
????CFBasicHashRef?table?=?取得對象對應的散列表(obj);
????int?count;
????switch?(op)?{
????????case?OPERATION_retainCount;
????????????count?=?CFBasicHashGetCountOfKey(table,?obj);
????????????return?count;
????????case?OPERATION_retain:
????????????CFBasicHashAddValue(table,?obj);
????????????return?obj;
????????case?OPERATION_release:
????????????count?=?CFBasicHashRemoveValue(table,?obj);
????????????return?0?==?count;
????}
}
//調用方法
-?(NSUInteger)retainCount?{??
?????return?(NSUInteger)?__CFDoExternRefOperation(OPERATION_retainCount,?self);??
}??
-?(id)retain?{??
?????return?(id)__CFDoExternRefOperation(OPERATION_retain,?self);??
}??
-?(void)release?{??
?????return?__CFDoExternRefOperation(OPERATION_release,?self);??
}
那么使用散列表和把引用計數保存在對象占用的內存頭部到底有什么優(yōu)勢呢?
通過內存塊頭部管理引用計數的好處:
少量代碼即可實現.
能夠統一管理引用計數用內存塊與對象用內存塊.
通過引用計數表管理引用計數的好處:
對象用內存塊的分配無需考慮內存塊頭部.
引用計數表格記錄中存有內存塊地址,可從各個記錄追溯到各對象的內存塊.
我們發(fā)現上面的說的好像也沒有什么優(yōu)勢,其實不然,假定對象的內存塊損壞,我們仍然可以通過散列表來確定各內存塊的位置,但是通過內存塊頭部管理引用計數的方式卻不行.
循環(huán)引用
循環(huán)引用問題算是老生常談的問題,但是我們只是知道兩個對象相互持有會產生循環(huán)引用,自身持有自己會產生循環(huán)引用,卻不明白其中的邏輯關系,下面我們就梳理一下是如何造成的循環(huán)引用的.
首先我們定義一個Test對象.
#import
@interface?Test?:?NSObject
{
????id?__strong?obj_;
}
-(void)setObject:(id?__strong)obj;
@end
#import?"Test.h"
@implementation?Test
-(instancetype)init{
????self?=?[super?init];
????return?self;
}
-(void)setObject:(id)obj{
????obj_?=?obj;
}
@end
然后我們自己創(chuàng)造一個循環(huán)引用的例子.
{
????id?test0?=?[[Test?alloc]init];
????id?test1?=?[[Test?alloc]init];
????[test0?addObject:test1];
????[test1?addObject:test0];
}
然后我們具體分析一下上面是如何造成循環(huán)引用的.
{
????id?test0?=?[[Test?alloc]init];/*對象A*/
????/*指針test0持有Test對象A的強引用*/
????id?test1?=?[[Test?alloc]init];/*對象B*/
????/*指針test1持有Test對象B的強引用*/
????[test0?addObject:test1];
????/*指針test0的obj_成員變量持有持有Test對象B的強引用.
?????*此時,持有對象B的強引用為Test對象A的obj_和test1;
?????*/
????[test1?addObject:test0];
????/*指針test1的obj_成員變量持有持有Test對象A的強引用.
?????*此時,持有對象A的強引用為Test對象B的obj_和test0;
?????*/
}
????/*
?????*??test0變量超出其作用域,強引用失效,所以自動釋放Test對象A.
?????*
?????*??test1變量超出其作用域,強引用失效,所以自動釋放Test對象B.
?????*
?????*??此時,持有Test對象A的強引用的變量為Test對象B的obj_;
?????*
?????*??此時,持有Test對象B的強引用的變量為Test對象A的obj_;
?????*?????
?????*??發(fā)生內存泄漏.
?????*/
上面是兩個對象之間的循環(huán)引用,相對的自身引用自身造成的循環(huán)引用是一樣的.比如下面的例子.
{
????id?test0?=?[[Test?alloc]init];
????[test0?addObject:test0];
}
解決循環(huán)引用的修飾符 :__weak 與__unsafe_unretained
上一個模塊我們了解到什么情況會造成循環(huán)引用從而進一步的造成內存泄漏,接下來我們看如何解決上面的循環(huán)引用問題,我們知道有強引用必然有弱引用,強引用表示持有某個對象,那么我們只要不持有某個對象就可以了(持有對象的本質是引用計數的增加,__weak修飾符不會引起引用計數的變化).這個時候我們就需要__weak修飾符了,比如上面的例子我們可以做如下修改就可以解決循環(huán)引用的問題.
#import
@interface?Test?:?NSObject
{
????id?__weak?obj_;
}
-(void)setObject:(id?__strong)obj;
@end
__weak修飾符是在iOS5以上才能使用,在此之前iOS4以及以前我們使用的__unsafe_unretained修飾符,那么這兩者有什么區(qū)別呢?下面我們就舉例說明.
????id?__weak?obj1?=?nil;
????@autoreleasepool{
????????id?__strong?obj0?=?[[NSObject?alloc]init];
????????obj1?=?obj0;
????????NSLog(@"A:?%@",obj1);
????}
????NSLog(@"B:?%@",obj1);
打印結果如下所示.
我們再換成__unsafe_unretained修飾符來進行一下比對.
????id?__unsafe_unretained?obj1?=?nil;
????@autoreleasepool{
????????id?__strong?obj0?=?[[NSObject?alloc]init];
????????obj1?=?obj0;
????????NSLog(@"A:?%@",obj1);
????}
????NSLog(@"B:?%@",obj1);
這時候,在第二個NSLog 程序已經崩掉了.
那么,都是可以解決循環(huán)引用的兩個修飾符,是什么造成這種差異呢?這是因為__weak修飾符有個優(yōu)點:
通過__weak修飾符持有對象的弱引用是,若改對象被廢棄,則此弱引用將會自動失效且處于nil被賦值的狀態(tài)(空弱引用),但是__unsafe_unretained修飾符卻沒有這樣的功能,所以造成了懸垂指針,也就是我們常說的野指針(指針指向已經被釋放的內存地址).
ARC中__weak修飾符的實現
我們知道通過<>這本書的67頁的講解,我們了解到__weak修飾符運行機制如下所示.
例如,我們做一下的代碼操作.
{
???id?__weak?obj1?=?obj;
}
通過模擬器的,我們可以得到下述的模擬代碼.
????????id?obj1;
????????objc_initWeak(&obj1,obj);
????????objc_destroyWeak(&obj1);
其中objc_initWeak()函數和 objc_destroyWeak()函數共同調用了objc_storeWeak()這個函數,objc_storeWeak()函數一共有兩個參數,函數把第二個參數的復制對象的地址作為鍵值,將第一參數的附有__weak修飾符的變量的指針注冊到weak表中.如果第二個參數為0,則吧變量的地址從weak表中刪除.所以上面的代碼可以如下表示.
????????id?obj1;
????????obj1?=?0;
????????objc_storeWeak(&obj1,obj);
????????objc_storeWeak(&obj1,0);
那么在釋放對象的時候,釋放誰都不持有的對象的同事,程序的動作是怎么樣的呢?對象是通過objc_release函數來釋放的.
objc_release函數的調用
由于引用計數為0所以執(zhí)行delloc
_objc_rootDealloc
object_dispose
objc_destructInstance
objc_clear_deallocating
其實對象在被廢棄時最后調用的objc_clear_deallocating函數會對__weak修飾的相關變量進行清除操作,步驟如下所示.
從weak表中獲取廢棄對象的地址為鍵值的記錄.
將包含在記錄中的所有附有__weak修飾符變量的地址(指針),賦值為nil.
從weak表中刪除該記錄.
從引用計數表中刪除廢棄對象的地址為鍵值的記錄.
通過上面步驟,我們就可以知道__weak修飾符變量為什么會在所引用的對象被廢棄時變?yōu)閚il,可是由于__weak修飾符修飾的變量的廢棄需要對weak表進行操作.所以如果大量使用附有__weak修飾符的變量,那么會增加對CPU的壓力.
結束
本篇并非是iOS 與OS X多線程和內存管理的第一章的全部內容,我只是挑選幾個日常容易碰到的知識點做了一下分享,比如__autoreleasing修飾符我這里都沒有說到.當然了,現在的ARC環(huán)境越來越好,所以有些知識點我們都可能用不到,大家在這里做一下了解即可.