前言
本文是上一篇:搞懂Objective-C中的ARC的延伸和補充
- 上一篇討論了下面幾個問題
1、iOS操作系統(tǒng)內(nèi)存分區(qū)
2、Objective-C中的指針
3、什么是ARC,引用計數(shù)存儲在哪里,哪些對象是通過引用計數(shù)來管理內(nèi)存的
4、ARC下哪些對象是autorelease對象
- 本文將討論下面幾個問題
1、autorelease對象的釋放時機
2、為什么需要手動添加autoreleasePool
3、autoreleasePool的源碼實現(xiàn)
4、autoreleasePool為什么用雙向鏈表來實現(xiàn)呢
從線程池說起
操作系統(tǒng)實現(xiàn)了一個線程工廠稱為線程池,還有一個配合工作的管理器稱為調(diào)度中心,iOS系統(tǒng)并沒有接口去直接操作線程池,線程的創(chuàng)建、調(diào)度、銷毀全權(quán)交給線程池,當我們試圖去創(chuàng)建一個線程的時候,調(diào)度中心會檢查線程池的運行狀態(tài)、運行線程數(shù)、運行策略,決定接下來的執(zhí)行流程,是直接申請線程執(zhí)行還是放入緩沖隊列中,待到執(zhí)行的時候,線程池決定是開辟新的線程還是復(fù)用現(xiàn)有空閑線程,長時間空閑的線程會被回收(一般10s左右)
iOS線程對應(yīng)的數(shù)據(jù)結(jié)構(gòu)
iOS操作系統(tǒng)為線程運行配備了一套數(shù)據(jù)結(jié)構(gòu),一個棧、一個autoreleasePool、一個runloop(懶加載)以及一些用于控制狀態(tài)的標志位變量
- 棧:存放函數(shù)的參數(shù)值與局部變量的值等
- autoreleasePool:存放當前線程產(chǎn)生的autorelease對象
- runloop:一個while循環(huán),用于線程可持續(xù)運行的一個循環(huán),蘋果將它封裝成一個類,內(nèi)部注冊了一些觀察者,實現(xiàn)了一套狀態(tài)機,動可執(zhí)行,靜可休眠
好的~我們回到問題本身
autorelease對象的釋放時機
autorelease對象何時釋放,主線程與非主線程的釋放時機有何不同?為什么需要手動添加autoreleasePool?
這個問題是不是經(jīng)常被問到,有真正理解過嘛~
先說結(jié)論:
加入到autoreleasePool中的對象,會在線程銷毀前,傾倒一次autoreleasePool,取出每個對象,發(fā)送一次release消息,對象的引用計數(shù)-1,此時對象是否釋放取決與引用計數(shù)是否為0
主線程的運行循環(huán)在app裝載、鏈接以后就會在main函數(shù)中啟動,這是常規(guī)設(shè)計,所以主線程不會被銷毀直到app進程退出之前,這就導(dǎo)致上面的策略無法釋放autorelease對象,于是蘋果在runloop中加入這部分對象的釋放時機,每次循環(huán)同步執(zhí)行完,在進入休眠之前,會傾倒一次autoreleasePool
通常我們執(zhí)行完現(xiàn)有任務(wù)就無需再次使用這個線程,所以非主線程的runloop無需啟動(特殊場景的需求除外),這就導(dǎo)致一個問題,非主線程的autorelease對象直到線程退出之前才會得到引用計數(shù)-1,當這個線程有長任務(wù)的時候,內(nèi)存會一直漲,這與峰值無關(guān),會一直漲到OOM
基于以上問題,蘋果公開了autoreleasePool的api,在非主線程的場景下,手動添加@autoreleasepool{},在autoreleasepool{}作用域結(jié)束前會傾倒一次
所謂嵌套autoreleasepool,不過是一個線程對應(yīng)了多個autoreleasepool,默認有一個,其他的是手動添加的,根本不存在嵌套一說,都只負責各自作用域產(chǎn)生的autorelease對象
主線程同樣有需要手動添加autoreleasepool的場景,可以降低內(nèi)存峰值,iOS屏幕的刷新幀率是60,即一秒刷新60次,一次約16.7毫秒,如果在這個時間內(nèi),一次運行循環(huán)還沒有執(zhí)行完的話就會出現(xiàn)卡頓現(xiàn)象,因為系統(tǒng)的vsync信號過來的時候,app沒有準備好下一幀圖像,所以看上去就是卡頓了,根本原因是因為主線程阻塞導(dǎo)致16.7毫秒內(nèi)沒有把圖像提交到緩沖區(qū)(說遠了),基于以上OOM通常不會出現(xiàn)在主線程,因為已經(jīng)卡爆了,調(diào)試過程中就會發(fā)現(xiàn),所以這里用于降低內(nèi)存峰值
再看個demo,這里只討論非主線程:
- (void)testRelease {
dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
while (1) {
NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆區(qū)string-release"];
NSLog(@"當前線程:%@, 值:%@", [NSThread currentThread], heapAreastring);
}
});
}
如上創(chuàng)建了一個串行隊列,寫了個死循環(huán),一直在創(chuàng)建堆區(qū)字符串,但是內(nèi)存不會漲一絲一毫,原因上一篇文章搞懂Objective-C中的ARC講過,這個對象直接會被release,不會加入autoreleasePool
修改一下:
- (void)testAutorelease {
dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
while (1) {
NSString *stringAutorelease = [NSString stringWithFormat:@"堆區(qū)string-autorelease"];
NSLog(@"當前線程:%@, 值:%@", [NSThread currentThread], heapAreastring);
}
});
}
會發(fā)現(xiàn)非主線程創(chuàng)建autorelease對象會導(dǎo)致內(nèi)存無休止的在漲,直到OOM
再次修改:
- (void)testAutoreleaseWithAutoreleasepool {
dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
while (1) {
@autoreleasepool {
NSString *stringAutorelease = [NSString stringWithFormat:@"堆區(qū)string-autorelease"];
NSLog(@"當前線程:%@, 值:%@", [NSThread currentThread], stringAutorelease);
}
}
});
}
結(jié)果是內(nèi)存也不會漲一絲一毫,數(shù)據(jù)剛好能對應(yīng)上面的結(jié)論,對于有知其所以然需求的可以往下看
autoreleasePool的源碼實現(xiàn)
前輩這篇講得很好了,本文會基于最新源碼做個淺析(可能不是最新源碼,作者開始看源碼時候的版本objc4-781.2),順便講述下如何查閱源碼,源碼鏈接
在源碼中搜索
會發(fā)現(xiàn)AutoreleasePoolPage是一個c++的類,繼承AutoreleasePoolPageData,具體實現(xiàn)很長,這里省去部分函數(shù)實現(xiàn)
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
void releaseAll()
{
releaseUntil(begin());
}
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
static void tls_dealloc(void *p)
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
// No objects or pool pages to clean up here.
return;
}
// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);
if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) objc_autoreleasePoolPop(page->begin()); // pop all of the pools
if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}
// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();
token = page->begin();
} else {
page = pageForPointer(token);
}
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
return popPage<false>(token, page, stop);
}
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
}
還是很長是不是,但是對于理解原理都重要,看看蘋果自己的注釋
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/
/***********************************************************************
Autorelease pool 的實現(xiàn)
線程的Autorelease pool是一系列聲明在棧上的指針。
每個指針要么是一個需要釋放的對象,要么是 POOL_BOUNDARY,它是
Autorelease pool邊界。
token是指向該池的 POOL_BOUNDARY 的指針。 什么時候
池被彈出,每個比哨兵更熱的對象都被釋放。
棧被分成一個雙向鏈接的AutoreleasePoolPage列表。 Pages根據(jù)需要,添加
和刪除。
線程本地存儲指向最新被存儲到自動釋池所在的熱點頁面
。
**********************************************************************/
看完這個注釋,我覺得都不用往下講了??,一個字,清晰透徹
autorelease對象加入到自動釋放池的流程
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置,當有新的obj加入autoreleasePool的時候取到next指針的地址存儲obj,next指針向棧頂方向移動8個字節(jié)
runloop沒有啟動的線程 autorelease對象的釋放流程
每個autoreleasePool初始化的時候都會綁定當前線程,如下是AutoreleasePoolPage的初始化函數(shù)
static void init()
{
int r __unused = pthread_key_init_np(AutoreleasePoolPage::key,
AutoreleasePoolPage::tls_dealloc);
ASSERT(r == 0);
}
第一個參數(shù)是AutoreleasePoolPage的標識,第二個參數(shù)是一個函數(shù)指針,線程退出之前會執(zhí)行這個函數(shù),就是tls_dealloc,調(diào)用棧如下:
1、tls_dealloc中會取AutoreleasePoolPage *page = coldPage()
2、page調(diào)用objc_autoreleasePoolPop(page->begin())函數(shù)
3、調(diào)用棧如下objc_autoreleasePoolPop->pop->popPage->releaseUntil
4、releaseUntil中取hotPage(),然后通過一個循環(huán),取next游標指針id obj = *--page->next,調(diào)用objc_release(obj),然后游標指針向棧底移動8個字節(jié)
5、所有autorelease對象清理干凈,最后調(diào)用kill()函數(shù)遍歷釋放所有autoreleasePage
runloop啟動的線程 autorelease對象的釋放流程
線程休眠之前,runloop會調(diào)用objc_autoreleasePoolPop,上面流程除了第一步,后面一樣
autoreleasePool為什么用雙向鏈表來實現(xiàn)呢
單向的可以嗎?可以呀,但是查詢速度會以指數(shù)級別降低,刪除棧頂page后想刪除的倒數(shù)第二個page,怎么辦,從頭開始遍歷唄,遍歷n-1次,然后遍歷n-2次.......
所以為什么設(shè)計成雙向鏈表,效率高,如此而已