iOS內(nèi)存管理 - Autorelease 詳解

前言

比較晚入坑iOS的同學(xué)大多沒怎么使用過MRC(Manual Reference Counting),直接享受了ARC的便利,ARC(Automatic Reference Counting)從Xcode4引入,由于ARC下禁止直接調(diào)用retain、release、autorelease等函數(shù),甚至不需要了解autorelease這些函數(shù)就能很好的進(jìn)行iOS開發(fā)。然而,為了知其所以然,還是有必要了解一下底層的原理。

一、 概述

首先明確一點(diǎn),ARC和Autorelease沒有直接的聯(lián)系,在MRC時(shí)代就存在Autorelease:
ARC:代替手動添加內(nèi)存管理函數(shù)進(jìn)行人工內(nèi)存管理(retain、release、autorelease),編譯階段自動的在需要持有對象時(shí)插入retain函數(shù)、需要釋放時(shí)插入release函數(shù)(就是這么智能);
Autorelease:向一個(gè)對象發(fā)送延遲釋放信息,使得這個(gè)對象可以在作用域意外范圍被使用;典型的例子就是將一個(gè)對象作為返回值給調(diào)用者,如果不延遲釋放,這個(gè)返回值在出了所在函數(shù)范圍就被立即釋放,調(diào)用者拿到的永遠(yuǎn)是nil,因?yàn)閕OS的內(nèi)存遵循誰申請誰釋放的原則,當(dāng)向一個(gè)對象發(fā)送了autorelease消息,實(shí)際上就是將該對象放入autoreleasePool池子動,等到延遲到適當(dāng)?shù)臅r(shí)機(jī)(通常是在NSRunloop即將進(jìn)入休眠或者退出時(shí))進(jìn)行釋放(對池子中的每個(gè)對象發(fā)送release消息)。

二、 底層實(shí)現(xiàn)

大家都知道獲取一個(gè)NSMutableArray實(shí)例對象有好多種方式:如[NSMutableArray new]或者[NSMutableArray array]。
a. iOS很特別的一點(diǎn)就是用函數(shù)名字了表明內(nèi)存管理的方式(包含alloc、retain、new、copy、mutablecopy等關(guān)鍵字的函數(shù)需要申請者負(fù)責(zé)釋放該實(shí)例對象),而后者不包含上述關(guān)鍵字的函數(shù)不需要調(diào)用者負(fù)責(zé)該對象的釋放,所以其實(shí)上面方法后者不要調(diào)用者釋放該實(shí)例,因?yàn)? (NSMutableArray *)array函數(shù)內(nèi)部調(diào)用了autorelase函數(shù),如下為其底層實(shí)現(xiàn):

// + (NSMutableArray *)array 底層函數(shù)實(shí)現(xiàn)
+ (NSMutableArray *)array {
         NSMutableArray *arr = [[NSMutableArray alloc] init];
         return [arr autorelease];
}

在出array函數(shù)范圍之前先不釋放arr對象,將其放入autoreleasePool,待到下一次清空自動釋放池時(shí)對其發(fā)送一次release,剩下的就看調(diào)用者是否持有了該對象, 如果函數(shù)調(diào)用者持有了該arr對象,則arr對象繼續(xù)存在,否則如果arr對象出了(NSMutableArray *)array沒有在被持有,則在autoreleasePool清空的時(shí)候(之后arr引用計(jì)數(shù)為0)arr被釋放內(nèi)存。所以一個(gè)autorelease對象,當(dāng)被__weak修飾的變量持有時(shí),它的生命周期就是所在autoreleasePool結(jié)束前的那段時(shí)間;
b. 接下來看一下- (id)autorelease函數(shù)的底層實(shí)現(xiàn):

id *objc_autorelease(id obj)
{
 return AutoreleasePoolPage::autorelease(obj);
}

