iOS | 內存管理(MRC和ARC的區別以及實現)

自動引用計數(ARC)是指內存管理中對引用採取自動計數的技術。

使用ARC,就無需再次鍵入retain或者release代碼,這降低了程序崩潰,內存泄漏等風險的同時,很大程度上減少了開發程序的工作量。ARC技術使得編譯器清楚目標對象,並能立刻釋放那些不再被使用的對象。如此一來,應用程序將具有可預測性,且能流暢運行,速度也將大幅提升。

MRC

人工引用計數(Manual Reference Counting)

內存管理的思考方式:

  • 自己生成的對象,自己持有
  • 非自己生成的對象,自己也能持有
  • 不需要自己持有的對象時釋放
  • 無法釋放非自己持有的對象
對象操作 oc方法
生成並持有對象 alloc/new/copy/mutableCopy 等方法
持有對象 retain 方法
釋放對象 release 方法
廢棄對象 dealloc 方法
自己生成的對象,自己持有
  • alloc
  • new
  • copy
  • mutablecopy
id obj = [[NSObject alloc] init];
id obj = [NSObject new];
// 兩者完全一致,生成並持有對象

// NSCopying和NSMutableCopying

非自己生成的對象,自己也能持有
id obj = [NSMutableArray array];
// 取得的對象存在,但自己不持有
[obj retain];
// 自己持有對象
不再需要自己持有的對象時釋放

自己持有的對象,一旦不再需要,持有者有義務釋放該對象。

id obj = [NSMutableArray array];
// 取得的對象存在,但自己不持有
[obj retain];
// 自己持有對象
[obj release];
// 釋放對象
// 對象不可再被訪問

array方法的實現

- (id) object
{
	id obj = [[NSObject alloc] init];
	// 自己持有對象
	[obj autorelease];
	// 釋放後取得的對象存在,但自己不持有該對象
	return obj;
}

release方法時調用後立即釋放,而autorelease方法則是不立即釋放而是註冊到autoreleasepool中,在超出指定的生存範圍時能夠自動並正確釋放(調用release)。

id obj1 = [obj0 object];
// 取得對象存在但不持有
[obj1 retain];
// 自己持有對象
無法釋放非自己持有的對象
  • 已經釋放過的對象再一次釋放。
id obj = [NSObject alloc] init];
[obj release];
[obj release];
  • 自己還未持有的對象進行釋放
id obj1 = [obj0 object];
[obj1 release];

alloc/retain/release/dealloc的實現

id obj = [NSObject alloc];
+(id)alloc
{
	return [self allocWithZone: NSDefaultMallocZone()];
}

+(id)allocWithZone:(NSZone*)z
{
	return NSAllocateObject(self, 0, z);
}


//
struct obj_layout
{
    NSUInteger retained;
};

inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
    int size = 計算容納對象所需內存大小;
    id new = NSZoneMalloc(zone, self);
    memset(new, o, size);
    new = (id)&((struct obj_layput *)new)[1];
}

NSDefaultMallocZone和NSZoneMalloc中包含的NSZone時防止內存碎片化而引入的結構,對內存分配的區域本身進行多重化管理,根據使用對象的目的,對象的大小分配內存,從而提高了內存管理的效率。

去掉NSZone後代碼簡化版

struct obj_layout
{
    NSUInteger retained;
};

+(id) alloc
{
	int size = sizeof(struct obj_layout) + 對象大小;
	struct obj_layout *p = (struct obj_layout *)calloc(1, size);
	return (id)(p+1);
}

方法中的retain整數用來保持引用計數並將其寫入對象內存頭部。

對象的引用計數通過retainCount來實現

id obj = [[NSObject alloc] init];
[obj retainCount];

- (NSUInteger)retainCount
{
	return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject)
{
    return ((struct obj_layout *)anObject)[-1].retained;
}

通過retain方法可使retained變量加1,通過release方法可使retained變量減1

retain方法的實現:

-(id)retain
{
	NSIncrementExtraRefCount(self);
	return self;
}

inline void NSIncrementExtraRefCount(id anObject)
{
    if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1)
        [NSException raise: NSInternalInconsistencyException format: @"....."];
    ((struct obj_layout *) anObject)[-1].retained++;
}

release方法的實現

