引言
最近有個大佬考察了我關於autoreleasepool的瞭解, 之前一直認為自己瞭解, 但是稍微一問深, 自己卻啞口無言. 仔細思考了下, 決定要將這個問題結合之前的知識從新梳理一下, 當然, 實踐是必不可少的.
- main函式中的autoreleasepool的作用?
- 系統的autoreleasepool我們自己建立的autoreleasepool釋放時機差別在哪?
- 在ARC的環境中, 什麼情況下需要使用autoreleasepool? 不使用autoreleasepool變數什麼時候會被釋放?
帶著這三個問題, 一起進行一下下面的思考.
正文
對於autoreleasepool釋放時機, 我們很容易在網上搜到這樣的說法:
分兩種情況:手動干預釋放時機、系統自動去釋放。
手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放。
系統自動去釋放--不手動指定autoreleasepool
先不談上面是否完全正確, 基於以上認知, 當時我靈光一閃推測main函式中autoreleasepool的作用可能為下面兩種之一:
1.系統主執行緒中的預設的autoreleasepool.
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…