Autorelease Pool的底層實(shí)現(xiàn)和相關(guān)事項(xiàng)總結(jié)

重新仔細(xì)讀了下源碼记盒,通過留些筆記的方式加深理解憎蛤。由于我的C++水平有限,可能有些地方理解的不到位,甚至有錯(cuò)誤俩檬,希望有朋友幫我指正萎胰。

一、@autoreleasepool {}是什么棚辽?
創(chuàng)建個(gè)新工程技竟,簡(jiǎn)單起見,把main.m改成這樣:

int main(int argc, char * argv[]) {
    @autoreleasepool { }
    return 0;
}

打開終端屈藐,cd到main.m的目錄下榔组,用命令“clang -rewrite-objc main.m”,看看C++的實(shí)現(xiàn)联逻。報(bào)錯(cuò)的話搓扯,根據(jù)錯(cuò)誤信息修改。打開同目錄下新生成的main.cpp遣妥,搜索找到main函數(shù)的實(shí)現(xiàn)擅编,如下:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}

初步可見,@autoreleasepool{}是通過__AtAutoreleasePool類型的局部變量__autoreleasepool來實(shí)現(xiàn)的箫踩。再搜索下爱态,會(huì)發(fā)現(xiàn)__AtAutoreleasePool是如下的結(jié)構(gòu)體:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

因?yàn)榫植孔兞柯暶鲿r(shí)調(diào)用構(gòu)造函數(shù),離開作用域時(shí)調(diào)用析構(gòu)函數(shù)境钟,那么:

// 假如我們寫:
@autoreleasepool{
    NSString *str = [NSString stringWithFormat:@"Hello, World! "];
}

// 實(shí)際就相當(dāng)于:
/* @autoreleasepool */ { 
    atautoreleasepoolobj = objc_autoreleasePoolPush();

    // 暫先不用理會(huì)這行代碼c++的實(shí)現(xiàn)是什么樣子
    NSString *str = [NSString stringWithFormat:@"Hello, World! "];

    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

接下來的任務(wù)是看objc_autoreleasePoolPush()和objc_autoreleasePoolPop()做了什么锦担。顯然要看runtime的源碼。

二慨削、Autorelease Pool的數(shù)據(jù)結(jié)構(gòu)
在繼續(xù)看源碼之前洞渔,我想先根據(jù)我個(gè)人的理解說明下autorelease pool的數(shù)據(jù)結(jié)構(gòu)(如果理解有誤,希望有同學(xué)指正)缚态,帶著這個(gè)總體概念有助于繼續(xù)閱讀源碼磁椒。

首先,每個(gè)線程都可以有自己的autorelease pool玫芦,系統(tǒng)會(huì)默認(rèn)為主線程創(chuàng)建一個(gè)浆熔,而子線程默認(rèn)是沒有的。這點(diǎn)后面會(huì)再進(jìn)一步說明桥帆。

然后医增,線程可以有0到多個(gè)pool,1到多個(gè)pool形成一個(gè)棧老虫,棧中存儲(chǔ)需要autorelease的對(duì)象指針或者名叫POOL_BOUNDARY的占位符(用來區(qū)隔不同的pool)叶骨。這個(gè)棧又由不同的page組成,每個(gè)page有固定的size祈匙,還有parent和child兩根指針忽刽。多個(gè)page之間通過指針相連,這樣就形成了一種類似鏈棧的結(jié)構(gòu)。

三缔恳、runtime源碼中的實(shí)現(xiàn)
runtime源碼(這里用objc4-781)里很容找到一中提到的兩個(gè)函數(shù)的實(shí)現(xiàn):

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

繼續(xù)向下找AutoreleasePoolPage宝剖。

  1. AutoreleasePoolPage
    首先我們會(huì)找到下面的代碼和注釋:
/***********************************************************************
   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. 
**********************************************************************/
class AutoreleasePoolPage : private AutoreleasePoolPageData

可以結(jié)合二中提到的內(nèi)容看看這些注釋:
// 一個(gè)線程的autorelease pool是一個(gè)指針棧洁闰。
// 棧中存放的指針指向需要release的對(duì)象或者POOL_BOUNDARY(作用如二中所提)歉甚。
// 每個(gè)pool會(huì)有一個(gè)token指針,指向pool的POOL_BOUNDARY(哨兵)扑眉。當(dāng)這個(gè)pool執(zhí)行出棧操作時(shí)纸泄,每一個(gè)比這個(gè)哨兵后入棧的對(duì)象都會(huì)被released。
// 這個(gè)棧是由一個(gè)以pages為節(jié)點(diǎn)的雙向鏈表組成腰素,pages會(huì)根據(jù)需求進(jìn)行增減聘裁。
// 線程局部變量(Thread-local storage / tls)存儲(chǔ)了指向最新page(the hot page)的指針,最近被autoreleased的對(duì)象會(huì)被存儲(chǔ)在這個(gè)page里弓千。

  1. AutoreleasePoolPageData
    這里我們可以看到上面所謂雙向鏈表的節(jié)點(diǎn)的結(jié)構(gòu):
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic;  // 用于結(jié)構(gòu)的完整性校驗(yàn)
    __unsafe_unretained id *next;  // 當(dāng)前page的棧頂指針
    pthread_t const thread;  // 所屬線程
    AutoreleasePoolPage * const parent;  // 父節(jié)點(diǎn)
    AutoreleasePoolPage *child;  // 子節(jié)點(diǎn)
    uint32_t const depth;  // 鏈表結(jié)點(diǎn)數(shù)
    uint32_t hiwat;  // 當(dāng)前鏈表儲(chǔ)存指針的個(gè)數(shù)衡便?

    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)
    {
    }
};