- (void)release
{
	if (NSDecrementExtraRefCountWasZero(self))
		[self dealloc];
}

BOOL NSDecrementExtraRefCountWasZero (id anObject)
{
    if ((struct obj_layout *) anObject)[-1].retained == 0) {
        return YES;
    }
    else
    {
        ((struct obj_layout *)anObject)[-1].retained--;
        return NO;
    }
}

dealloc方法的實現

-(void)dealloc
{
	NSDeallocateObject(self);
}

inline void NSDeallocateObject (id anObject)
{
    struct obj_layout *o = &((struct obj_layout *) anObject)[-1];
    free(o);
}
// 廢棄alloc分配的內存塊

實際上蘋果是通過散列表的形式實現引用計數的,散列表的鍵值爲內存塊地址的散列值。

散列表存儲信息爲引用計數以及內存塊地址。

autorelease

autorelease會類似c語言中的自動變量來對待對象實例,超出作用域時則將對象釋放。

{
    int a;
}

autorelease就是類似 { } 的作用,使用方法如下:

  • 生成並持有NSAutoreleasePool對象
  • 調用已分配對象的autorelease實例方法。
  • 廢棄NSAutoreleasePool對象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; //等同於[obj release]

在Cocoa框架中,相當於程序主循環的NSRunLoop或者在其他程序可運行的地方,對NSAutoreleasePool 對象進行生成、持有和廢棄處理。因此開發者不一定非得使用NSAutoreleasePool對象來進行開發工作。

但是在大量產生autorelease對象時,只有不廢棄NSAutoreleasePool對象,那麼生成的對象就不能被釋放,因此可能出現內存不足的情況。例如:

for (int i = 0; i < 圖像數; ++i)
{
    /*
     * 讀入圖像
     * 大量產生autorelease對象
     * 沒有廢棄NSAutoreleasePool 對象
     * 導致最終內存不足
     */
    [pool drain];
    /*
    * 通過drain方法autorelease對象被release
    */
}

Cocoa框架中也有很多類方法返回autorelease對象,比如NSMutableArray類的arrayWithCapacity類方法。

id array = [NSMutableArray arrayWithCapacity:1];

等同於

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

autorelease的實現

[obj autorelease]

-(id)autorelease
{
	[NSAutoreleasePool addObject:self];
}
+(void)addObject:(id)anObj
{
	NSAutorelease *pool = 取得正在使用的NSAutorelease對象;
	if (pool != nil)
    {
    	[pool addObject:anObj];
    }
    else
    {
    	NSLog(@"AutoreleasePool 對象非存在狀態下調用");
    }
}

如果嵌套生成或持有多個NSAutoreleasePool對象,則會使用最內側的對象。

[pool drain]

- (void)drain
{
	[self dealloc];
}

- (void) dealloc
{
	[self emptyPool];
	[array release];
}

- (void) emtpyPool
{
	for (id obj in array)
    {
    	[obj release];
    }
}

ARC

與MRC的引用計數式內存管理在本質上沒有太大變化,只是自動幫我們處理引用計數的部分。

通過__strong修飾符可以不必再次鍵入retain或者release,自動地實現了上述的四種內存管理思考方式。

但引用計數式內存管理會發生"循環引用"的問題。

{
    id test0 = [[Test alloc] init]; /* 對象A */
    /*
     * test0 持有Test對象A的強引用
     */
    
    id test1 = [[Test alloc] init]; /* 對象B */
    /*
     * test1 持有Test對象B的強引用
     */
    [test0 setObject:test1];
    /*
     * 此時持有Test對象B的強引用變量爲test1和對象A的obj_成員變量
     */
    [test1 setObject:test0];
    /*
     * 此時持有Test對象A的強引用變量爲test0和對象B的_obj變量
     */
}
/*
* 因爲test0變量超出其作用域,強引用失效,所以自動釋放對象A
*
* 因爲test1變量超出其作用域,強引用失效,所以自動釋放對象B
*
* 此時持有對象A的強引用變量爲對象B的_obj,持有對象B的強引用變量爲對象A的_obj
*
* 發生內存泄漏!(應當廢棄的對象在超出其生命週期後依然存在)
*/

同時像下面的情況,雖然只有一個對象,但對象持有其自身時也會發生循環引用。

id test = [[Test alloc] init];
[test setObject:test];

