搞懂Objective-C中的autorelease

前言

本文是上一篇:搞懂Objective-C中的ARC的延伸和補(bǔ)充

  • 上一篇討論了下面幾個(gè)問題

1藻烤、iOS操作系統(tǒng)內(nèi)存分區(qū)
2、Objective-C中的指針
3、什么是ARC,引用計(jì)數(shù)存儲(chǔ)在哪里,哪些對(duì)象是通過引用計(jì)數(shù)來管理內(nèi)存的
4瑞佩、ARC下哪些對(duì)象是autorelease對(duì)象

  • 本文將討論下面幾個(gè)問題

1、autorelease對(duì)象的釋放時(shí)機(jī)
2、為什么需要手動(dòng)添加autoreleasePool
3潭千、autoreleasePool的源碼實(shí)現(xiàn)
4、autoreleasePool為什么用雙向鏈表來實(shí)現(xiàn)呢

從線程池說起

操作系統(tǒng)實(shí)現(xiàn)了一個(gè)線程工廠稱為線程池借尿,還有一個(gè)配合工作的管理器稱為調(diào)度中心刨晴,iOS系統(tǒng)并沒有接口去直接操作線程池,線程的創(chuàng)建路翻、調(diào)度狈癞、銷毀全權(quán)交給線程池,當(dāng)我們?cè)噲D去創(chuàng)建一個(gè)線程的時(shí)候茂契,調(diào)度中心會(huì)檢查線程池的運(yùn)行狀態(tài)蝶桶、運(yùn)行線程數(shù)、運(yùn)行策略掉冶,決定接下來的執(zhí)行流程真竖,是直接申請(qǐng)線程執(zhí)行還是放入緩沖隊(duì)列中,待到執(zhí)行的時(shí)候厌小,線程池決定是開辟新的線程還是復(fù)用現(xiàn)有空閑線程恢共,長時(shí)間空閑的線程會(huì)被回收(一般10s左右)

iOS線程對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)

iOS操作系統(tǒng)為線程運(yùn)行配備了一套數(shù)據(jù)結(jié)構(gòu),一個(gè)棧召锈、一個(gè)autoreleasePool旁振、一個(gè)runloop(懶加載)以及一些用于控制狀態(tài)的標(biāo)志位變量

  • 棧:存放函數(shù)的參數(shù)值與局部變量的值等
  • autoreleasePool:存放當(dāng)前線程產(chǎn)生的autorelease對(duì)象
  • runloop:一個(gè)while循環(huán),用于線程可持續(xù)運(yùn)行的一個(gè)循環(huán),蘋果將它封裝成一個(gè)類拐袜,內(nèi)部注冊(cè)了一些觀察者吉嚣,實(shí)現(xiàn)了一套狀態(tài)機(jī),動(dòng)可執(zhí)行蹬铺,靜可休眠

好的~我們回到問題本身

autorelease對(duì)象的釋放時(shí)機(jī)

  • autorelease對(duì)象何時(shí)釋放尝哆,主線程與非主線程的釋放時(shí)機(jī)有何不同?為什么需要手動(dòng)添加autoreleasePool甜攀?
    這個(gè)問題是不是經(jīng)常被問到秋泄,有真正理解過嘛~

