目錄
- autorelease的本質(zhì)
- autorelease對象什么時候釋放?
- autoreleasepool的工作原理
- autoreleasepool的內(nèi)部結(jié)構(gòu)
- autoreleasepool的嵌套
- autoreleasePoolPage
- NSThread、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系
- 其他autorelease相關(guān)知識點
- 面試題
- 參考文章
autorelease的本質(zhì)
- autorelease
本質(zhì)
上就是延遲
調(diào)用release方法 - MRC環(huán)境,通過調(diào)用
[obj autorelease]
來延遲
內(nèi)存的釋放 - ARC環(huán)境,甚至可以
完全不知道
autorelease也能管理好內(nèi)存
autorelease對象什么時候釋放?
實驗環(huán)境
- ARC
- 測試機型:
iPhone 4S模擬器
, -
注意:
在蘋果一些新的硬件設(shè)備上,本實驗的結(jié)果已經(jīng)不再成立
__weak NSString *_weakStr = nil;
- (void)viewDidLoad
{
[super viewDidLoad];
// 場景 1
NSString *string = [NSString stringWithFormat:@"yanhoo"];
_weakStr = string;
// 場景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"yanhoo"];
// _weakStr = string;
// }
// 場景 3
// NSString *string = nil;
// @autoreleasepool {
// string = [NSString stringWithFormat:@"yanhoo"];
// _weakStr = string;
// }
NSLog(@"string1: %@", _weakStr);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"string2: %@", _weakStr);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"string3: %@", _weakStr);
}
測試結(jié)果
場景1
2016-08-19 01:30:01.686 test[8866:554553] string1: yanhoo
2016-08-19 01:30:01.687 test[8866:554553] string2: yanhoo
2016-08-19 01:30:01.695 test[8866:554553] string3: (null)
場景2
2016-08-19 01:32:07.020 test[8886:556042] string1: (null)
2016-08-19 01:32:07.021 test[8886:556042] string2: (null)
2016-08-19 01:32:07.032 test[8886:556042] string3: (null)
場景3
2016-08-19 01:32:57.038 test[8900:557349] string1: yanhoo
2016-08-19 01:32:57.038 test[8900:557349] string2: (null)
2016-08-19 01:32:57.048 test[8900:557349] string3: (null)
分析
-
首先來了解下
__weak
修飾符-
不會
影響所指向?qū)ο蟮纳芷冢矗菏褂?code>__weak修飾的變量不會
導(dǎo)致所引用的對象的引用計數(shù)+1 - 當
__weak
修飾的變量所指向的對象被釋放時,__weak
修飾的變量的值會被置為nil
,不存在
野指針問題
-
-
場景1分析
- 當使用[NSString stringWithFormat:@"yanhoo"]創(chuàng)建一個
autorelease對象
時,這個對象的引用計數(shù)為1,并且這個對象被系統(tǒng)自動
添加到了最近
的autoreleasepool
中 - 當使用
局部變量string
(在ARC
下不指定變量所有權(quán)修飾符的情況下,默認為__strong
)指向這個對象時,這個對象的引用計數(shù) +1 ,變成了 2 - 所以在
viewDidLoad
方法返回之前
,這個對象是一直存在的,且引用計數(shù)為2 - 當
viewDidLoad
方法返回之后
,局部變量 string被回收,其所指向?qū)ο蟮囊糜嫈?shù) -1 ,變成了1 - 在
viewWillAppear
方法中,我們?nèi)匀豢梢源蛴〕鲞@個對象的值,說明這個對象并沒有被釋放,說明到此autoreleasepool還未釋放
,從而導(dǎo)致autorelease對象
未釋放,因為只有當這個autoreleasepool
自身被drain
的時候,autoreleasepool
中的autoreleased 對象
才會被release
掉 - 在
viewDidAppear
中再打印這個對象的時候,對象的值變成了 nil,說明此時autoreleased對象
已經(jīng)被釋放了,可以大膽猜測autoreleasepool
一定在viewWillAppear
和viewDidAppear
方法之間的某個時候被drain
了
- 當使用[NSString stringWithFormat:@"yanhoo"]創(chuàng)建一個
場景2和場景3請讀者自行分析,這里就不再啰嗦了
可以通過
lldb
的watchpoint
命令來觀察,具體參考這篇文章-
總結(jié)
-
場景1
出現(xiàn)得最多,就是不需要我們手動添加@autoreleasepool {}
的情況,直接使用系統(tǒng)維護
的autoreleasepool
; -
場景2
就是需要我們手動添加@autoreleasepool {}
的情況,手動干預(yù) autoreleased對象
的釋放時機,在一些很耗內(nèi)存的循環(huán)調(diào)用的場景下有時需要手動干預(yù)autoreleased 對象的釋放時機,不然會導(dǎo)致內(nèi)存暴增,最終導(dǎo)致程序奔潰; -
場景3
是為了區(qū)別于
場景2而引入的,在這種場景下并不能
達到出了@autoreleasepool {}
的作用域時autoreleased 對象
被釋放的目的
-
autoreleasepool的工作原理
-
ARC
環(huán)境下,@autoreleasepool{ }
被編譯器編譯后,生成如下代碼(以下代碼是簡化版
)
// push
void *poolToken = objc_autoreleasePoolPush();
// 這中間為寫在{...}中的代碼
// pop
objc_autoreleasePoolPop(poolToken);
- 在運行循環(huán)
開始前
,系統(tǒng)會自動創(chuàng)建
一個autoreleasepool
(一個
autoreleasepool會存在多個
AutoreleasePoolPage),此時會調(diào)用一次objc_autoreleasePoolPush
函數(shù),runtime會向當前的AutoreleasePoolPage
中add進一個POOL_SENTINEL
(哨兵對象
,值為0,也就是個nil,代表autoreleasepool的起始邊界
),并返回此哨兵對象的內(nèi)存地址poolToken
- 在運行循環(huán)
結(jié)束時
,autoreleasepool
會被drain
掉,此時會調(diào)用objc_autoreleasePoolPop(poolToken)
函數(shù),入?yún)⑹侵爱a(chǎn)生的POOL_SENTINEL
的內(nèi)存地址poolToken
,對在POOL_SENTINEL之后
添加的所有autoreleased對象
調(diào)用一次release
,可以向前
跨越若干個page,直到哨兵對象
所在的page,并向回移動next指針
到哨兵對象所在位置
- 中間
{...}
所產(chǎn)生的autoreleased對象
都會被插入到最近
的autoreleasepool中(因為autoreleasepool存在嵌套
的情況) -
單個
autoreleasepool的運行過程可以簡單地理解為以下三個過程- objc_autoreleasePoolPush()
- objc_autoreleasePoolPush()
本質(zhì)
上就是調(diào)用的 AutoreleasePoolPage的push函數(shù),如下所示
void * objc_autoreleasePoolPush(void) { if (UseGC) return nil; return AutoreleasePoolPage::push(); }
- 每執(zhí)行一次
push
操作就會新建
一個autoreleasepool,對應(yīng)的具體實現(xiàn)就是往AutoreleasePoolPage中的next位置
插入一個POOL_SENTINEL
,并且返回
插入的POOL_SENTINEL的內(nèi)存地址poolToken
,這個地址在執(zhí)行pop
操作的時候作為函數(shù)的入?yún)ⅲ旅媸茿utoreleasePoolPage的push函數(shù)代碼
static inline void *push() { id *dest = autoreleaseFast(POOL_SENTINEL); assert(*dest == POOL_SENTINEL); return dest; }
- push 函數(shù)通過調(diào)
autoreleaseFast
函數(shù)來執(zhí)行具體的插入操作
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) {// 當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置 return page->add(obj); } else if (page) {// 當前 page 存在且已滿時,創(chuàng)建一個新的 page ,并將對象添加到新創(chuàng)建的 page 中 return autoreleaseFullPage(obj, page); } else {// 當前 page 不存在時,即還沒有 page 時,創(chuàng)建第一個 page ,并將對象添加到新創(chuàng)建的 page 中 return autoreleaseNoPage(obj); } }
- objc_autoreleasePoolPush()
- [對象 autorelease]
- 本質(zhì)上就是調(diào)用AutoreleasePoolPage的autorelease函數(shù)
__attribute__((noinline,used)) id objc_object::rootAutorelease2() { assert(!isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this); }
- AutoreleasePoolPage 的 autorelease 函數(shù)的實現(xiàn)對我們來說就比較好理解了,它跟 push 操作的實現(xiàn)非常相似。只不過
push 操作
插入的是一個POOL_SENTINEL
,而autorelease 操作
插入的是一個具體的autoreleased 對象
static inline id autorelease(id obj) { assert(obj); assert(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); assert(!dest || *dest == obj); return obj; }
- objc_autoreleasePoolPop(poolToken)
- objc_autoreleasePoolPop(poolToken) 函數(shù)本質(zhì)上也是調(diào)用的AutoreleasePoolPage的 pop 函數(shù)
void objc_autoreleasePoolPop(void *ctxt) { if (UseGC) return; // fixme rdar://9167170 if (!ctxt) return; AutoreleasePoolPage::pop(ctxt); }
- pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值,也就是 POOL_SENTINEL 的內(nèi)存地址
poolToken
。當執(zhí)行pop
操作時,在POOL_SENTINEL內(nèi)存地址在之后
添加的所有autoreleased 對象
都會被release
,可以向前
跨越若干個page,直到哨兵對象所在的page,并向回移動next指針
到哨兵對象所在位置
- objc_autoreleasePoolPush()
autoreleasepool的內(nèi)部結(jié)構(gòu)
- autoreleasepool
本質(zhì)
上就是一個指針堆棧
-
指針堆棧
中存放的是autoreleased對象
的內(nèi)存地址 或者POOL_SENTINEL
的內(nèi)存地址 - 內(nèi)部結(jié)構(gòu)是由若干個
以page為結(jié)點的雙向鏈表
組成,系統(tǒng)會在需要的時候動態(tài)地增加或刪除
page節(jié)點,這里說的page就是下面即將說到的AutoreleasePoolPage
對象
autoreleasepool的嵌套
- 每產(chǎn)生
一個autoreleasePool
,就會產(chǎn)生一個哨兵對象
,作為pool的邊界
- pool的
嵌套
其實就是產(chǎn)生多個哨兵對象
而已 - pop的時候可以
向前
跨越若干個page,直到指定哨兵對象
所在的page為止
AutoreleasePoolPage
-
一個
空的
AutoreleasePoolPage 的內(nèi)存結(jié)構(gòu)
如下圖所示:AutoreleasePoolPage.png-
magic
用來校驗
AutoreleasePoolPage的結(jié)構(gòu)是否完整
; -
next
指向下一個即將產(chǎn)生的autoreleased對象
的存放位置(當next == begin()
時,表示AutoreleasePoolPage為空
;當next == end()
時,表示AutoreleasePoolPage已滿
) -
thread
指向當前線程
,一個
AutoreleasePoolPage只會對應(yīng)一個線程
,但一個線程
可以對應(yīng)多個
AutoreleasePoolPage; -
parent
指向父結(jié)點,第一個
結(jié)點的 parent 值為 nil; -
child
指向子結(jié)點,最后一個
結(jié)點的 child 值為 nil; -
depth
代表深度,第一個page的depth為0,往后每遞增一個page,depth會加1; -
hiwat
代表 high water mark
-
前面所說的autoreleasepool的內(nèi)部結(jié)構(gòu)是由
若干個
以AutoreleasePoolPage
為結(jié)點的雙向鏈表
組成,這個雙向鏈表
就是通過上述結(jié)構(gòu)中的parent指針
和child指針
連接起來的每個AutoreleasePoolPage對象會開辟
4KB
內(nèi)存(也就是虛擬內(nèi)存一頁
的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autoreleased對象的內(nèi)存地址
一個page的空間被占滿時,會新建一個page,通過
parent指針
和child指針
連接鏈表,之后的autoreleased對象
會加入到新建的page中
NSThread、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系
- NSThread 和 NSRunLoop是
一一對應(yīng)
的關(guān)系 - 在NSRunLoop對象的每個
運行循環(huán)(event loop)
開始前,系統(tǒng)會自動創(chuàng)建一個autoreleasepool,并在運行循環(huán)(event loop)
結(jié)束時drain
掉這個pool,同時釋放所有autoreleased對象 -
autoreleasepool
只會對應(yīng)一個
線程,每個線程
可能會對應(yīng)多個
autoreleasepool,比如autoreleasepool嵌套
的情況
Autorelease返回值的快速釋放機制
- ARC下,runtime有一套對autorelease返回值的優(yōu)化策略
- 通過
objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
的配合使用可以達到最優(yōu)化
程序運行的目的 -
Thread Local Storage(TLS)
線程局部存儲,在返回值
返回前調(diào)用objc_autoreleaseReturnValue
方法時,runtime會將這個返回值
儲存在TLS
中,然后直接返回返回值
(不調(diào)用
autorelease), - 在
外部接收
這個返回值
時通過調(diào)用objc_retainAutoreleasedReturnValue
發(fā)現(xiàn)TLS
中已存在這個返回值
,就直接返回(不調(diào)用retain
),免去了對返回值
的內(nèi)存管理,達到優(yōu)化目的
其他Autorelease相關(guān)知識點
- 使用容器的block版本的枚舉器時,內(nèi)部會
自動添加
一個autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
{
// 這里被一個局部@autoreleasepool{ }包圍著
}];
普通
for循環(huán)
和for in循環(huán)
中沒有
,所以,還是新版的block版本枚舉器更加方便,但是性能還是for in循環(huán)
最高-
下面三種情況是需要我們
手動
添加autoreleasepool- 如果你編寫的程序
不是基于 UI 框架
的,比如:命令行工具; - for循環(huán)中遍歷產(chǎn)生
大量
autorelease變量時,就需要手動
添加加局部
autoreleasePool來進行手動干預(yù)
- 如果你創(chuàng)建了一個
子線程
,一般會自定義繼承自NSOperation
的操作,在main方法中要加上@autoreleasepool{...}
,這段代碼是在子線程上執(zhí)行是無法訪問主線程
的自動釋放池的,所以得自己創(chuàng)建
- (void)main { // 自己創(chuàng)建自動釋放池,如果這段代碼是在子線程上執(zhí)行是無法訪問主線程的自動釋放池的,所以得自己創(chuàng)建 @autoreleasepool { // 代碼邏輯 } }
- 如果你編寫的程序
面試題
- autoreleasepool的實現(xiàn)原理