使用弱引用__weak可以實現,弱引用不持有對象,只是持有該對象的強引用,如果該對象的所有強持有者都釋放,則對象廢棄,即使此時仍有弱持有者。

_unsafe_unretained 和weak的不同之處在於前者在賦值給帶有strong修飾符的變量必須確保被賦值對象確實存在,不然其不會像weak一樣被設置爲nil而是造成懸浮指針。(存在是歷史遺留問題,有了weak後基本不用它了)

strong修飾符類似於c++中的std::shared_ptr指針,而weak修飾符類似於c++中的std::weak_ptr,在c++中沒有strong,weak強烈推薦使用這兩個指針。

ARC,MRC區別

說了這麼多總結一下ARC,MRC的區別吧。MRC類似於c++中的普通指針,程序猿要手動的進行生成持有釋放廢棄操作,但是可以將其註冊到autoreleasepool中(類似c++的作用域),在作用域消失時也就是autoreleasepool對象釋放時會釋放所有註冊在它裏面的對象。而ARC則是類似於c++的智能指針,不需要顯示的對對象實例進行釋放,出現了strong,weak等修飾符,採用自動引用計數的方法,使得當一個對象實例在強引用計數爲0時,則廢棄這個對象實例,釋放其所佔的內存塊。

ARC的規則:

  • 在ARC有效時編譯代碼一定要遵守以下規則,
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 須遵守內存管理的命名規則
    • init返回的必須是實例對象
  • 不要顯式地調用dealloc
  • 使用@autorelease塊代替NSAutoreleasePool
  • 不能使用NSZone
  • 對象型變量不能作爲C語言結構體成員
  • 顯式轉換id和void*
    • 不能強制轉換
    • 如果只是想單純地賦值可以通過__bridge轉換,但可能造成懸空指針問題,不推薦。

ARC中的屬性:

屬性聲明 所有權修飾符
assign _unsafe__unretained修飾符
copy __strong修飾符
retain __strong修飾符
strong __strong修飾符
unsafe_unretained _unsafe__unretained修飾符
weak __weak修飾符
  • **assign:**對應到__unsafe_unretained, 表明setter僅做賦值,不增加對象的引用計數,用於基本數據類型

  • __weak修飾符賦值後不會註冊到autoreleasepool中,只會在使用時註冊,而且每次使用都會註冊一次,在autoreleasepool結束後會全部釋放。

  • **copy:**對應到__strong,但是賦值操作比較特殊:賦值時進行copy而非retain操作,原來的值可變則深拷貝,不可變則淺拷貝。

ARC的實現

__strong

先看如下一段代碼:

{
// ARC中默認會在對象前添加一個修飾符__strong
id obj = [[NSObject alloc] init];
//<==>等價於
id __strong obj = [[NSObject alloc] init];
}

根據runtime特性,它的實際調用如下:

{
// 消息轉發
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 編譯器在obj作用域結束時自動插入release
objc_release(obj);
}

當然這裏是以alloc/new/copy/mutableCopy生成的對象,這種對象會被當前的變量所持有,引用計數會加1.那如果不是用被持有的方式生成對象呢?
看下面這段代碼:

{
id obj = [NSMutableArray array];
}

這種方式生成的對象不會被obj持有,通常情況下會被註冊到autoreleasepool中.但也有特殊情況,上面的代碼可以轉換成如下代碼:

{
// 消息轉發
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 調用objc_retainAutoreleasedReturnValue函數
objc_retainAutoreleasedReturnValue(obj);
// 編譯器在obj作用域結束時自動插入release
objc_release(obj);
}

這裏介紹兩個相關函數:

  • objc_retainAutoreleasedReturnValue():這個函數的作用是返回註冊在autoreleasepool當中的對象.
  • objc_retainAutoreleaseReturnValue():這個函數一般是和objc_retainAutoreleasedReturnValue()成對出現的.目的是註冊對象到autoreleasepool中.但不僅限於此.
    爲何說不僅限於此呢?原因在於,objc_retainAutoreleaseReturnValue()函數在發現對象調用了方法或者函數之後又調用了objc_retainAutoreleasedReturnValue(),那麼就不會再把返回的對象註冊到autoreleasepool中了,而是直接把對象傳遞過去.
    這樣的好處顯而易見:不用再去autoreleasepool中取出對象,傳遞出去,而是越過autoreleasepool直接傳遞,提升了性能.