先說結(jié)論:

  • 加入到autoreleasePool中的對(duì)象,會(huì)在線程銷毀前规阀,傾倒一次autoreleasePool恒序,取出每個(gè)對(duì)象,發(fā)送一次release消息谁撼,對(duì)象的引用計(jì)數(shù)-1歧胁,此時(shí)對(duì)象是否釋放取決與引用計(jì)數(shù)是否為0

  • 主線程的運(yùn)行循環(huán)在app裝載、鏈接以后就會(huì)在main函數(shù)中啟動(dòng)厉碟,這是常規(guī)設(shè)計(jì)喊巍,所以主線程不會(huì)被銷毀直到app進(jìn)程退出之前,這就導(dǎo)致上面的策略無法釋放autorelease對(duì)象箍鼓,于是蘋果在runloop中加入這部分對(duì)象的釋放時(shí)機(jī)崭参,每次循環(huán)同步執(zhí)行完,在進(jìn)入休眠之前款咖,會(huì)傾倒一次autoreleasePool

  • 通常我們執(zhí)行完現(xiàn)有任務(wù)就無需再次使用這個(gè)線程何暮,所以非主線程的runloop無需啟動(dòng)(特殊場(chǎng)景的需求除外),這就導(dǎo)致一個(gè)問題铐殃,非主線程的autorelease對(duì)象直到線程退出之前才會(huì)得到引用計(jì)數(shù)-1郭卫,當(dāng)這個(gè)線程有長任務(wù)的時(shí)候,內(nèi)存會(huì)一直漲背稼,這與峰值無關(guān)贰军,會(huì)一直漲到OOM

  • 基于以上問題,蘋果公開了autoreleasePool的api蟹肘,在非主線程的場(chǎng)景下词疼,手動(dòng)添加@autoreleasepool{},在autoreleasepool{}作用域結(jié)束前會(huì)傾倒一次

  • 所謂嵌套autoreleasepool帘腹,不過是一個(gè)線程對(duì)應(yīng)了多個(gè)autoreleasepool贰盗,默認(rèn)有一個(gè),其他的是手動(dòng)添加的阳欲,根本不存在嵌套一說舵盈,都只負(fù)責(zé)各自作用域產(chǎn)生的autorelease對(duì)象

  • 主線程同樣有需要手動(dòng)添加autoreleasepool的場(chǎng)景陋率,可以降低內(nèi)存峰值,iOS屏幕的刷新幀率是60秽晚,即一秒刷新60次瓦糟,一次約16.7毫秒,如果在這個(gè)時(shí)間內(nèi)赴蝇,一次運(yùn)行循環(huán)還沒有執(zhí)行完的話就會(huì)出現(xiàn)卡頓現(xiàn)象菩浙,因?yàn)橄到y(tǒng)的vsync信號(hào)過來的時(shí)候,app沒有準(zhǔn)備好下一幀圖像句伶,所以看上去就是卡頓了劲蜻,根本原因是因?yàn)橹骶€程阻塞導(dǎo)致16.7毫秒內(nèi)沒有把圖像提交到緩沖區(qū)(說遠(yuǎn)了),基于以上OOM通常不會(huì)出現(xiàn)在主線程考余,因?yàn)橐呀?jīng)卡爆了先嬉,調(diào)試過程中就會(huì)發(fā)現(xiàn),所以這里用于降低內(nèi)存峰值

再看個(gè)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(@"當(dāng)前線程:%@, 值:%@", [NSThread currentThread], heapAreastring);
        }
    });
}

如上創(chuàng)建了一個(gè)串行隊(duì)列坝初,寫了個(gè)死循環(huán),一直在創(chuàng)建堆區(qū)字符串钾军,但是內(nèi)存不會(huì)漲一絲一毫,原因上一篇文章搞懂Objective-C中的ARC講過绢要,這個(gè)對(duì)象直接會(huì)被release吏恭,不會(huì)加入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(@"當(dāng)前線程:%@, 值:%@", [NSThread currentThread], heapAreastring);
        }
    });
}

會(huì)發(fā)現(xiàn)非主線程創(chuàng)建autorelease對(duì)象會(huì)導(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(@"當(dāng)前線程:%@, 值:%@", [NSThread currentThread], stringAutorelease);
            }
        }
    });
}

結(jié)果是內(nèi)存也不會(huì)漲一絲一毫重罪,數(shù)據(jù)剛好能對(duì)應(yīng)上面的結(jié)論樱哼,對(duì)于有知其所以然需求的可以往下看

autoreleasePool的源碼實(shí)現(xiàn)

前輩這篇講得很好了,本文會(huì)基于最新源碼做個(gè)淺析(可能不是最新源碼剿配,作者開始看源碼時(shí)候的版本objc4-781.2)搅幅,順便講述下如何查閱源碼,源碼鏈接

在源碼中搜索
AutoreleasePoolPage.png

會(huì)發(fā)現(xiàn)AutoreleasePoolPage是一個(gè)c++的類呼胚,繼承AutoreleasePoolPageData茄唐,具體實(shí)現(xiàn)很長,這里省去部分函數(shù)實(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();
            }
        }
    }
}


還是很長是不是蝇更,但是對(duì)于理解原理都重要沪编,看看蘋果自己的注釋

/***********************************************************************
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 的實(shí)現(xiàn)
線程的Autorelease pool是一系列聲明在棧上的指針。
每個(gè)指針要么是一個(gè)需要釋放的對(duì)象年扩,要么是 POOL_BOUNDARY蚁廓,它是
Autorelease pool邊界。
token是指向該池的 POOL_BOUNDARY 的指針厨幻。 什么時(shí)候
池被彈出相嵌,每個(gè)比哨兵更熱的對(duì)象都被釋放腿时。
棧被分成一個(gè)雙向鏈接的AutoreleasePoolPage列表。 Pages根據(jù)需要饭宾,添加
和刪除批糟。
線程本地存儲(chǔ)指向最新被存儲(chǔ)到自動(dòng)釋池所在的熱點(diǎn)頁面

**********************************************************************/

