目錄
- autorelease的本質(zhì)
- autorelease對(duì)象什么時(shí)候釋放托修?
- autoreleasepool的工作原理
- autoreleasepool的內(nèi)部結(jié)構(gòu)
- autoreleasepool的嵌套
- autoreleasePoolPage
- NSThread梁棠、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系
- 其他autorelease相關(guān)知識(shí)點(diǎn)
- 面試題
- 參考文章
autorelease的本質(zhì)
- autorelease
本質(zhì)
上就是延遲
調(diào)用release方法 - MRC環(huán)境疑故,通過(guò)調(diào)用
[obj autorelease]
來(lái)延遲
內(nèi)存的釋放 - ARC環(huán)境轨功,甚至可以
完全不知道
autorelease也能管理好內(nèi)存
autorelease對(duì)象什么時(shí)候釋放汗茄?
實(shí)驗(yàn)環(huán)境
- ARC
- 測(cè)試機(jī)型:
iPhone 4S模擬器
, -
注意:
在蘋(píng)果一些新的硬件設(shè)備上医咨,本實(shí)驗(yàn)的結(jié)果已經(jīng)不再成立
__weak NSString *_weakStr = nil;
- (void)viewDidLoad
{
[super viewDidLoad];
// 場(chǎng)景 1
NSString *string = [NSString stringWithFormat:@"yanhoo"];
_weakStr = string;
// 場(chǎng)景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"yanhoo"];
// _weakStr = string;
// }
// 場(chǎng)景 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);
}
測(cè)試結(jié)果
場(chǎng)景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)
場(chǎng)景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)
場(chǎng)景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)
分析
-
首先來(lái)了解下
__weak
修飾符-
不會(huì)
影響所指向?qū)ο蟮纳芷冢矗菏褂?code>__weak修飾的變量不會(huì)
導(dǎo)致所引用的對(duì)象的引用計(jì)數(shù)+1 - 當(dāng)
__weak
修飾的變量所指向的對(duì)象被釋放時(shí)架诞,__weak
修飾的變量的值會(huì)被置為nil
拟淮,不存在
野指針問(wèn)題
-
-
場(chǎng)景1分析
- 當(dāng)使用[NSString stringWithFormat:@"yanhoo"]創(chuàng)建一個(gè)
autorelease對(duì)象
時(shí),這個(gè)對(duì)象的引用計(jì)數(shù)為1谴忧,并且這個(gè)對(duì)象被系統(tǒng)自動(dòng)
添加到了最近
的autoreleasepool
中 - 當(dāng)使用
局部變量string
(在ARC
下不指定變量所有權(quán)修飾符的情況下很泊,默認(rèn)為__strong
)指向這個(gè)對(duì)象時(shí),這個(gè)對(duì)象的引用計(jì)數(shù) +1 沾谓,變成了 2 - 所以在
viewDidLoad
方法返回之前
委造,這個(gè)對(duì)象是一直存在的,且引用計(jì)數(shù)為2 - 當(dāng)
viewDidLoad
方法返回之后
均驶,局部變量 string被回收昏兆,其所指向?qū)ο蟮囊糜?jì)數(shù) -1 ,變成了1 - 在
viewWillAppear
方法中辣恋,我們?nèi)匀豢梢源蛴〕鲞@個(gè)對(duì)象的值亮垫,說(shuō)明這個(gè)對(duì)象并沒(méi)有被釋放,說(shuō)明到此autoreleasepool還未釋放
伟骨,從而導(dǎo)致autorelease對(duì)象
未釋放饮潦,因?yàn)橹挥挟?dāng)這個(gè)autoreleasepool
自身被drain
的時(shí)候,autoreleasepool
中的autoreleased 對(duì)象
才會(huì)被release
掉 - 在
viewDidAppear
中再打印這個(gè)對(duì)象的時(shí)候携狭,對(duì)象的值變成了 nil继蜡,說(shuō)明此時(shí)autoreleased對(duì)象
已經(jīng)被釋放了,可以大膽猜測(cè)autoreleasepool
一定在viewWillAppear
和viewDidAppear
方法之間的某個(gè)時(shí)候被drain
了
- 當(dāng)使用[NSString stringWithFormat:@"yanhoo"]創(chuàng)建一個(gè)
場(chǎng)景2和場(chǎng)景3請(qǐng)讀者自行分析逛腿,這里就不再啰嗦了
可以通過(guò)
lldb
的watchpoint
命令來(lái)觀察稀并,具體參考這篇文章-
總結(jié)
-
場(chǎng)景1
出現(xiàn)得最多,就是不需要我們手動(dòng)添加@autoreleasepool {}
的情況单默,直接使用系統(tǒng)維護(hù)
的autoreleasepool
碘举; -
場(chǎng)景2
就是需要我們手動(dòng)添加@autoreleasepool {}
的情況,手動(dòng)干預(yù) autoreleased對(duì)象
的釋放時(shí)機(jī)搁廓,在一些很耗內(nèi)存的循環(huán)調(diào)用的場(chǎng)景下有時(shí)需要手動(dòng)干預(yù)autoreleased 對(duì)象的釋放時(shí)機(jī)引颈,不然會(huì)導(dǎo)致內(nèi)存暴增,最終導(dǎo)致程序奔潰境蜕; -
場(chǎng)景3
是為了區(qū)別于
場(chǎng)景2而引入的蝙场,在這種場(chǎng)景下并不能
達(dá)到出了@autoreleasepool {}
的作用域時(shí)autoreleased 對(duì)象
被釋放的目的
-
autoreleasepool的工作原理
-
ARC
環(huán)境下,@autoreleasepool{ }
被編譯器編譯后粱年,生成如下代碼(以下代碼是簡(jiǎn)化版
)
// push
void *poolToken = objc_autoreleasePoolPush();
// 這中間為寫(xiě)在{...}中的代碼
// pop
objc_autoreleasePoolPop(poolToken);
- 在運(yùn)行循環(huán)
開(kāi)始前
售滤,系統(tǒng)會(huì)自動(dòng)創(chuàng)建
一個(gè)autoreleasepool
(一個(gè)
autoreleasepool會(huì)存在多個(gè)
AutoreleasePoolPage),此時(shí)會(huì)調(diào)用一次objc_autoreleasePoolPush
函數(shù),runtime會(huì)向當(dāng)前的AutoreleasePoolPage
中add進(jìn)一個(gè)POOL_SENTINEL
(哨兵對(duì)象
完箩,值為0赐俗,也就是個(gè)nil,代表autoreleasepool的起始邊界
)嗜憔,并返回此哨兵對(duì)象的內(nèi)存地址poolToken
- 在運(yùn)行循環(huán)
結(jié)束時(shí)
秃励,autoreleasepool
會(huì)被drain
掉,此時(shí)會(huì)調(diào)用objc_autoreleasePoolPop(poolToken)
函數(shù)吉捶,入?yún)⑹侵爱a(chǎn)生的POOL_SENTINEL
的內(nèi)存地址poolToken
,對(duì)在POOL_SENTINEL之后
添加的所有autoreleased對(duì)象
調(diào)用一次release
皆尔,可以向前
跨越若干個(gè)page呐舔,直到哨兵對(duì)象
所在的page,并向回移動(dòng)next指針
到哨兵對(duì)象所在位置
- 中間
{...}
所產(chǎn)生的autoreleased對(duì)象
都會(huì)被插入到最近
的autoreleasepool中(因?yàn)閍utoreleasepool存在嵌套
的情況) -
單個(gè)
autoreleasepool的運(yùn)行過(guò)程可以簡(jiǎn)單地理解為以下三個(gè)過(guò)程- objc_autoreleasePoolPush()
- objc_autoreleasePoolPush()
本質(zhì)
上就是調(diào)用的 AutoreleasePoolPage的push函數(shù)慷蠕,如下所示
void * objc_autoreleasePoolPush(void) { if (UseGC) return nil; return AutoreleasePoolPage::push(); }
- 每執(zhí)行一次
push
操作就會(huì)新建
一個(gè)autoreleasepool珊拼,對(duì)應(yīng)的具體實(shí)現(xiàn)就是往AutoreleasePoolPage中的next位置
插入一個(gè)POOL_SENTINEL
,并且返回
插入的POOL_SENTINEL的內(nèi)存地址poolToken
流炕,這個(gè)地址在執(zhí)行pop
操作的時(shí)候作為函數(shù)的入?yún)⑴煜郑旅媸茿utoreleasePoolPage的push函數(shù)代碼
static inline void *push() { id *dest = autoreleaseFast(POOL_SENTINEL); assert(*dest == POOL_SENTINEL); return dest; }
- push 函數(shù)通過(guò)調(diào)
autoreleaseFast
函數(shù)來(lái)執(zhí)行具體的插入操作
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) {// 當(dāng)前 page 存在且沒(méi)有滿(mǎn)時(shí),直接將對(duì)象添加到當(dāng)前 page 中每辟,即 next 指向的位置 return page->add(obj); } else if (page) {// 當(dāng)前 page 存在且已滿(mǎn)時(shí)剑辫,創(chuàng)建一個(gè)新的 page ,并將對(duì)象添加到新創(chuàng)建的 page 中 return autoreleaseFullPage(obj, page); } else {// 當(dāng)前 page 不存在時(shí)渠欺,即還沒(méi)有 page 時(shí)妹蔽,創(chuàng)建第一個(gè) page ,并將對(duì)象添加到新創(chuàng)建的 page 中 return autoreleaseNoPage(obj); } }
- objc_autoreleasePoolPush()
- [對(duì)象 autorelease]
- 本質(zhì)上就是調(diào)用AutoreleasePoolPage的autorelease函數(shù)
__attribute__((noinline,used)) id objc_object::rootAutorelease2() { assert(!isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this); }
- AutoreleasePoolPage 的 autorelease 函數(shù)的實(shí)現(xiàn)對(duì)我們來(lái)說(shuō)就比較好理解了挠将,它跟 push 操作的實(shí)現(xiàn)非常相似胳岂。只不過(guò)
push 操作
插入的是一個(gè)POOL_SENTINEL
,而autorelease 操作
插入的是一個(gè)具體的autoreleased 對(duì)象
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
乳丰。當(dāng)執(zhí)行pop
操作時(shí),在POOL_SENTINEL內(nèi)存地址在之后
添加的所有autoreleased 對(duì)象
都會(huì)被release
内贮,可以向前
跨越若干個(gè)page产园,直到哨兵對(duì)象所在的page,并向回移動(dòng)next指針
到哨兵對(duì)象所在位置
- objc_autoreleasePoolPush()
autoreleasepool的內(nèi)部結(jié)構(gòu)
- autoreleasepool
本質(zhì)
上就是一個(gè)指針堆棧
-
指針堆棧
中存放的是autoreleased對(duì)象
的內(nèi)存地址 或者POOL_SENTINEL
的內(nèi)存地址 - 內(nèi)部結(jié)構(gòu)是由若干個(gè)
以page為結(jié)點(diǎn)的雙向鏈表
組成贺归,系統(tǒng)會(huì)在需要的時(shí)候動(dòng)態(tài)地增加或刪除
page節(jié)點(diǎn)淆两,這里說(shuō)的page就是下面即將說(shuō)到的AutoreleasePoolPage
對(duì)象
autoreleasepool的嵌套
- 每產(chǎn)生
一個(gè)autoreleasePool
,就會(huì)產(chǎn)生一個(gè)哨兵對(duì)象
拂酣,作為pool的邊界
- pool的
嵌套
其實(shí)就是產(chǎn)生多個(gè)哨兵對(duì)象
而已 - pop的時(shí)候可以
向前
跨越若干個(gè)page秋冰,直到指定哨兵對(duì)象
所在的page為止
AutoreleasePoolPage
-
一個(gè)
空的
AutoreleasePoolPage 的內(nèi)存結(jié)構(gòu)
如下圖所示:-
magic
用來(lái)校驗(yàn)
AutoreleasePoolPage的結(jié)構(gòu)是否完整
; -
next
指向下一個(gè)即將產(chǎn)生的autoreleased對(duì)象
的存放位置(當(dāng)next == begin()
時(shí)婶熬,表示AutoreleasePoolPage為空
剑勾;當(dāng)next == end()
時(shí)埃撵,表示AutoreleasePoolPage已滿(mǎn)
) -
thread
指向當(dāng)前線程
,一個(gè)
AutoreleasePoolPage只會(huì)對(duì)應(yīng)一個(gè)線程
虽另,但一個(gè)線程
可以對(duì)應(yīng)多個(gè)
AutoreleasePoolPage暂刘; -
parent
指向父結(jié)點(diǎn),第一個(gè)
結(jié)點(diǎn)的 parent 值為 nil捂刺; -
child
指向子結(jié)點(diǎn)谣拣,最后一個(gè)
結(jié)點(diǎn)的 child 值為 nil; -
depth
代表深度族展,第一個(gè)page的depth為0森缠,往后每遞增一個(gè)page,depth會(huì)加1仪缸; -
hiwat
代表 high water mark
-
前面所說(shuō)的autoreleasepool的內(nèi)部結(jié)構(gòu)是由
若干個(gè)
以AutoreleasePoolPage
為結(jié)點(diǎn)的雙向鏈表
組成贵涵,這個(gè)雙向鏈表
就是通過(guò)上述結(jié)構(gòu)中的parent指針
和child指針
連接起來(lái)的每個(gè)AutoreleasePoolPage對(duì)象會(huì)開(kāi)辟
4KB
內(nèi)存(也就是虛擬內(nèi)存一頁(yè)
的大小)恰画,除了上面的實(shí)例變量所占空間宾茂,剩下的空間全部用來(lái)儲(chǔ)存autoreleased對(duì)象的內(nèi)存地址
一個(gè)page的空間被占滿(mǎn)時(shí),會(huì)新建一個(gè)page拴还,通過(guò)
parent指針
和child指針
連接鏈表跨晴,之后的autoreleased對(duì)象
會(huì)加入到新建的page中
NSThread、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系
- NSThread 和 NSRunLoop是
一一對(duì)應(yīng)
的關(guān)系 - 在NSRunLoop對(duì)象的每個(gè)
運(yùn)行循環(huán)(event loop)
開(kāi)始前自沧,系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè)autoreleasepool坟奥,并在運(yùn)行循環(huán)(event loop)
結(jié)束時(shí)drain
掉這個(gè)pool,同時(shí)釋放所有autoreleased對(duì)象 -
autoreleasepool
只會(huì)對(duì)應(yīng)一個(gè)
線程拇厢,每個(gè)線程
可能會(huì)對(duì)應(yīng)多個(gè)
autoreleasepool爱谁,比如autoreleasepool嵌套
的情況
Autorelease返回值的快速釋放機(jī)制
- ARC下,runtime有一套對(duì)autorelease返回值的優(yōu)化策略
- 通過(guò)
objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
的配合使用可以達(dá)到最優(yōu)化
程序運(yùn)行的目的 -
Thread Local Storage(TLS)
線程局部存儲(chǔ)孝偎,在返回值
返回前調(diào)用objc_autoreleaseReturnValue
方法時(shí)访敌,runtime會(huì)將這個(gè)返回值
儲(chǔ)存在TLS
中,然后直接返回返回值
(不調(diào)用
autorelease)衣盾, - 在
外部接收
這個(gè)返回值
時(shí)通過(guò)調(diào)用objc_retainAutoreleasedReturnValue
發(fā)現(xiàn)TLS
中已存在這個(gè)返回值
寺旺,就直接返回(不調(diào)用retain
),免去了對(duì)返回值
的內(nèi)存管理势决,達(dá)到優(yōu)化目的
其他Autorelease相關(guān)知識(shí)點(diǎn)
- 使用容器的block版本的枚舉器時(shí)阻塑,內(nèi)部會(huì)
自動(dòng)添加
一個(gè)autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
{
// 這里被一個(gè)局部@autoreleasepool{ }包圍著
}];
普通
for循環(huán)
和for in循環(huán)
中沒(méi)有
,所以果复,還是新版的block版本枚舉器更加方便陈莽,但是性能還是for in循環(huán)
最高-
下面三種情況是需要我們
手動(dòng)
添加autoreleasepool- 如果你編寫(xiě)的程序
不是基于 UI 框架
的,比如:命令行工具; - for循環(huán)中遍歷產(chǎn)生
大量
autorelease變量時(shí)走搁,就需要手動(dòng)
添加加局部
autoreleasePool來(lái)進(jìn)行手動(dòng)干預(yù)
- 如果你創(chuàng)建了一個(gè)
子線程
独柑,一般會(huì)自定義繼承自NSOperation
的操作,在main方法中要加上@autoreleasepool{...}
私植,這段代碼是在子線程上執(zhí)行是無(wú)法訪問(wèn)主線程
的自動(dòng)釋放池的忌栅,所以得自己創(chuàng)建
- (void)main { // 自己創(chuàng)建自動(dòng)釋放池,如果這段代碼是在子線程上執(zhí)行是無(wú)法訪問(wèn)主線程的自動(dòng)釋放池的曲稼,所以得自己創(chuàng)建 @autoreleasepool { // 代碼邏輯 } }
- 如果你編寫(xiě)的程序
面試題
- autoreleasepool的實(shí)現(xiàn)原理