AutoreleasePool 的實(shí)現(xiàn)機(jī)制 (四)

本文章基于 objc4-725 進(jìn)行測(cè)試.
objc4 的代碼可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.
本篇文章主要分析 AutoreleasePool 銷毀相關(guān)操作的函數(shù).

AutoreleasePoolPage 類的成員函數(shù)

AutoreleasePoolPage 類的靜態(tài)函數(shù)和成員函數(shù)眾多, 有些函數(shù)沒有貼出源碼, 只寫了內(nèi)部邏輯, 所以需要結(jié)合源碼來(lái)看.

  • pop
static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 為空池標(biāo)志
        if (hotPage()) { //如果有 hotPage, 即池非空
            pop(coldPage()->begin()); //將整個(gè)自動(dòng)釋放池銷毀
        } else {
            setHotPage(nil); //沒有 hotPage, 即為空池, 設(shè)置 hotPage 為 nil
        }
        return;
    }
    page = pageForPointer(token); //根據(jù) token 找到所在的 節(jié)點(diǎn)
    stop = (id *)token; //token 轉(zhuǎn)換給 stop
    if (*stop != POOL_BOUNDARY) { //如果 stop 中存儲(chǔ)的不是哨兵節(jié)點(diǎn)
        if (stop == page->begin()  &&  !page->parent) {
            //存在自動(dòng)釋放池的第一個(gè)節(jié)點(diǎn)存儲(chǔ)的第一個(gè)對(duì)象不是哨兵對(duì)象的情況, 有兩種情況導(dǎo)致:
            //1. 頂層池唄是否, 但留下了第一個(gè)節(jié)點(diǎn)(有待深挖)
            //2. 沒有自動(dòng)釋放池的 autorelease 對(duì)象(有待深挖)
        } else {
            //非自動(dòng)釋放池的第一個(gè)節(jié)點(diǎn), stop 存儲(chǔ)的也不是哨兵對(duì)象的情況
            return badPop(token); //調(diào)用錯(cuò)誤情況下的 badPop()
        }
    }
    if (PrintPoolHiwat) printHiwat(); //如果需要打印 hiwat, 則打印
    page->releaseUntil(stop); //將自動(dòng)釋放池中 stop 地址之后的所有對(duì)象釋放掉
    if (...) {
        //這一段代碼都是調(diào)試用代碼
    } else if (page->child) { //如果 page 有 child 節(jié)點(diǎn)
        if (page->lessThanHalfFull()) { //如果 page 已占用空間少于一半
            page->child->kill(); //kill 掉 page 的 child 節(jié)點(diǎn)
        } else if (page->child->child) { //如果 page 的占用空間已經(jīng)大于一半, 并且 page 的 child 節(jié)點(diǎn)有 child 節(jié)點(diǎn)
            page->child->child->kill(); //kill 掉 child 節(jié)點(diǎn)的 child 節(jié)點(diǎn)
        }
    }
}

pop() 函數(shù)的主要作用是根據(jù)自動(dòng)釋放池狀態(tài)以及傳入的 token 參數(shù)來(lái)決定合適的釋放方案, 如果傳入的 token 是空池標(biāo)識(shí), 則需要確保銷毀整個(gè)自動(dòng)釋放池; 如果 token 和自動(dòng)釋放池狀態(tài)沖突, 則調(diào)用 badPop(); 如果釋放操作是正常的, 則使用 releaseUntil() 方法來(lái)釋放 stop 之后的 autorelease 對(duì)象, 釋放完成后如果 hotPage 使用量過(guò)半, 則預(yù)留下一級(jí)節(jié)點(diǎn), 從下下一級(jí)的節(jié)點(diǎn)開始 kill, 這樣可以節(jié)省創(chuàng)建新節(jié)點(diǎn)的時(shí)間, 如果 hotPage 的使用量未過(guò)半, 則從下一級(jí)節(jié)點(diǎn)開始 kill, 并不預(yù)留節(jié)點(diǎn), 這樣可以節(jié)省空間.

  • coldPage

通過(guò)

while (result->parent) {
    result = result->parent;
    result->fastcheck();
}

這種形式, 一直找到自動(dòng)釋放池的第一個(gè)節(jié)點(diǎn).

  • pageForPointer
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE; //轉(zhuǎn)換為十進(jìn)制數(shù)的 p 余上 4096
    assert(offset >= sizeof(AutoreleasePoolPage)); //如果余數(shù)小于 AutoreleasePoolPage 的大小則拋出異常
    result = (AutoreleasePoolPage *)(p - offset); //十進(jìn)制數(shù) p 減掉剛剛得到的余數(shù) offset, 結(jié)果轉(zhuǎn)換為AutoreleasePoolPage * 類型指針
    result->fastcheck(); //根據(jù)配置進(jìn)行 check
    return result;
}