節(jié)點(diǎn)結(jié)構(gòu)清楚了,來看看push操作洋访。

  1. AutoreleasePoolPage::push()
    直接看源碼加注釋:
static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // debug模式下镣陕,每一個(gè)autorelease pool都從一個(gè)新的page開始。
        // 注意這里操作的對(duì)象是:POOL_BOUNDARY
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    // 返回POOL_BOUNDARY存入的位置
    return dest;
}

static inline id *autoreleaseFast(id obj)
{
    // hotPage可以理解為當(dāng)前工作的page姻政,也就是最近被autoreleased的對(duì)象指針會(huì)被加入的page呆抑,下同
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 如果hotPage沒滿,存入obj
        return page->add(obj);
    } else if (page) {
        // 如果滿了汁展,方法內(nèi)部向下尋找沒滿的子節(jié)點(diǎn)鹊碍,并存入obj;如果沒有食绿,就新建一個(gè)節(jié)點(diǎn)
        return autoreleaseFullPage(obj, page);
    } else {
        // 新建一個(gè)節(jié)點(diǎn)并存入obj
        return autoreleaseNoPage(obj);
    }
}

// 邏輯大致如上侈咕,只是沒有判斷hotPage是否滿了
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page) return autoreleaseFullPage(obj, page);
    else return autoreleaseNoPage(obj);
}

push操作內(nèi)部會(huì)有一些邏輯分支,條件如上面代碼注釋器紧,最終的目的都是找到或創(chuàng)建一個(gè)page將指針存進(jìn)去耀销。既然要存新指針,autoreleaseFullPage(obj, page)和autoreleaseNoPage(obj)內(nèi)部最終也都調(diào)用了page->add(obj)方法品洛。add方法很簡(jiǎn)單树姨,指針存入hotPage頂部(next),頂部加1:

id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    // next指向的空間存入obj桥状,然后加1
    *next++ = obj;
    protect();
    // 返回存入的位置
    return ret;
}

顯然存儲(chǔ)的目的就是為了在[pool drain]漓骚,也就是pop操作的時(shí)候向被存入的對(duì)象發(fā)送release消息。

  1. AutoreleasePoolPage::pop(ctxt)

pop的代碼有點(diǎn)多耐亏,這里就不貼出來了龟糕。
概括來講就是要找到ctxt對(duì)應(yīng)的page,并將比ctxt晚入棧的對(duì)象全部出棧,并調(diào)用方法:objc_release(obj)查邢,也就是[obj release]蔗崎。可以對(duì)照三.1中的這段注釋來理解:

// 每個(gè)pool會(huì)有一個(gè)token指針扰藕,指向pool的POOL_BOUNDARY(哨兵)缓苛。當(dāng)這個(gè)pool執(zhí)行出棧操作時(shí),每一個(gè)比這個(gè)哨兵后入棧的對(duì)象都會(huì)被released邓深。

  1. AutoreleasePoolPage:: autorelease(obj)

上面1中提到未桥,autorelease pool棧中存放的指針有兩種:指向需要release的對(duì)象或者POOL_BOUNDARY。
上面的push和pop操作針對(duì)的就是POOL_BOUNDARY芥备,而這里要講的方法的操作對(duì)象就是pool drain時(shí)需要release的對(duì)象冬耿。
其方法實(shí)現(xiàn)很簡(jiǎn)單,內(nèi)部調(diào)用的autoreleaseFast方法上面也講過了萌壳。
我們知道亦镶,當(dāng)通過遍歷構(gòu)造器,比如“NSString *str = [NSString stringWithFormat:@"%@", xxx]”創(chuàng)建對(duì)象的時(shí)候袱瓮,方法內(nèi)部執(zhí)行了“[str autorelease]”缤骨。而這時(shí)底層就通過調(diào)用“AutoreleasePoolPage:: autorelease(str)”,將str加入自動(dòng)釋放池懂讯。

