本文章基于 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.