AutoreleasePoolPage即自動釋放池的底層實(shí)現(xiàn),可以看我的博客AutoreleasePool,其實(shí)就是將需要延遲釋放的對象放入了一個(gè)鏈表,程序中可能存在很多個(gè)自動釋放池(每次會使用棧頂?shù)淖詣俞尫懦胤湃耄?br> c. 那么,實(shí)際結(jié)果真是這樣的嗎???(使用函數(shù)打印、TLS)
我們可以使用clang命令對文件進(jìn)行編譯,查看ARC之后的代碼是怎么樣的;另外通常情況需要制定引用的Framework(控制臺程序通常不需要):

xcrun -sdk iphonesimulator10.2 clang -S -fobjc-arc -emit-llvm main.m -o main.ll -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework

使用上述命令可以生成編譯器ARC之后的代碼,

int main(int argc, const char * argv[]) {
    @autoreleasepool{
        NSObject *obj = [NSObject new];
    }
    return 0;
}

下面是生成的底層代碼:

define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4 //在棧上分配變量名內(nèi)存
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca %0*, align 8 //實(shí)際上這里就是NSObject *obj變量定義處
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %7 = call i8* @objc_autoreleasePoolPush() #2 //創(chuàng)建一個(gè)自動釋放池
  %8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8 //加載類對象地址(這里是NSObject元類) 
  %9 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8 //加載SEL地址,這里是new函數(shù)
  %10 = bitcast %struct._class_t* %8 to i8* //類型轉(zhuǎn)換
  %11 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %10, i8* %9) //[NSObject new]
  %12 = bitcast i8* %11 to %0*
  store %0* %12, %0** %6, align 8 //賦值給NSObject *obj,定義在開始處
  %13 = bitcast %0** %6 to i8**
  call void @objc_storeStrong(i8** %13, i8* null) #2 //釋放
  call void @objc_autoreleasePoolPop(i8* %7) //銷毀自動釋放池
  ret i32 0
}

第一步,在棧上先分配變量名所需要的內(nèi)存(alloca x, x);
第二步,將一個(gè)自動釋放池壓入棧頂(@ objc_autoreleasePoolPush());
第三步,中間一些代碼就是獲取獲取NSObject元類對象以及加載new 函數(shù)的SEL地址,然后調(diào)用objc_msgSend()進(jìn)行實(shí)例化;
第四步,使用完之后調(diào)用objc_storeStrong(src, value),這個(gè)函數(shù)可以用來保存一個(gè)新對象,當(dāng)然如果傳入的value = null,也可以用來釋放原來的對象,下面是objc_storeStrong(,)內(nèi)部實(shí)現(xiàn):

void objc_storeStrong(id *location, id obj)
{
    id prev = *location; //暫存現(xiàn)在對象
    if (obj == prev) {
        return;
    }
    objc_retain(obj); //持有新對象
    *location = obj;  //指向新對象
    objc_release(prev); //釋放舊對象
}

當(dāng)然,本文這里傳入的是null,所以就是使用完就立即釋放掉了;
而當(dāng)源碼中實(shí)例化對象方法改成這樣時(shí):

   NSMutableArray *obj = [NSMutableArray array];

改變實(shí)例化方式,array內(nèi)部會調(diào)用autorelease:

  %7 = call i8* @objc_autoreleasePoolPush() #2
  %11 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend
  %12 = call i8* @objc_retainAutoreleasedReturnValue(i8* %11) #2
...
  call void @objc_storeStrong(i8** %14, i8* null) #2
  call void @objc_autoreleasePoolPop(i8* %7)

可以看出,雖然這里看不出array里調(diào)用了autorelease(下面會驗(yàn)證),但是對array返回的對象調(diào)用了objc_retainAutoreleasedReturnValue,這個(gè)其實(shí)是clang編譯器對其進(jìn)行了優(yōu)化(只針對64位系統(tǒng)),會去一個(gè)叫TLS(Thread Local Storage)的地方(實(shí)際上是一個(gè)放在線程上的字典局部變量)查看該返回對象是否做了標(biāo)記,這樣可以節(jié)約retain和release的開銷;

三、放入AutoreleasePool時(shí)機(jī)

