[toc]
參考
https://www.cnblogs.com/XXxiaotaiyang/p/5118737.html
http://m.itdecent.cn/p/dfec601d84da
http://m.itdecent.cn/p/b0c19505a5a4
http://m.itdecent.cn/p/50bdd8438857
https://blog.csdn.net/mr_xiaojie/article/details/52953807
http://m.itdecent.cn/p/32265cbb2a26
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
簡介
AutoreleasePool 是OC中的一種內(nèi)存自動回收機(jī)制, 它可以 延遲 加入 AutoreleasePool 中的變量的 release 時機(jī)。
在正常情況下, 創(chuàng)建的變量會在超出其作用域的時候 release, 但是如果將變量加入 AutoreleasePool , 那么 release 將延遲執(zhí)行。
autorelease 是 ARC 進(jìn)行引用計(jì)數(shù)管理的一個機(jī)制。
autorelease 本質(zhì)是把 release 延遲到 autoreleasepool drain 的時候, 延遲內(nèi)存的釋放。
autorelease 和作用域沒有任何關(guān)系。
NSAutoreleasePool 是什么?
NSAutoreleasePool 實(shí)際上是個對象引用計(jì)數(shù)自動處理器, 是一個繼承于NSObject的類。
OC對象, 全部繼承自NSObject, 使用引用計(jì)數(shù)的方法來管理對象的存活, 眾所周知, 當(dāng)引用計(jì)數(shù)為0時, 對象就被銷毀了。
操作非常簡單, 當(dāng)對象被創(chuàng)建時, 引用計(jì)數(shù)被設(shè)成1??梢越o對象發(fā)送retain消息, 讓對象對自己的引用計(jì)數(shù)加1。
而當(dāng)對象接受到release消息時, 對象就會對自己的引用計(jì)數(shù)進(jìn)行減1, 當(dāng)引用計(jì)數(shù)到了0, 對象就會調(diào)用自己的dealloc處理。
當(dāng)對象被加入到 AutoreleasePool 中, 會對其對象retain一次, 當(dāng) AutoreleasePool 結(jié)束時, 會對其所有對象發(fā)送一次release消息。
AutoreleasePool 可以同時有多個, 它的組織是個棧, 總是存在一個棧頂pool, 也就是當(dāng)前pool;
每創(chuàng)建一個pool, 就往棧里壓一個, 改變當(dāng)前pool為新建的pool; 每次給pool發(fā)送drain消息, 就彈出棧頂?shù)膒ool, 改當(dāng)前pool為棧里的下一個pool。
設(shè)計(jì)場景
A retain 了一個對象, A有職責(zé) release 它, 但是現(xiàn)在不能release, 因?yàn)锳知道后續(xù)代碼可能想要 retain 它。
定義一個函數(shù), 返回對象A, 方法內(nèi)部 retain 了A, 意味著我們需要 release A, 來維持A引用計(jì)數(shù)的平衡;
但由于A是函數(shù)返回值, 我們要確保函數(shù)調(diào)用方拿到的對象A是沒被回收的, 所以不能在函數(shù)返回前 release。
也就是說, 我們需要 release 返回值, 但又不能在函數(shù)返回前。
有兩個選擇, 要么延后 release, 要么函數(shù)調(diào)用方幫我們 release。
這分別對應(yīng)著解決函數(shù)返回值引用計(jì)數(shù)問題的兩種方式:
① autorelease, 即延遲release, 函數(shù)返回前不進(jìn)行 release, 先把返回值暫存在 autoreleasepool 中一段時間。
這段時間內(nèi), 調(diào)用方如果需要, 可以 retain 這個返回值, 等到 autorelease pool 干(drain)的時候, 再去 release 這個對象, 平衡對象引用計(jì)數(shù), 適用于除 alloc、copy、new、mutableCopy 之外的函數(shù)返回值。
② 函數(shù)調(diào)用方負(fù)責(zé) release 函數(shù)返回值, 函數(shù)和函數(shù)調(diào)用方配合維護(hù)引用計(jì)數(shù), 適用于 alloc、copy、new、mutableCopy 之類的函數(shù)。
向?qū)ο蟀l(fā)送 autorelease 消息, 會發(fā)生什么?
將該對象加入到當(dāng)前 AutoreleasePoolPage 的 棧頂 next 指針 指向的位置。
autoreleased 對象的釋放時機(jī)? ★
加入到 autoreleasepool 中的對象, 是什么時候被釋放的?
未手加 AutoreleasePool 的情況下, Autoreleased 對象是在 當(dāng)前的 runloop 迭代結(jié)束時釋放的, 而它能夠釋放的原因是, 系統(tǒng)在每個 runloop 迭代中都加入了自動釋放池Push和Pop。
autoreleasepool 銷毀時, 會對 autoreleasepool 里面的所有對象做一次 release 操作。
autoreleasepool 銷毀時, 在調(diào)用棧中可以發(fā)現(xiàn), 系統(tǒng)調(diào)用了 [NSAutoreleasePool release] 方法, 這個方法最終通過調(diào)用 AutoreleasePoolPage::pop(void *) 函數(shù)來負(fù)責(zé)對 autoreleasepool 中的 autoreleased 對象執(zhí)行 release 操作。
雖然 autoreleased 對象并不都是在代碼塊結(jié)束后就釋放。但是他們有一個共同特性: 必定是在 @autoreleasepool 被銷毀時釋放。
所以要清楚 autoreleased 對象什么時候被釋放, 只需要搞清楚 @autoreleasepool 什么時候被銷毀即可。
在 ARC 下, 在線程中的臨時對象, 是在當(dāng)前線程的 Runloop 進(jìn)入休眠 或者 退出loop 或者 退出線程時被執(zhí)行release的。
main() 的 pool
// main() 添加的 autoreleasepool, 在程序退出時才會銷毀
// 項(xiàng)目中調(diào)用了 autorelease 的對象, 并不是被 main() 函數(shù)里添加的 autoreleasepool 管理的
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
手動添加的 pool
// MRC 下才能主動調(diào)用autorelease
@implementation Person
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
// 手動添加的pool, 對象 release 時機(jī)就是 `}` 結(jié)束 ★
@autoreleasepool {
Person *person = [[[Person alloc] init] autorelease];
}
NSLog(@"2");
}
@end
輸出
1
-[Person dealloc]
3
runloop 管理的 pool ★
// MRC
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 這個 person 什么時候調(diào)用 release, 是由 RunLoop 來控制的
// 它可能是在某次RunLoop循環(huán)中, RunLoop休眠之前調(diào)用了release
// 會在他所處的那一次 runloop 休眠之前, 被調(diào)用release ★
Person *person = [[[Person alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
@end
輸出:
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[Person dealloc]
-[ViewController viewDidAppear:]
可見, viewDidLoad 和 viewWillAppear 是處在同一次 runloop 中;
分析
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- (void)viewDidLoad {
[super viewDidLoad];
// 打印出當(dāng)前線程runloop的信息
NSLog(@"%@", NSRunLoop.currentRunLoop);
}
// 輸出可見, UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 所注冊的observer是完全一樣的, 說明observer是注冊到runloop上的, 所有mode共用
// 從輸出內(nèi)容中找到 與 AutoreleasePool 相關(guān)的 CFRunLoopObserver
// activities = 0x01 // kCFRunLoopEntry
<CFRunLoopObserver 0x60000118c140 [0x7fff8062d610]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600002ecc390 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa6ca00a048>\n)}}
// activities = 0xa0 // kCFRunLoopBeforeWaiting | kCFRunLoopExit (160 = 32 + 128)
<CFRunLoopObserver 0x60000118c1e0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600002ecc390 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa6ca00a048>\n)}}
結(jié)論 ★
iOS 在主線程的 Runloop 中注冊了2個 AutoreleasePool 相關(guān)的 Observer, 其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler
第1個 observer
監(jiān)聽了 kCFRunLoopEntry事件,進(jìn)入時會調(diào)用objc_autoreleasePoolPush()
-
第2個 observer
-
監(jiān)聽了 kCFRunLoopBeforeWaiting 事件, 休眠之前會先后調(diào)用
objc_autoreleasePoolPop()、objc_autoreleasePoolPush()所以休眠之前會將在這之前加入 pool 的對象, 統(tǒng)一調(diào)用一次 release
監(jiān)聽了 kCFRunLoopExit 事件, 退出時會調(diào)用
objc_autoreleasePoolPop()
-
這保證了push() 和 pop() 是成對出現(xiàn)的
<u>這個結(jié)論對于其他線程, 應(yīng)該也是適用的。</u>
autoreleasepool 釋放時機(jī)?
① MRC 下顯式的調(diào)用 drain 方法。
② 在 runloop 開始時, 都會隱式創(chuàng)建一個 autoreleasepool, 并會在 runloop 退出時, 把前面創(chuàng)建的 autoreleasepool drain。
釋放順序
主線程中既有系統(tǒng)創(chuàng)建的 @autoreleasepool, 也有開發(fā)者手動創(chuàng)建的 @autoreleasepool。那么他的釋放順序是怎樣的呢?
因?yàn)?@autoreleasepool 是以棧的形式存儲的, 按照先進(jìn)后出的規(guī)則, 釋放棧中每個@autoreleasepool。
系統(tǒng)創(chuàng)建的 pool 是在 Runloop 一開始創(chuàng)建的, 所以它必然是棧底的;
手動創(chuàng)建的 pool 是在 Runloop 運(yùn)行中創(chuàng)建的, 所以在系統(tǒng)的 pool 上面;
按照棧的規(guī)則, @autoreleasepool 是先釋放自行創(chuàng)建的 pool, 再釋放系統(tǒng)創(chuàng)建的。
與 線程 和 runloop 的關(guān)系:
有沒有想過我們直接調(diào)用 autorelease 方法就可以把釋放對象的任務(wù)交給 Autoreleasepool 對象, Autoreleasepool 對象從哪里來?
Autoreleasepool 對象又會在何時調(diào)用 [pool drain] 方法?
每一個線程, 包括主線程, 都會擁有一個專屬的 NSRunLoop 對象, 并且會在有需要的時候自動創(chuàng)建。
線程 與 Runloop 是一對一關(guān)系, 主線程中會自動創(chuàng)建 Runloop, 而子線程需要手動獲取。
子線程的 runloop 需要自己手動獲取, 如果子線程的 runloop 沒有任何事件, runloop會馬上退出。
在每個 loop 開始前, 系統(tǒng)會自動創(chuàng)建一個 autoreleasepool , 并在 loop 結(jié)束時 drain 。
NSAutoreleasePool 中還提到, 每一個線程都會維護(hù)自己的 autoreleasepool 堆棧。
換句話說 autoreleasepool 是與線程緊密相關(guān)的, 每一個 autoreleasepool 只對應(yīng)一個線程。(一對多???)
系統(tǒng)創(chuàng)建的 pool 和 RunLoop 的關(guān)系
見<>
在主線程執(zhí)行的代碼, 通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著, 所以不會出現(xiàn)內(nèi)存泄漏, 開發(fā)者也不必顯式創(chuàng)建 Pool 了。
也就是說 AutoreleasePool 創(chuàng)建是在一個RunLoop事件開始之前(push), AutoreleasePool釋放是在一個RunLoop事件即將結(jié)束之前(pop)。
AutoreleasePool 里的 Autorelease 對象的加入是在RunLoop事件中,
AutoreleasePool 里的 Autorelease 對象的釋放是在 AutoreleasePool 釋放時。
手動創(chuàng)建的 @autoreleasepool
@autoreleasepool { // 這個 `{` 開始, 創(chuàng)建自動釋放池, 內(nèi)部的對象自動加入autorelease
} // 這個 `}` 開始, 自動釋放池被銷毀。
子線程上的 autoreleasepool? ★
子線程默認(rèn)不會開啟 Runloop, 對象調(diào)用 autorelease 如何處理? 不手動處理會內(nèi)存泄漏嗎?
① 若當(dāng)前線程已經(jīng)創(chuàng)建了 Pool , Autoreleased 對象就會交給 pool 去管理。
② 若當(dāng)前線程沒有pool, 代碼調(diào)用順序?yàn)? autorelease -> autoreleaseFast -> autoreleaseNoPage。
在 autoreleaseNoPage 方法中, 會創(chuàng)建一個hotPage, 然后調(diào)用 page->add(obj) 將對象添加到 AutoreleasePoolPage 的棧中。
也就是說即使當(dāng)前線程沒有pool (沒有開啟runloop), 對象調(diào)用autorelease時, 也會 new 一個 AutoreleasepoolPage 出來管理autorelease對象, 不用擔(dān)心內(nèi)存泄漏。
<u>也就是說, 不一定要runloop開啟, 才能創(chuàng)建pool</u>
這個是 OS X 10.9+ 和 iOS 7+ 才加入的特性。并且蘋果沒有對應(yīng)的官方文檔闡述此事, 但是你可以通過源碼了解。
子線程的 AutoreleasepoolPage 和主線程的 page 有關(guān)聯(lián)嗎?
沒有, 釋放池和 線程是一一對應(yīng)的
實(shí)現(xiàn)原理 ★
先建立一個 autorelease pool;
對象從這個 autorelease pool 里面生成;
對象生成之后調(diào)用 autorelease 函數(shù), 這個函數(shù)的作用僅僅是在 autorelease pool 中做個標(biāo)記, 讓 pool 記得將來 release 一下這個對象;
當(dāng) pool 要把池中的對象 release 時, pool本身也需要rerlease, 此時 pool 會把每一個標(biāo)記為 autorelease 的對象 release 一次;
但, 如果某個對象此時 retain count 大于1, 這個對象還是沒有被銷毀;
被標(biāo)為autorelease的對象, 并不是等程序結(jié)束時才release, 如:
在 viewDidLoad 中創(chuàng)建一個 autorelease 對象, 在 viewDidLoad 走完, 一次消息循環(huán)完畢, 這個 autorelease pool 中的對象就會被 release;
button 的點(diǎn)擊事件, 點(diǎn)擊代碼執(zhí)行完會release一下autorelease類型的變量;
上面這個例子應(yīng)該這樣寫:
ClassName *myName = [[[ClassName alloc] init] autorelease]; // 標(biāo)記為autorelease
[classA setName:myName]; // retain count == 2 (如果myName的屬性是retain的話)
[myName release]; // retain count == 1, 注意, 在ClassA的dealloc中不能release name, 否則release pool時會release這個retain count為0的對象, 這是不對的。
記住一點(diǎn):
如果這個對象是你alloc或者new出來的, 你就需要調(diào)用release。如果使用autorelease, 那么僅在發(fā)生過retain的時候release一次(讓retain count始終為1)。
Autoreleasepool 對象從哪里來?
對于每一個Runloop運(yùn)行循環(huán), 系統(tǒng)會隱式創(chuàng)建一個 Autoreleasepool 對象,
+ (instancetype)student; 中執(zhí)行autorelease的操作, 就會將student對象添加到這個系統(tǒng)隱式創(chuàng)建的Autoreleasepool 中
Autoreleasepool 對象又會在何時調(diào)用 [pool drain] 方法?
當(dāng)Runloop執(zhí)行完一系列動作沒有更多事情要它做時, 它會進(jìn)入休眠狀態(tài), 避免一直占用大量系統(tǒng)資源, 或者Runloop要退出時, 會觸發(fā)執(zhí)行_objc_autoreleasePoolPop()方法, 相當(dāng)于讓 Autoreleasepool 對象執(zhí)行一次 drain 方法,
Autoreleasepool 對象會對自動釋放池中所有的對象依次執(zhí)行依次release操作
為什么對象在被釋放前, 打印出來的retainCount為1而不為0?
當(dāng)對象最后一次執(zhí)行release時, 系統(tǒng)知道馬上就要回收內(nèi)存了, 就沒有必要再將retainCount減1了, 因?yàn)椴还軠p不減1, 該對象都肯定會被回收, 而對象被回收后, 它的所有的內(nèi)存區(qū)域, 包括retainCount值也變得沒有意義。不將這個值從1變成0, 可以減少一次內(nèi)存的操作, 加速對象的回收。
每個線程只有一個 autoreleasepool 嗎?
可以手動添加 局部釋放池
一個線程有幾個 autoreleasepool棧?
autoreleasepool棧 的說法應(yīng)該是 autoreleasepoolPage, 一個autoreleasepool 可以有多個page
enumerateObjectsUsingBlock
使用容器的block版本的枚舉器時, 內(nèi)部會自動添加一個AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這里被一個局部 @autoreleasepool 包圍著
}];
當(dāng)然, 在普通for循環(huán)和for in循環(huán)中沒有, 所以, 還是新版的block版本枚舉器更加方便。for循環(huán)中遍歷產(chǎn)生大量autorelease變量時, 就需要手加局部AutoreleasePool咯。
當(dāng)一個 runloop 在不停的循環(huán)工作, 那么runloop每一次循環(huán)必定會經(jīng)過BeforeWaiting(準(zhǔn)備進(jìn)入休眠):而去BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊池并創(chuàng)建新池, 那么這兩個方法來銷毀要釋放的對象, 所以我們根本不需要擔(dān)心Autorelease的內(nèi)存管理問題, 這就是ARC背后的“高人”。
當(dāng)對象調(diào)用 autorelease 方法時, 會將對象加入 AutoreleasePoolPage 的棧中
調(diào)用 AutoreleasePoolPage::pop 方法會向棧中的對象發(fā)送 release 消息
什么對象會入池? ★
被加入 autoreleasepool 的對象, 怎樣進(jìn)行內(nèi)存管理?
對象被加入到離該對象最近的 autoreleasepool 中, 只有當(dāng)這個 pool drain 時, pool 中的 autoreleased 對象才會被 release ( 當(dāng)retainCount = 0 時對象才被釋放)。
未被加入 autoreleasepool 的對象, 怎樣進(jìn)行內(nèi)存管理?
未被加入 autoreleasepool 的對象, 都是由系統(tǒng)管理, 根據(jù)引用計(jì)數(shù)來控制釋放的時機(jī), 在適當(dāng)?shù)奈恢?release。
什么樣的對象會交給自動釋放池管理? ★
MRC下, 對象調(diào)用
autorelease方法;ARC下, 給對象添加
__autoreleasing修飾符;非自己創(chuàng)建的對象: 一般是系統(tǒng)提供的類方法 / 工廠方法, 創(chuàng)建的對象如
[NSArray array];。函數(shù)返回值: 對象作為方法的返回值時, 也會被附加上
__autoreleasing修飾符; (其實(shí)這就是非自己創(chuàng)建的對象)
什么是自己創(chuàng)建的對象?
自己創(chuàng)建的對象: 使用 alloc、new、copy、mutablecopy 及其駝峰變形 allocObject、newObject、copyObject、mutablecopyObject 的方法創(chuàng)建的對象;
自己創(chuàng)建的對象, 遵循
"誰創(chuàng)建, 誰釋放, 誰引用, 誰管理"的內(nèi)存管理原則, 在用完之后直接release
拿NSArray來舉例, 什么情況會入池?
使用
[[NSArray alloc] init];創(chuàng)建的對象, 不會入池。使用
[[[NSArray alloc] init] autorelease];(MRC下)調(diào)用了autorelease, 入池。使用
[NSArray array];即調(diào)用 Foundation 的類方法/工廠方法創(chuàng)建出來的對象, 入池。
id 的指針或?qū)ο蟮闹羔? 在沒有顯式指定修飾符時, 會被默認(rèn)加上 __autoreleasing 修飾符, 注冊到 autoreleasepool 中。
__weak對象
__weak 修飾的對象, 為了保證在引用時不被廢棄, 會注冊到 autoreleasepool 中。這是蘋果之前的實(shí)現(xiàn)
現(xiàn)在 ARC 不會 autorelease 弱引用對象, 而是直接 release
__weak 保證了弱指針使用期間, 狀態(tài)的一致性。LLVM8的最新實(shí)現(xiàn)是, 不推遲release到autoreleasepool, 而是直接release。
https://stackoverflow.com/questions/40993809/why-weak-object-will-be-added-to-autorelease-pool
In conclusion, this design of __weak ensure that during the usage of weak pointer, its state is consistent. The new implmenetation of __weak of Apple LLVM version 8.0.0 (clang-800.0.42.1) do not postponed the release to autoreleasepool, but use objc_release directly.
方法里有局部對象, 出了方法作用域會立刻釋放嗎?
答: 會立刻釋放
// ARC
@implementation Person
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
@end
輸出:
-[ViewController viewDidLoad]
-[Person dealloc]
-[ViewController viewWillAppear:]
可見 viewDidLoad 執(zhí)行完, 局部變量就 dealloc 了;
猜測 ARC 是在局部變量作用域結(jié)束之前, 插入了一句 release