__weak

weak修飾符想必大家都非常熟悉,它有一個衆所周知的特性:用weak修飾的對象在銷燬後會被自動置爲nil.另外還補充一點:凡是用weak修飾過的對象,必定是註冊到autoreleasepool中的對象.
看下面的代碼:

{
// obj默認有__strong修飾
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}

實際過程如下:

{
// 省略obj的實現
id obj1;
// 通過objc_initWeak初始化變量
objc_initWeak(&obj1,obj);
// 通過objc_destroyWeak釋放變量
objc_destroyWeak(&obj1);
}
  • objc_initWeak()函數的作用是將obj1初始化爲0,然後將obj作爲參數傳遞到這個函數中objc_storeWeak(&obj1,obj)
  • objc_destroyWeak()函數則將0作爲參數來調用:objc_storeWeak(&obj1,0)
  • objc_storeWeak()函數的作用是以第二個參數(obj || 0)作爲key,第一個參數(&obj1)作爲value,將第一個參數的地址註冊到weak表中.當key爲0,即從weak表中刪除變量地址.

那麼weak表中的對象是如何被釋放的呢?

  • 從weak表中獲取廢棄對象的鍵值記錄.
  • 將記錄中所有包含__weak的變量地址,賦值爲nil.
  • 從weak表中刪除該記錄.
  • 從引用計數表中刪除對應的記錄.

這就是__weak修飾的變量會在釋放後自動置爲nil的原因.同時,因爲weak修飾之後涉及到註冊到weak表等相關操作,如果大量使用weak可能會造成不必要的CPU資源浪費,所以書裏指出儘量在循環引用中使用weak.
這裏不得不提到另外一個和__weak相近的屬性:__unsafe_unretained,它與weak的區別在於,釋放對象後不會對其置爲nil,在某些特定的場合下,需要延遲釋放的時候,可以考慮用這個屬性修飾.

好了,下一個問題,看如下代碼:

{
id __weak obj1 = obj;
// 這裏使用了obj1這個用weak修飾的變量
NSLog(@"%@",obj1);
}

在weak變量被使用的情況下,實際過程如下:

{
id obj1;
objc_initWeak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
}

從這段實現代碼中我們可以看出如下幾點:

  • 當我們使用weak修飾的對象時,實際過程中產生了一個tmp對象,因爲objc_loadWeakRetained()函數會從weak表中取出weak修飾的對象,所以tmp會對這個取出的對象進行一次強引用.
  • 因爲上述原因,weak修飾的對象在當前變量作用域結束前都可以放心使用.
  • objc_autorelease()會將tmp對象也註冊到autoreleasepool中.所以當大量使用weak對象的時候,註冊到autoreleasepool的對象會大量增加.解決方案就是用一個__strong修飾的臨時變量來使用.
{
id __weak obj1 = obj;
id tmp = obj1;
// 後面使用tmp即可
}

延伸一下:爲什麼有循環引用block內用weakObject的時候最好能在block內套一層strongObject?

  • 在異步線程中weakObject可能會被銷燬,所以需要套一層strong.
  • 如果內部有耗時的循環語句,頻繁使用weakObject也會增加內存損耗.

!!! 爲什麼訪問weak修飾的對象就會訪問註冊到自動釋放池的對象呢?

因爲weak不會引起對象的引用計數器變化,因此,該對象在運行過程中很有可能會被釋放。所以,需要將對象註冊到自動釋放池中並在autoreleasePool銷燬時釋放對象佔用的內存。

__autoreleasing

它的主要作用就是將對象註冊到autoreleasepool中.沒啥好說的.

最後補充幾種在ARC環境下獲取引用計數的方法,但並不一定準確:ARC的一些引用計數優化,以及多線程的中的競態條件問題,有興趣的可以自己去了解一下.

(1) 使用_objc_rootRetainCount()私有函數
OBJC_EXTERN int _objc_rootRetainCount(id);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",_objc_rootRetainCount(obj));
}
@end

(2) 使用KVC
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",[[obj valueForKey:@"retainCount"] integerValue]);
}
@end

(3) 使用CFGetRetainCount()
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
@end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章