其實(shí)上面已經(jīng)說明了,通常系統(tǒng)提供的實(shí)例化方法如new/alloc/copy/mutablecopy等方法返回的實(shí)例對象是不會放進(jìn)自動釋放池,而像[NSMutableArray array]等簡便函數(shù)構(gòu)造器會內(nèi)部調(diào)用autotorelase。
我們可以使用下面的函數(shù)和命令驗(yàn)證:

void _objc_autoreleasePoolPrint();

《Objective-C 高級編程》中有介紹該函數(shù),這是一個(gè)非公開的私有函數(shù),可以打印出自動釋放池里的內(nèi)容,具體的用法現(xiàn)在文件開頭聲明該外部函數(shù)

extern void _objc_autoreleasePoolPrint(void);

下面看一下[NSMutableArray new]和[NSMutableArray array]下自動釋放池的情況:
[NSMutableArray new]下自動釋放池情況:

objc[55819]: [0x102003000]  ................  PAGE  (hot) (cold)
objc[55819]: [0x102003038]  ################  POOL 0x102003038
objc[55819]: ##############

[NSMutableArray array]下:

objc[56005]: [0x101004000]  ................  PAGE  (hot) (cold)
objc[56005]: [0x101004038]  ################  POOL 0x101004038
objc[56005]: [0x101004040]       0x100621490  __NSArrayM
objc[56005]: ##############

可以看出,在+(NSMutableArray *)array下一個(gè)__NSArrayM被加入自動釋放池,所以前面的理論是正確的。

四、應(yīng)用場景

通常情況下我們不需要Autorelease也沒什么關(guān)系,但是有時(shí)候理解了底層原理,我們就可以理解了一些大家常用的技巧:

for (int i=0; i<1000000; i++)
{
  @autoreleasepool{
        //實(shí)例化臨時(shí)對象
    }
}

這常常用來解決內(nèi)存峰值問題,但是其實(shí)如果里邊不包含array這種類型的函數(shù),即使實(shí)例化大量對象也是不會導(dǎo)致內(nèi)存峰值出現(xiàn)的,如:

 for (int i=0; i<100000000; i++){
        NSMutableArray *obj = [NSMutableArray new];
        NSNumber *num = [[NSNumber alloc] initWithInt:i];
        [obj addObject:num];
        NSLog(@"%@", obj);
    }

五、小結(jié)

  1. Autorelease用來延遲釋放對象,該對象釋放的時(shí)機(jī)在所在自動釋放池情況時(shí);
  2. 使用new/alloc/copy/mutablecopy等函數(shù)實(shí)例化的對象不會放入自動釋放池,相反,用其他簡便構(gòu)造器獲(如+(NSMutableArray *)array)得到的實(shí)例則會放入自動釋放池;
  3. 上面講的是通常情況便于理解,其實(shí)編譯器內(nèi)部還對Autorelease進(jìn)行了優(yōu)化,用于降低內(nèi)存管理的開銷。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.1 什么是自動引用計(jì)數(shù) 概念:在 LLVM 編譯器中設(shè)置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,478評論 1 17
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,628評論 30 472
  • 29.理解引用計(jì)數(shù) Objective-C語言使用引用計(jì)數(shù)來管理內(nèi)存,也就是說,每個(gè)對象都有個(gè)可以遞增或遞減的計(jì)數(shù)...
    Code_Ninja閱讀 1,743評論 1 3
  • 概述 在iOS中開發(fā)中,我們或多或少都聽說過內(nèi)存管理。iOS的內(nèi)存管理一般指的是OC對象的內(nèi)存管理,因?yàn)镺C對象分...
    DamonMok閱讀 4,121評論 2 20
  • 前言 從我開始學(xué)習(xí)iOS的時(shí)候,身邊的朋友、網(wǎng)上的博客都告訴我iOS的內(nèi)存管理是依靠引用計(jì)數(shù)的,然后說引用計(jì)數(shù)大于...
    蓋世英雄_ix4n04閱讀 664評論 0 1

友情鏈接更多精彩內(nèi)容