前言
比較晚入坑iOS的同學(xué)大多沒怎么使用過MRC(Manual Reference Counting),直接享受了ARC的便利点待,ARC(Automatic Reference Counting)從Xcode4引入,由于ARC下禁止直接調(diào)用retain璧尸、release遮咖、autorelease等函數(shù),甚至不需要了解autorelease這些函數(shù)就能很好的進行iOS開發(fā)懊烤。然而,為了知其所以然宽堆,還是有必要了解一下底層的原理腌紧。
一、 概述
首先明確一點畜隶,ARC和Autorelease沒有直接的聯(lián)系壁肋,在MRC時代就存在Autorelease:
ARC:代替手動添加內(nèi)存管理函數(shù)進行人工內(nèi)存管理(retain、release籽慢、autorelease)浸遗,編譯階段自動的在需要持有對象時插入retain函數(shù)、需要釋放時插入release函數(shù)(就是這么智能)箱亿;
Autorelease:向一個對象發(fā)送延遲釋放信息跛锌,使得這個對象可以在作用域意外范圍被使用;典型的例子就是將一個對象作為返回值給調(diào)用者届惋,如果不延遲釋放髓帽,這個返回值在出了所在函數(shù)范圍就被立即釋放,調(diào)用者拿到的永遠是nil脑豹,因為iOS的內(nèi)存遵循誰申請誰釋放的原則郑藏,當(dāng)向一個對象發(fā)送了autorelease消息,實際上就是將該對象放入autoreleasePool池子動晨缴,等到延遲到適當(dāng)?shù)臅r機(通常是在NSRunloop即將進入休眠或者退出時)進行釋放(對池子中的每個對象發(fā)送release消息)译秦。
二、 底層實現(xiàn)
大家都知道獲取一個NSMutableArray實例對象有好多種方式:如[NSMutableArray new]或者[NSMutableArray array]。
a. iOS很特別的一點就是用函數(shù)名字了表明內(nèi)存管理的方式(包含alloc筑悴、retain们拙、new、copy阁吝、mutablecopy等關(guān)鍵字的函數(shù)需要申請者負責(zé)釋放該實例對象)砚婆,而后者不包含上述關(guān)鍵字的函數(shù)不需要調(diào)用者負責(zé)該對象的釋放,所以其實上面方法后者不要調(diào)用者釋放該實例突勇,因為+ (NSMutableArray *)array函數(shù)內(nèi)部調(diào)用了autorelase函數(shù)装盯,如下為其底層實現(xiàn):
// + (NSMutableArray *)array 底層函數(shù)實現(xiàn)
+ (NSMutableArray *)array {
NSMutableArray *arr = [[NSMutableArray alloc] init];
return [arr autorelease];
}
在出array函數(shù)范圍之前先不釋放arr對象,將其放入autoreleasePool甲馋,待到下一次清空自動釋放池時對其發(fā)送一次release埂奈,剩下的就看調(diào)用者是否持有了該對象, 如果函數(shù)調(diào)用者持有了該arr對象定躏,則arr對象繼續(xù)存在账磺,否則如果arr對象出了(NSMutableArray *)array沒有在被持有,則在autoreleasePool清空的時候(之后arr引用計數(shù)為0)arr被釋放內(nèi)存痊远。所以一個autorelease對象垮抗,當(dāng)被__weak修飾的變量持有時,它的生命周期就是所在autoreleasePool結(jié)束前的那段時間碧聪;
b. 接下來看一下- (id)autorelease函數(shù)的底層實現(xiàn):
id *objc_autorelease(id obj)
{
return AutoreleasePoolPage::autorelease(obj);
}
AutoreleasePoolPage即自動釋放池的底層實現(xiàn)冒版,可以看我的博客AutoreleasePool,其實就是將需要延遲釋放的對象放入了一個鏈表逞姿,程序中可能存在很多個自動釋放池(每次會使用棧頂?shù)淖詣俞尫懦胤湃耄?br>
c. 那么辞嗡,實際結(jié)果真是這樣的嗎?哼凯?欲间?(使用函數(shù)打印楚里、TLS)
我們可以使用clang命令對文件進行編譯断部,查看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 //實際上這里就是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)建一個自動釋放池
%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)蔑祟;
第二步,將一個自動釋放池壓入棧頂(@ objc_autoreleasePoolPush())沉唠;
第三步疆虚,中間一些代碼就是獲取獲取NSObject元類對象以及加載new 函數(shù)的SEL地址,然后調(diào)用objc_msgSend()進行實例化;
第四步径簿,使用完之后調(diào)用objc_storeStrong(src, value)罢屈,這個函數(shù)可以用來保存一個新對象,當(dāng)然如果傳入的value = null篇亭,也可以用來釋放原來的對象缠捌,下面是objc_storeStrong(,)內(nèi)部實現(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)源碼中實例化對象方法改成這樣時:
NSMutableArray *obj = [NSMutableArray array];
改變實例化方式,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(下面會驗證),但是對array返回的對象調(diào)用了objc_retainAutoreleasedReturnValue哑芹,這個其實是clang編譯器對其進行了優(yōu)化(只針對64位系統(tǒng)),會去一個叫TLS(Thread Local Storage)的地方(實際上是一個放在線程上的字典局部變量)查看該返回對象是否做了標記捕透,這樣可以節(jié)約retain和release的開銷绩衷;
三、放入AutoreleasePool時機
其實上面已經(jīng)說明了激率,通常系統(tǒng)提供的實例化方法如new/alloc/copy/mutablecopy等方法返回的實例對象是不會放進自動釋放池咳燕,而像[NSMutableArray array]等簡便函數(shù)構(gòu)造器會內(nèi)部調(diào)用autotorelase。
我們可以使用下面的函數(shù)和命令驗證:
void _objc_autoreleasePoolPrint();
《Objective-C 高級編程》中有介紹該函數(shù)乒躺,這是一個非公開的私有函數(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下一個__NSArrayM被加入自動釋放池曹货,所以前面的理論是正確的。
四讳推、應(yīng)用場景
通常情況下我們不需要Autorelease也沒什么關(guān)系顶籽,但是有時候理解了底層原理,我們就可以理解了一些大家常用的技巧:
for (int i=0; i<1000000; i++)
{
@autoreleasepool{
//實例化臨時對象
}
}
這常常用來解決內(nèi)存峰值問題银觅,但是其實如果里邊不包含array這種類型的函數(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é)
- Autorelease用來延遲釋放對象镊绪,該對象釋放的時機在所在自動釋放池情況時;
- 使用new/alloc/copy/mutablecopy等函數(shù)實例化的對象不會放入自動釋放池洒忧,相反蝴韭,用其他簡便構(gòu)造器獲(如+(NSMutableArray *)array)得到的實例則會放入自動釋放池;
- 上面講的是通常情況便于理解熙侍,其實編譯器內(nèi)部還對Autorelease進行了優(yōu)化榄鉴,用于降低內(nèi)存管理的開銷履磨。