最近有個大佬考察了我關於autoreleasepool的瞭解, 以前一直認爲本身瞭解, 可是稍微一問深, 本身卻啞口無言. 仔細思考了下, 決定要將這個問題結合以前的知識重新梳理一下, 固然, 實踐是必不可少的.git
帶着這三個問題, 一塊兒進行一下下面的思考.github
對於autoreleasepool釋放時機, 咱們很容易在網上搜到這樣的說法:bash
分兩種狀況:手動干預釋放時機、系統自動去釋放。服務器
手動干預釋放時機--指定autoreleasepool 就是所謂的:當前做用域大括號結束時釋放。多線程
系統自動去釋放--不手動指定autoreleasepool併發
先不談上面是否徹底正確, 基於以上認知, 當時我靈光一閃推測main函數中autoreleasepool的做用可能爲下面兩種之一:app
1.系統主線程中的默認的autoreleasepool.ide
2.整個App相對於iOS系統的一個autoreleasepool.函數
其餘的解釋其實在網上能夠搜到不少, 因此這裏咱們能夠作一個小實驗.高併發
第一點其實很好驗證, 將main函數中的autoreleasepool註釋掉, 運行
for (int i = 0; i < 10e5 * 2; i++) {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
NSLog(@"finished!");複製代碼
實際結果代表, 內存波動並無什麼區別:
未註釋Main函數中的autoreleasepool
註釋Main函數中的autoreleasepool
因此咱們能夠認爲第二種是對的嗎, 後來本身一想也以爲不對, 對於系統內存管理相關代碼怎麼會在程序裏面呢, 不符合蘋果的風格. 結果很明顯我本身推測的都不對, 因此到底起什麼做用呢? 待會再細說, 先驗證一下釋放時機的問題.
一樣是上面一段函數, 在for循環中加入autoreleasepool:
for (int i = 0; i < 10e5 * 2; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
}
NSLog(@"finished!");複製代碼
我相信稍微瞭解一點的同窗已經知道了運行結果:
爲臨時變量分配的內存已經獲得平穩的釋放, 因此結論就是最上面咱們看到的認知? 其實自己每一個Runloop已經默認會建立一個autoreleasepool了, 因此咱們這裏添加至關於嵌套(便於理解)了一個, 並無弄清楚autoreleasepool自身的釋放時機. 下面作另一個小測試:
這一次在代碼中新增對Runloop的Observer, 及時獲取Runloop的狀態變化確認釋放時機, 代碼以下:
// 添加一個監聽者
- (void)addRunLoopObserver {
// 1. 建立監聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"進入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. 添加監聽者
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}複製代碼
另外上面的方法運行連續運行兩次, 不手動添加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!");
}複製代碼
運行以後的效果是這樣的:
很清楚的看到Runloop沒有完成一次循環以前全部內存都未釋放, 即便局部變量出了做用域也必須等待Runloop循環完成.
下面一樣, 手動添加autoreleasepool觀察釋放時機.
結果是意外也合理的. 即便Runloop未完成循環, 內存也即便釋放了
@autoreleasepool{}複製代碼
等價於
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);複製代碼
每次出了{}時objc_autoreleasePoolPop()就被調用, 因此直接釋放掉了. 固然, 系統自動建立的autoreleasepool也是同樣, 只是調用的時機不一樣: 線程與Runloop是一一對應, Runloop與系統建立的autoreleasepool也是一一對應, 因此不管是Runloop完成了一次循環仍是線程被關閉時, autoreleasepool都會釋放, 固然手動添加的也會被管理, 上面爲了方便理解, 說的是嵌套, 本質上是沒有嵌套這個說法的, 對@autoreleasepool{}本質的一些我的總結:
主要就是一個類:AutoreleasePoolPage
兩個函數: objc_autoreleasePoolPush()、objc_autoreleasePoolPop()
運做方式: autoreleasepool由若干個autoreleasePoolPage類以雙向鏈表的形式組合而成, 當程序運行到@autoreleasepool{時, objc_autoreleasePoolPush()將被調用, runtime會向當前的AutoreleasePoolPage中添加一個nil對象做爲哨兵,
在{}中建立的對象會被依次記錄到AutoreleasePoolPage的棧頂指針,
當運行完@autoreleasepool{}時, objc_autoreleasePoolPop(哨兵)將被調用, runtime就會向AutoreleasePoolPage中記錄的對象發送release消息直到哨兵的位置, 即完成了一次完整的運做.
另外根據官方文檔:
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......
主線程中的自動釋放池是自動建立的, 文檔中說子線程中的自動釋放池是須要手動建立的, 但實測, 其實咱們經常使用的多線程管理方式(GCD, NSOprationQueue, NSThread)都已經幫咱們處理好了, 其中NSThread在iOS7以後才自動建立線程中的AutoreleasePool, 這個在官方文檔中找不到記錄, 參考StackOverflow: stackoverflow.com/questions/2…
另外網上有說法AutoreleasePool會影響性能, 其實看上面的函數運行的時間就能夠發現, 並無影響, 甚至加入了AutoreleasePool運行快了2秒(不嚴謹).
回到最初的問題, main函數中的autoreleasepool的做用, 我翻閱了大量資料, 在StackOverflow上讚的比較高的回答是沒卵用... 暫且只能先這樣認爲了.. 但願有了解的同窗能夠講解一下~
在實際中的使用場景其實很明確了, 在程序中中有大量臨時變量的時候最好手動建立.
最常出現大量變量的時候顯然是循環/遍歷, 咱們經常使用的for循環, 以及enumerate其實跟autoreleasepool也有關, for循環是不自動建立autoreleasepool的, 而enumerate中已經自動建立了autoreleasepool, 值得注意的是高併發enumerate經常會出一些意外的問題, 例如對象被提早釋放, 因此建議高併發狀況下使用for循環(性能高於enumerate), 再手動添加autoreleasepool.
本人前幾篇文章中提到的一個App: 直播伴侶中就是手機端對彈幕進行高併發計算, 分詞, 對比.. 使用了autoreleasepool以後明顯在鬥魚彈幕服務器"炸魚"時有所改善..歡迎Star: github.com/syik/Bullet…