引言
開始之前先看一段代碼,猜猜輸出結(jié)果是什么?
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
__weak id refStr1 = nil;
__weak id refStr2 = nil;
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello"];
refStr1 = str;
refStr2 = [str stringByAppendingString:@" world"];
}
NSLog(@"refStr1:%@",refStr1);
NSLog(@"refStr2:%@",refStr2);
}
return 0;
}
測(cè)試輸出結(jié)果如下:
refStr1:hello
refStr2:(null)
refStr1為何不是空,不合常理啊!難道我所理解的自動(dòng)釋放池是錯(cuò)的!autoreleasepool到底做了什么?
autoreleasepool探究
我們知道自動(dòng)釋放池用于存放那些稍后在某個(gè)時(shí)刻需要釋放的對(duì)象,清空自動(dòng)釋放池,會(huì)向其中的對(duì)象發(fā)送release消息,釋放對(duì)象。上面測(cè)試中refStr1是個(gè)弱引用,不會(huì)遞增str引用計(jì)數(shù),autoreleasepool作用域結(jié)束后,str應(yīng)該釋放,但refStr1仍會(huì)輸出"hello"。難道str沒有被釋放?str有沒有被加入到自動(dòng)釋放池中?autoreleasepool本質(zhì)又是什么?
使用clang命令轉(zhuǎn)化為C++代碼,如果當(dāng)前環(huán)境不支持weak引用,可將weak聲明改為__unsafe_unretained后再執(zhí)行轉(zhuǎn)換,__unsafe_unretained也不會(huì)增加對(duì)象的引用計(jì)數(shù),但所指向的對(duì)象釋放后,其值不會(huì)置為空,再訪問可能導(dǎo)致意想不到的錯(cuò)誤
clang -rewrite-objc main.m
找到main入口函數(shù)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((objc_ownership(none))) id refStr1 = __null;
__attribute__((objc_ownership(none))) id refStr2 = __null;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSString *str = ((NSString *(*)(id, SEL, NSString *, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_0);
refStr1 = str;
refStr2 = ((NSString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("stringByAppendingString:"), (NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_1);
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_2,refStr1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_3,refStr2);
}
return 0;
}
其中@autoreleasepool{}塊被轉(zhuǎn)換成了{(lán)__AtAutoreleasePool __autoreleasepool;},那__AtAutoreleasePool又是什么呢?繼續(xù)查找其定義
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
__AtAutoreleasePool是個(gè)結(jié)構(gòu)體,定義很簡單,一個(gè)構(gòu)造函數(shù),一個(gè)析構(gòu)函數(shù),一個(gè)指針。其中在構(gòu)造函數(shù)中調(diào)用了objc_autoreleasePoolPush(),在析構(gòu)函數(shù)中調(diào)用了objc_autoreleasePoolPop(atautoreleasepoolobj)。至于atautoreleasepoolobj又是什么東東,下文再說??梢钥闯鯜autoreleasepool{}其實(shí)等價(jià)于下面代碼
void* pt =objc_autoreleasePoolPush();
//your code
objc_autoreleasePoolPop(pt);
objc_autoreleasePoolPush與objc_autoreleasePoolPop這兩個(gè)函數(shù)實(shí)現(xiàn)很簡單,分別調(diào)用了AutoreleasePoolPage的靜態(tài)方法push與pop
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
終于揭開autoreleasepool的神秘面紗,原來AutoreleasePoolPage才是其中的核心,其主要結(jié)構(gòu)如下:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
至于AutoreleasePoolPage的具體實(shí)現(xiàn),在此不再做詳細(xì)介紹,具體可參考sunnyxx《黑幕背后的Autorelease》和Draveness的《自動(dòng)釋放池的前世今生》。這里主要引述其結(jié)論:
- AutoreleasePool與線程一一對(duì)應(yīng),結(jié)構(gòu)中的thread指向當(dāng)前線程
- AutoreleasePoolPage每個(gè)對(duì)象內(nèi)存大小為4096字節(jié),除了存儲(chǔ)其本身的實(shí)例變量外,剩下的空間用來儲(chǔ)存加入到自動(dòng)釋放池中的對(duì)象的地址
- AutoreleasePoolPage以雙向鏈表的形式組合,parent指針指向上一個(gè)page,child指針指向下一個(gè)page
- 每次調(diào)用objc_autoreleasePoolPush時(shí),會(huì)返回一個(gè)哨兵對(duì)象,也就是上文提到的autoreleasepoolobj,指向當(dāng)前AutoreleasePoolPage中next指針指向的地址。
- 向一個(gè)對(duì)象發(fā)送autorelease消息,會(huì)把這個(gè)對(duì)象的地址加入到當(dāng)前AutoreleasePoolPage中next指針指向的位置,之后next指針指向新加入對(duì)象的下一位置
- 一個(gè)AutoreleasePoolPage的空間被占滿時(shí),會(huì)新建一個(gè)AutoreleasePoolPage對(duì)象,child指針指向新建的page,后來加入到自動(dòng)釋放池中的對(duì)象添加到新的page
- 自動(dòng)釋放池釋放時(shí),根據(jù)push時(shí)創(chuàng)建的哨兵對(duì)象,找到對(duì)應(yīng)的自動(dòng)釋放池。從最新加入的對(duì)象一直向前清理(發(fā)送release消息),可以向前跨越若干個(gè)page,直至哨兵對(duì)象所指向的地址。
AutoreleasePoolPage調(diào)試
了解autoreleasepool的原理后,回到開始的問題,我們的疑惑還沒解決。結(jié)合調(diào)試,來看看到底發(fā)生了生么?調(diào)試需要編譯后objc源碼,有網(wǎng)友已編譯好了,這里下載
修改開始的代碼,輸出refStr1、refStr2地址,便于對(duì)照
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello"];
refStr1 = str;
refStr2 = [str stringByAppendingString:@" world"];
NSLog(@"refStr1=%p,refStr2=%p",refStr1,refStr2);
}
在大括號(hào)結(jié)束前插入斷點(diǎn),在控制臺(tái)執(zhí)行下面命令
expression AutoreleasePoolPage::hotPage() //獲取當(dāng)前AutoreleasePoolPage指針$0
p *$0 //查看AutoreleasePoolPage結(jié)構(gòu)
p $0->printAll() //輸出AutoreleasePoolPage信息
運(yùn)行結(jié)果如下圖

