引言
最近有個(gè)大佬考察了我關(guān)于autoreleasepool的了解, 之前一直認(rèn)為自己了解, 但是稍微一問(wèn)深, 自己卻啞口無(wú)言. 仔細(xì)思考了下, 決定要將這個(gè)問(wèn)題結(jié)合之前的知識(shí)從新梳理一下, 當(dāng)然, 實(shí)踐是必不可少的.
- main函數(shù)中的autoreleasepool的作用?
- 系統(tǒng)的autoreleasepool我們自己創(chuàng)建的autoreleasepool釋放時(shí)機(jī)差別在哪?
- 在ARC的環(huán)境中, 什么情況下需要使用autoreleasepool? 不使用autoreleasepool變量什么時(shí)候會(huì)被釋放?
帶著這三個(gè)問(wèn)題, 一起進(jìn)行一下下面的思考.
正文
對(duì)于autoreleasepool釋放時(shí)機(jī), 我們很容易在網(wǎng)上搜到這樣的說(shuō)法:
分兩種情況:手動(dòng)干預(yù)釋放時(shí)機(jī)食寡、系統(tǒng)自動(dòng)去釋放顷牌。
手動(dòng)干預(yù)釋放時(shí)機(jī)--指定autoreleasepool 就是所謂的:當(dāng)前作用域大括號(hào)結(jié)束時(shí)釋放遣疯。
系統(tǒng)自動(dòng)去釋放--不手動(dòng)指定autoreleasepool
先不談上面是否完全正確, 基于以上認(rèn)知, 當(dāng)時(shí)我靈光一閃推測(cè)main函數(shù)中autoreleasepool的作用可能為下面兩種之一:
1.系統(tǒng)主線程中的默認(rèn)的autoreleasepool.
2.整個(gè)App相對(duì)于iOS系統(tǒng)的一個(gè)autoreleasepool.
其他的解釋其實(shí)在網(wǎng)上可以搜到很多, 所以這里我們可以做一個(gè)小實(shí)驗(yàn).
第一點(diǎn)其實(shí)很好驗(yàn)證, 將main函數(shù)中的autoreleasepool注釋掉, 運(yùn)行
for (int i = 0; i < 10e5 * 2; i++) {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
NSLog(@"finished!");
實(shí)際結(jié)果表明, 內(nèi)存波動(dòng)并沒(méi)有什么區(qū)別:
-
未注釋Main函數(shù)中的autoreleasepool
-
注釋Main函數(shù)中的autoreleasepool
所以我們可以認(rèn)為第二種是對(duì)的嗎, 后來(lái)自己一想也覺(jué)得不對(duì), 對(duì)于系統(tǒng)內(nèi)存管理相關(guān)代碼怎么會(huì)在程序里面呢, 不符合蘋(píng)果的風(fēng)格. 結(jié)果很明顯我自己推測(cè)的都不對(duì), 所以到底起什么作用呢? 待會(huì)再細(xì)說(shuō), 先驗(yàn)證一下釋放時(shí)機(jī)的問(wèn)題.
同樣是上面一段函數(shù), 在for循環(huán)中加入autoreleasepool:
for (int i = 0; i < 10e5 * 2; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
}
NSLog(@"finished!");
我相信稍微了解一點(diǎn)的同學(xué)已經(jīng)知道了運(yùn)行結(jié)果:
為臨時(shí)變量分配的內(nèi)存已經(jīng)得到平穩(wěn)的釋放, 所以結(jié)論就是最上面我們看到的認(rèn)知? 其實(shí)本身每個(gè)Runloop已經(jīng)默認(rèn)會(huì)創(chuàng)建一個(gè)autoreleasepool了, 所以我們這里添加相當(dāng)于嵌套(便于理解)了一個(gè), 并沒(méi)有弄清楚autoreleasepool自身的釋放時(shí)機(jī). 下面做另外一個(gè)小測(cè)試:
這一次在代碼中新增對(duì)Runloop的Observer, 及時(shí)獲取Runloop的狀態(tài)變化確認(rèn)釋放時(shí)機(jī), 代碼如下:
// 添加一個(gè)監(jiān)聽(tīng)者
- (void)addRunLoopObserver {
// 1. 創(chuàng)建監(jiān)聽(tīng)者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"進(jìn)入RunLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"被喚醒");
break;
case kCFRunLoopExit:
NSLog(@"退出RunLoop");
break;
default:
break;
}
});
// 2. 添加監(jiān)聽(tīng)者
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
另外上面的方法運(yùn)行連續(xù)運(yùn)行兩次, 不手動(dòng)添加autoreleasepool, 大概是這樣:
- (void)test1 {
NSLog(@"test1 begin!");
for (int i = 0; i < 10e5 * 2; i++) {
//@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
//}
}
NSLog(@"test1 finished!");
}
- (void)test2 {
NSLog(@"test2 begin!");
for (int i = 0; i < 10e5 * 2; i++) {
//@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
//}
}
NSLog(@"test2 finished!");
}
運(yùn)行之后的效果是這樣的:
很清楚的看到Runloop沒(méi)有完成一次循環(huán)之前所有內(nèi)存都未釋放, 即使局部變量出了作用域也必須等待Runloop循環(huán)完成.
下面同樣, 手動(dòng)添加autoreleasepool觀察釋放時(shí)機(jī).
結(jié)果是意外也合理的. 即使Runloop未完成循環(huán), 內(nèi)存也即使釋放了.
總結(jié)
@autoreleasepool{}
等價(jià)于
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
每次出了{(lán)}時(shí)objc_autoreleasePoolPop()就被調(diào)用, 所以直接釋放掉了. 當(dāng)然, 系統(tǒng)自動(dòng)創(chuàng)建的autoreleasepool也是一樣, 只是調(diào)用的時(shí)機(jī)不同: 線程與Runloop是一一對(duì)應(yīng), Runloop與系統(tǒng)創(chuàng)建的autoreleasepool也是一一對(duì)應(yīng), 所以不論是Runloop完成了一次循環(huán)還是線程被關(guān)閉時(shí), autoreleasepool都會(huì)釋放, 當(dāng)然手動(dòng)添加的也會(huì)被管理, 上面為了方便理解, 說(shuō)的是嵌套, 本質(zhì)上是沒(méi)有嵌套這個(gè)說(shuō)法的, 對(duì)@autoreleasepool{}本質(zhì)的一些個(gè)人總結(jié):
主要就是一個(gè)類(lèi):AutoreleasePoolPage
兩個(gè)函數(shù): objc_autoreleasePoolPush()、objc_autoreleasePoolPop()
運(yùn)作方式: autoreleasepool由若干個(gè)autoreleasePoolPage類(lèi)以雙向鏈表的形式組合而成, 當(dāng)程序運(yùn)行到@autoreleasepool{時(shí), objc_autoreleasePoolPush()將被調(diào)用, runtime會(huì)向當(dāng)前的AutoreleasePoolPage中添加一個(gè)nil對(duì)象作為哨兵,
在{}中創(chuàng)建的對(duì)象會(huì)被依次記錄到AutoreleasePoolPage的棧頂指針,
當(dāng)運(yùn)行完@autoreleasepool{}時(shí), objc_autoreleasePoolPop(哨兵)將被調(diào)用, runtime就會(huì)向AutoreleasePoolPage中記錄的對(duì)象發(fā)送release消息直到哨兵的位置, 即完成了一次完整的運(yùn)作.
另外根據(jù)官方文檔:
Threads
If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool......
主線程中的自動(dòng)釋放池是自動(dòng)創(chuàng)建的, 文檔中說(shuō)子線程中的自動(dòng)釋放池是需要手動(dòng)創(chuàng)建的, 但實(shí)測(cè), 其實(shí)我們常用的多線程管理方式(GCD, NSOprationQueue, NSThread)都已經(jīng)幫我們處理好了, 其中NSThread在iOS7之后才自動(dòng)創(chuàng)建線程中的AutoreleasePool, 這個(gè)在官方文檔中找不到記錄, 參考StackOverflow: https://stackoverflow.com/questions/24952549/does-nsthread-create-autoreleasepool-automatically-now
另外網(wǎng)上有說(shuō)法AutoreleasePool會(huì)影響性能, 其實(shí)看上面的函數(shù)運(yùn)行的時(shí)間就可以發(fā)現(xiàn), 并沒(méi)有影響, 甚至加入了AutoreleasePool運(yùn)行快了2秒(不嚴(yán)謹(jǐn)).
回到最初的問(wèn)題, main函數(shù)中的autoreleasepool的作用, 我翻閱了大量資料, 在StackOverflow上贊的比較高的回答是沒(méi)卵用... 暫且只能先這樣認(rèn)為了.. 希望有了解的同學(xué)可以講解一下~
在實(shí)際中的使用場(chǎng)景其實(shí)很明確了, 在程序中中有大量臨時(shí)變量的時(shí)候最好手動(dòng)創(chuàng)建.
最常出現(xiàn)大量變量的時(shí)候顯然是循環(huán)/遍歷, 我們常用的for循環(huán), 以及enumerate其實(shí)跟autoreleasepool也有關(guān), for循環(huán)是不自動(dòng)創(chuàng)建autoreleasepool的, 而enumerate中已經(jīng)自動(dòng)創(chuàng)建了autoreleasepool, 值得注意的是高并發(fā)enumerate常常會(huì)出一些意外的問(wèn)題, 例如對(duì)象被提前釋放, 所以建議高并發(fā)情況下使用for循環(huán)(性能高于enumerate), 再手動(dòng)添加autoreleasepool.
本人前幾篇文章中提到的一個(gè)App: 直播伴侶中就是手機(jī)端對(duì)彈幕進(jìn)行高并發(fā)計(jì)算, 分詞, 對(duì)比.. 使用了autoreleasepool之后明顯在斗魚(yú)彈幕服務(wù)器"炸魚(yú)"時(shí)有所改善..歡迎Star: https://github.com/syik/BulletAnalyzer