static inline id autorelease(id obj)
{
    ASSERT(obj);
    ASSERT(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

源碼的部分先到這荷憋,主要的結(jié)構(gòu)和方法都提到了,還有很多細(xì)節(jié)和值得深挖的地方褐望,留給你自己勒庄。

四、Autorelease Pool的使用場(chǎng)景

autorelease pool是為引用計(jì)數(shù)機(jī)制服務(wù)的瘫里,我們創(chuàng)建的所有需要autorelease的對(duì)象实蔽,都要在一個(gè)pool中進(jìn)行。

  1. 主線程在每一個(gè)runloop的circle開始時(shí)都會(huì)自動(dòng)創(chuàng)建一個(gè)pool谨读,然后在結(jié)束時(shí)自動(dòng)調(diào)用[pool drain]局装。所以我們?cè)谥骶€程中如果沒有特殊需求不需要手動(dòng)創(chuàng)建autorelease pool。有特殊需求的情況見3劳殖。
  2. 在子線程中铐尚,如果我們調(diào)用了諸如“NSString *str = [NSString stringWithFormat:@"%@", xxx]這類的方法,就需要自動(dòng)釋放池哆姻,這時(shí)我們必須手動(dòng)創(chuàng)建autorelease pool宣增,否則會(huì)導(dǎo)致leak。但是如果我們只用[[MyClass alloc] init]方法來創(chuàng)建對(duì)象矛缨,因?yàn)樗鼈兂隽俗饔糜蚓蜁?huì)被自動(dòng)銷毀爹脾,則不需要我們手動(dòng)創(chuàng)建池子了帖旨。
  3. 另一個(gè)被廣泛討論的場(chǎng)景就是在主線程(子線程中如果我們開啟了runloop,當(dāng)然也一樣)的一個(gè)循環(huán)中短時(shí)間創(chuàng)建大量對(duì)象灵妨,如果我們不想等到當(dāng)前runloop的circle結(jié)束時(shí)才釋放這些對(duì)象(這時(shí)內(nèi)存可能已經(jīng)暴漲很多了)解阅,就可以在循環(huán)體中手動(dòng)創(chuàng)建一個(gè)pool,使得每次循環(huán)結(jié)束時(shí)都將本次循環(huán)中產(chǎn)生的臨時(shí)變量及時(shí)釋放掉泌霍。

五货抄、Swift中的Autorelease Pool

  1. Swift工程的程序入口標(biāo)記為@UIApplicationMain,沒有了main.m烹吵,也就看不到@autoreleasepool {}了碉熄。
  2. Swift中還需不需要使用autorelease pool?
    Swift 1.0時(shí)還有各種對(duì)應(yīng)OC中遍歷構(gòu)造器的創(chuàng)建對(duì)象的方法肋拔,比如“String.stringWithFormat”。而從Swift 1.1開始這類方法都被廢棄了呀酸,我們基本都只能使用init方法來創(chuàng)建對(duì)象凉蜂,這種情況下就不需要autorelease pool了。(這點(diǎn)是從喵神15年的博客看來的...那時(shí)候我還沒開始學(xué)習(xí)Swift性誉。)
    不過窿吩,我發(fā)現(xiàn)現(xiàn)在還有一些類似的api存在,比如:
public class func localizedStringWithFormat(_ format: NSString, _ args: CVarArg...) -> Self

如果使用這類方法的話错览,應(yīng)該還需要autorelease pool吧纫雁。?

  1. Swift中autorelease pool的使用形式不再是@autoreleasepool {}倾哺,去掉了“@”符號(hào)轧邪,直接是autoreleasepool {},其實(shí)是一個(gè)函數(shù)羞海,還有返回值忌愚。
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市却邓,隨后出現(xiàn)的幾起案子硕糊,更是在濱河造成了極大的恐慌,老刑警劉巖腊徙,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件简十,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡撬腾,警方通過查閱死者的電腦和手機(jī)螟蝙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來时鸵,“玉大人胶逢,你說我怎么就攤上這事厅瞎。” “怎么了初坠?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵和簸,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我碟刺,道長(zhǎng)锁保,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任半沽,我火速辦了婚禮爽柒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘者填。我一直安慰自己浩村,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布占哟。 她就那樣靜靜地躺著心墅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪榨乎。 梳的紋絲不亂的頭發(fā)上怎燥,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音蜜暑,去河邊找鬼铐姚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肛捍,可吹牛的內(nèi)容都是我干的隐绵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼篇梭,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼氢橙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恬偷,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤悍手,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后袍患,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坦康,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年诡延,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滞欠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肆良,死狀恐怖筛璧,靈堂內(nèi)的尸體忽然破棺而出逸绎,到底是詐尸還是另有隱情,我是刑警寧澤夭谤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布棺牧,位于F島的核電站,受9級(jí)特大地震影響朗儒,放射性物質(zhì)發(fā)生泄漏颊乘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一醉锄、第九天 我趴在偏房一處隱蔽的房頂上張望乏悄。 院中可真熱鬧,春花似錦恳不、人聲如沸檩小。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽识啦。三九已至,卻和暖如春神妹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背家妆。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工鸵荠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伤极。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓蛹找,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親哨坪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子庸疾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354