我們發(fā)現(xiàn)str并沒有被加入到自動(dòng)釋放池中,所以refStr1最后仍能輸出"hello"。為何str沒有被加入到自動(dòng)釋放池中呢?
記得以前在《Effective Objective-C》中看過一段話(在"不要使用retainCount"一節(jié)),當(dāng)時(shí)只做了標(biāo)注,并未深思,至此方有所悟。
- 系統(tǒng)會(huì)盡可能把NSString實(shí)現(xiàn)成單例對(duì)象,這種對(duì)象的保留及釋放操作都是'空操作'。編譯器會(huì)把NSString對(duì)象所表示的數(shù)據(jù)放到應(yīng)用程序的二進(jìn)制文件里,這樣的話,運(yùn)行程序時(shí)就可以直接使用了,無須再創(chuàng)建NSString對(duì)象。
- NSNumber也類似,它使用了一種叫做'標(biāo)簽指針'(tagged pointer)的概念來標(biāo)注特定類型的數(shù)值。這種做法不使用NSNumber對(duì)象,而是把與數(shù)值有關(guān)的全部消息都放在指針里面。運(yùn)行期系統(tǒng)會(huì)在消息派發(fā)期間檢測(cè)到這種標(biāo)簽指針,并對(duì)它執(zhí)行相應(yīng)操作,使其行為看上去和真正的NSNumber對(duì)象一樣。這種優(yōu)化只在某些場(chǎng)合使用,同樣是NSNumber對(duì)象,整數(shù)做了優(yōu)化,浮點(diǎn)數(shù)對(duì)象就沒有優(yōu)化。
修改上面代碼,又進(jìn)行了測(cè)試,果然如此!
__weak id refStr1 = nil;
__weak id refStr2 = nil;
__weak id refNum1 = nil;
__weak id refNum2 = nil;
__weak id refObj = nil;
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello"];
refStr1 = str;
refStr2 = [str stringByAppendingString:@" world"];
NSNumber *number1 = [NSNumber numberWithInt:32];
NSNumber *number2 = [NSNumber numberWithFloat:3.2];
refNum1 = number1;
refNum2 = number2;
NSObject *obj = [NSObject new];
refObj = obj;
}
NSLog(@"refStr1:%@",refStr1);
NSLog(@"refStr2:%@",refStr2);
NSLog(@"refNum1:%@",refNum1);
NSLog(@"refNum2:%@",refNum2);
NSLog(@"refObj:%@",refObj);
輸出結(jié)果
refStr1:hello
refStr2:(null)
refNum1:32
refNum2:(null)
refObj:(null)
至此,我們解決了開始的疑惑。
結(jié)論
- autorelease塊在開始和結(jié)束時(shí),分別調(diào)用了objc_autoreleasePoolPush和objc_autoreleasePoolPop方法
- 自動(dòng)釋放池功能由AutoreleasePoolPage類實(shí)現(xiàn),向一個(gè)對(duì)象發(fā)送autorelease消息,會(huì)把對(duì)象對(duì)象加入到當(dāng)前的AutoreleasePoolPage中。
- 自動(dòng)釋放池清理時(shí),會(huì)從當(dāng)前最新加入的對(duì)象開始,直至push時(shí)創(chuàng)建的哨兵對(duì)象結(jié)束。
- NSString和NSNumber部分對(duì)象的保留及釋放操作可能是空操作,釋放時(shí)不會(huì)被加入到自動(dòng)釋放池。
結(jié)語
剛看sunnyxx的《黑幕背后的Autorelease》,感覺甚是深?yuàn)W,不解其義,自己的測(cè)試結(jié)果也與文中開始實(shí)驗(yàn)的結(jié)果不同,未明白是怎么回事。其實(shí)sunnyxx的文章發(fā)布至今三年多都過去了,蘋果說不一定已做了優(yōu)化。其測(cè)試環(huán)境是真機(jī)還是模擬器,也不得而知,不同的測(cè)試環(huán)境,其結(jié)果也可能會(huì)有差異。
后來又讀到Draveness的《自動(dòng)釋放池的前世今生》,學(xué)習(xí)了其中的調(diào)試技巧,結(jié)合調(diào)試、測(cè)試,終于搞明白了autoreleasepool的原理,解決了以前的困惑,但目前所知只是一角。
紙上得來終覺淺,絕知此事要躬行。自己動(dòng)動(dòng)手,你所學(xué)到的遠(yuǎn)比你看到的多!
思考
留個(gè)問題,ref1、ref2分別在什么時(shí)候釋放?
在viewDidLoad方法結(jié)束時(shí)釋放?還是在當(dāng)前RunLoop即將休眠或結(jié)束時(shí)釋放?或者不會(huì)釋放?
諸位怎么看
__weak id ref1;
__weak id ref2;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"haha";
ref1 = str;
NSObject *obj = [NSObject new];
ref2 = obj;
}