由于為 AutoreleasePoolPage 對(duì)象分配的地址都是按 4096 對(duì)齊的, 也就是說(shuō) AutoreleasePoolPage 對(duì)象所處的地址都是 4096 的倍數(shù), 所以 token 轉(zhuǎn)換為十進(jìn)制數(shù)時(shí), 對(duì) 4096 取余, 就能得到 token 地址對(duì) AutoreleasePoolPage 對(duì)象地址的偏移量. 又因?yàn)?AutoreleasePoolPage 對(duì)象本身的大小是 56, 所以如果 token 對(duì) 4096 取余的結(jié)果如果小于 56 就是錯(cuò)誤的, 此時(shí)會(huì)拋出異常. 否則 token 地址減去偏移量, 就是 AutoreleasePoolPage 對(duì)象的地址, 轉(zhuǎn)換為 AutoreleasePoolPage * 類型的指針, 就是該 token 所處的 page 節(jié)點(diǎn).

  • badPop
static void badPop(void *token)
{
    // 對(duì)于舊的 SDK 來(lái)說(shuō), 這個(gè)錯(cuò)誤并不是致命的
    if (DebugPoolAllocation || sdkIsAtLeast(10_12, 10_0, 10_0, 3_0, 2_0)) {
        //對(duì)于開啟 pool 內(nèi)存分配的 debug 模式, 以及最新 SDK 的情況, 調(diào)用到 badPop 是錯(cuò)誤的 
        _objc_fatal(...); //輸出一系列錯(cuò)誤信息
    }
    // 舊 SDK 下, Bad pop 會(huì)記錄一次日志
    static bool complained = false; //這個(gè)靜態(tài)變量確保下面的 crush log 只寫入一次
    if (!complained) {
         complained = true;
        _objc_inform_now_and_on_crash(...); //輸出一系列信息到 crash log 里, 但不會(huì)觸發(fā) crash
    }
    objc_autoreleasePoolInvalid(token); //摧毀包含 token 的自動(dòng)釋放池
}

首先這個(gè)函數(shù)正常情況下是調(diào)用不到的, 只有使用舊 SDK 的時(shí)候有可能會(huì)發(fā)生. 一旦發(fā)生 badPop 時(shí), 會(huì)記錄下錯(cuò)誤日志, 并銷毀該自動(dòng)釋放池.

  • releaseUntil 和 releaseAll
void releaseAll() 
{
    releaseUntil(begin()); //直接調(diào)用 releaseUntil, 傳入 begin()
}

void releaseUntil(id *stop)
{
    //這里沒有使用遞歸, 防止發(fā)生棧溢出
    while (this->next != stop) { //一直循環(huán)到 next 指針指向 stop 為止
        AutoreleasePoolPage *page = hotPage(); //取出 hotPage
        //接手的開發(fā)者認(rèn)為這里也可以用 if 來(lái)代替 while, 但是找不到證據(jù)證明自己, 所以他留下了這么一句注釋: 
        //fixme I think this `while` can be `if`, but I can't prove it
        while (page->empty()) { //從節(jié)點(diǎn) page 開始, 向前找到第一個(gè)非空節(jié)點(diǎn)
            page = page->parent; //page 非空的話, 就向 page 的 parent 節(jié)點(diǎn)查找
            setHotPage(page); //把新的 page 節(jié)點(diǎn)設(shè)置為 HotPage
        }
        page->unprotect(); //如果需要的話, 解除 page 的內(nèi)存鎖定
        id obj = *--page->next; //先將 next 指針向前移位, 然后再取出移位后地址中的值
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); //將 next 指向的內(nèi)存清空為SCRIBBLE
        page->protect(); //如果需要的話, 設(shè)置內(nèi)存鎖定
        if (obj != POOL_BOUNDARY) { //如果取出的對(duì)象不是哨兵對(duì)象
            objc_release(obj); //給取出來(lái)的對(duì)象進(jìn)行一次 release 操作
        }
    }
    setHotPage(this); //將本節(jié)點(diǎn)設(shè)置為 hotPage
#if DEBUG
    // 調(diào)試模式下, 檢查剛剛被釋放的 page 節(jié)點(diǎn)是否都為空
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