看完這個(gè)注釋捏雌,我覺得都不用往下講了??跃赚,一個(gè)字,清晰透徹

autorelease對(duì)象加入到自動(dòng)釋放池的流程

    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

next指針作為游標(biāo)指向棧頂最新add進(jìn)來的autorelease對(duì)象的下一個(gè)位置性湿,當(dāng)有新的obj加入autoreleasePool的時(shí)候取到next指針的地址存儲(chǔ)obj纬傲,next指針向棧頂方向移動(dòng)8個(gè)字節(jié)

runloop沒有啟動(dòng)的線程 autorelease對(duì)象的釋放流程

每個(gè)autoreleasePool初始化的時(shí)候都會(huì)綁定當(dāng)前線程,如下是AutoreleasePoolPage的初始化函數(shù)

    static void init()
    {
        int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                             AutoreleasePoolPage::tls_dealloc);
        ASSERT(r == 0);
    }

第一個(gè)參數(shù)是AutoreleasePoolPage的標(biāo)識(shí)肤频,第二個(gè)參數(shù)是一個(gè)函數(shù)指針叹括,線程退出之前會(huì)執(zhí)行這個(gè)函數(shù),就是tls_dealloc宵荒,調(diào)用棧如下:

1汁雷、tls_dealloc中會(huì)取AutoreleasePoolPage *page = coldPage()
2、page調(diào)用objc_autoreleasePoolPop(page->begin())函數(shù)
3报咳、調(diào)用棧如下objc_autoreleasePoolPop->pop->popPage->releaseUntil
4侠讯、releaseUntil中取hotPage(),然后通過一個(gè)循環(huán)暑刃,取next游標(biāo)指針id obj = *--page->next厢漩,調(diào)用objc_release(obj),然后游標(biāo)指針向棧底移動(dòng)8個(gè)字節(jié)
5岩臣、所有autorelease對(duì)象清理干凈溜嗜,最后調(diào)用kill()函數(shù)遍歷釋放所有autoreleasePage

runloop啟動(dòng)的線程 autorelease對(duì)象的釋放流程

線程休眠之前,runloop會(huì)調(diào)用objc_autoreleasePoolPop架谎,上面流程除了第一步炸宵,后面一樣

autoreleasePool為什么用雙向鏈表來實(shí)現(xiàn)呢

單向的可以嗎?可以呀谷扣,但是查詢速度會(huì)以指數(shù)級(jí)別降低土全,刪除棧頂page后想刪除的倒數(shù)第二個(gè)page,怎么辦会涎,從頭開始遍歷唄涯曲,遍歷n-1次,然后遍歷n-2次.......
所以為什么設(shè)計(jì)成雙向鏈表在塔,效率高幻件,如此而已

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蛔溃,隨后出現(xiàn)的幾起案子绰沥,更是在濱河造成了極大的恐慌学少,老刑警劉巖法绵,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件函喉,死亡現(xiàn)場(chǎng)離奇詭異反璃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)秃臣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門涧衙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人奥此,你說我怎么就攤上這事弧哎。” “怎么了稚虎?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵撤嫩,是天一觀的道長。 經(jīng)常有香客問我蠢终,道長序攘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任寻拂,我火速辦了婚禮程奠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祭钉。我一直安慰自己瞄沙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布朴皆。 她就那樣靜靜地躺著,像睡著了一般泛粹。 火紅的嫁衣襯著肌膚如雪遂铡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天晶姊,我揣著相機(jī)與錄音扒接,去河邊找鬼。 笑死们衙,一個(gè)胖子當(dāng)著我的面吹牛钾怔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蒙挑,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼宗侦,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了忆蚀?” 一聲冷哼從身側(cè)響起矾利,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤姑裂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后男旗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舶斧,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年察皇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茴厉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡什荣,死狀恐怖矾缓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情溃睹,我是刑警寧澤而账,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站因篇,受9級(jí)特大地震影響泞辐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜竞滓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一咐吼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧商佑,春花似錦锯茄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抓半,卻和暖如春喂急,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笛求。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工廊移, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人探入。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓狡孔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜂嗽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苗膝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容