自動(dòng)釋放池銷毀對(duì)象中最重要的一環(huán), 調(diào)用者是用 pageForPointer() 找到的, token 所在的 page 節(jié)點(diǎn), 參數(shù)為 token. 這個(gè)函數(shù)主要操作流程就是, 從 hotPage 開始, 使用 next 指針遍歷存儲(chǔ)在節(jié)點(diǎn)里的 autorelease 對(duì)象列表, 對(duì)每個(gè)對(duì)象進(jìn)行一次 release 操作, 并且把 next 指向的指針清空, 如果 hotPage 里面的對(duì)象全部清空, 則繼續(xù)循環(huán)向前取 parent 并繼續(xù)用 next 指針遍歷 parent, 一直到 next 指針指向的地址為 token 為止. 因?yàn)?token 就在 this 里面, 所以這個(gè)時(shí)候的 hotPage 應(yīng)該是 this.

  • lessThanHalfFull
bool lessThanHalfFull() {
     return (next - begin() < (end() - begin()) / 2);
}

next - begin() 是已經(jīng)使用的字節(jié)數(shù), end() - begin() 是一共可以用來(lái)存儲(chǔ) autorelease 對(duì)象的字節(jié)數(shù), 這里判斷使用量是否過(guò)半.

  • kill
void kill()
{
    //這里沒有使用遞歸, 防止發(fā)生棧溢出
    AutoreleasePoolPage *page = this; //從調(diào)用者開始
    while (page->child) page = page->child; //先找到最后一個(gè)節(jié)點(diǎn)
    AutoreleasePoolPage *deathptr;
    do { //從最后一個(gè)節(jié)點(diǎn)開始遍歷到調(diào)用節(jié)點(diǎn)
        deathptr = page; //保留當(dāng)前遍歷到的節(jié)點(diǎn)
        page = page->parent; //向前遍歷
        if (page) { //如果有值
            page->unprotect(); //如果需要的話, 解除內(nèi)存鎖定
            page->child = nil; //child 置空
            page->protect(); //如果需要的話, 設(shè)置內(nèi)存鎖定
        }
        delete deathptr; //回收剛剛保留的節(jié)點(diǎn), 重載 delete, 內(nèi)部調(diào)用 free
    } while (deathptr != this);
}

自動(dòng)釋放池中需要 release 的對(duì)象都已操作完成, 此時(shí) hotPage 之后的 page 節(jié)點(diǎn)都已經(jīng)清空了, 需要把這些節(jié)點(diǎn)的內(nèi)存都回收, 操作方案就是從最后一個(gè)節(jié)點(diǎn), 遍歷到調(diào)用者節(jié)點(diǎn), 挨個(gè)回收.

  • ~AutoreleasePoolPage

AutoreleasePoolPage 的析構(gòu)函數(shù), 內(nèi)部都是檢查類的函數(shù), 判斷銷毀 AutoreleasePoolPage 之前, pop() 操作是否正確執(zhí)行完成, 如果出現(xiàn)意外則會(huì)直接拋出異常.

至此 AutoreleasePool 的銷毀操作已經(jīng)全部完成.
值得注意的就是自動(dòng)釋放池銷毀時(shí), 僅僅是為相應(yīng)的 autorelease 對(duì)象調(diào)用 release 方法, 并不會(huì)直接銷毀該對(duì)象, 該對(duì)象是否銷毀還是要看它本身的引用計(jì)數(shù). 另外 autorelease 對(duì)象加入到自動(dòng)釋放池時(shí)不會(huì)調(diào)用 retain 方法, 但加入到自動(dòng)釋放池時(shí)不會(huì)判重, 所以對(duì)一個(gè)對(duì)象調(diào)用多次 autorelease 方法的話, 會(huì)重復(fù)加入自動(dòng)釋放池, 最后銷毀時(shí)會(huì)多次 release, 引發(fā) crash.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市懦胞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颅拦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讥电,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)抗碰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門看疗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事√嘏兀” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任芋忿,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘薪铜。我一直安慰自己,他們只是感情好帮掉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昧甘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天墓拜,我揣著相機(jī)與錄音,去河邊找鬼。 笑死玄捕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了甩挫?” 一聲冷哼從身側(cè)響起亦渗,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洛勉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后输拇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體带族,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡前普,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年换途,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炮车,死狀恐怖瘫拣,靈堂內(nèi)的尸體忽然破棺而出黔姜,到底是詐尸還是另有隱情纳寂,我是刑警寧澤晦雨,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響已脓,放射性物質(zhì)發(fā)生泄漏堕担。R本人自食惡果不足惜霹购,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一旭咽、第九天 我趴在偏房一處隱蔽的房頂上張望仲墨。 院中可真熱鬧俩由,春花似錦痘系、人聲如沸佛纫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至甥啄,卻和暖如春存炮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜈漓。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工穆桂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人迎变。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓充尉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親衣形。 傳聞我的和親對(duì)象是個(gè)殘疾皇子驼侠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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