本文使用的 runtime 版本為 objc4-706。
對于 autorelease
的研究需要先從 @autoreleasepool { ... }
著手课兄。首先對有 @autoreleasepool { ... }
的代碼使用 clang -rewrite-objc
進行轉換萎胰,在轉換后的文件中靠汁,可以看到 @autoreleasepool { ... }
變成了這樣:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
...
}
當然還可以找到 __AtAutoreleasePool
的定義:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
可以看到王暗,代碼利用了變量聲明和自動變量在代碼塊結束后自動銷毀的特性姊扔,在構造函數(shù)和析構函數(shù)中調用了 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
函數(shù)鳖枕。在 NSObject.mm
文件中可以找到這兩個函數(shù)的實現(xiàn):
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以發(fā)現(xiàn)魄梯,這兩個函數(shù)只是對 AutoreleasePoolPage
這個 C++ 類的兩個類方法 push
和 pop
的簡單封裝。
AutoreleasePoolPage
在 NSObject.mm
中可以找到 AutoreleasePoolPage
類的實現(xiàn)宾符,先可以看一下它的成員變量:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
一個一個過一下這些成員變量:
-
magic
:這個變量的類型是magic_t
酿秸,是用來檢查AutoreleasePoolPage
的內存沒有被修改的,放在第一個也就是這個原因魏烫,防止前面地址有內容溢過來辣苏。 -
next
:類型是id *
肝箱,存放的是下一個被autorelease
的對象指針存放的地址。 -
thread
:對應的線程稀蟋,這說明了自動釋放池是對應線程的煌张。 -
parent
和child
:用來保存前一個AutoreleasePoolPage
和后一個AutoreleasePoolPage
,就是一個雙向鏈表退客,畢竟一個AutoreleasePoolPage
能存放的對象是有限的骏融。 -
depth
:很明顯是這個鏈表有多深。 -
hiwat
:一個在 DEBUG 時才有用的參數(shù)井辜,表示最高有記錄過多少對象(hi-water)绎谦。
可以注意到,這些成員變量并沒有指示出對象記錄在哪里粥脚,繼續(xù)在 AutoreleasePoolPage
的實現(xiàn)里看一看窃肠,能發(fā)現(xiàn)一些有趣的東西:
static size_t const SIZE =
PAGE_MAX_SIZE; // size and alignment, power of 2
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
AutoreleasePoolPage
重載了 new
操作符,這樣一個新的對象需要 SIZE
這么多的內存空間刷允,SIZE
的值 PAGE_MAX_SIZE
是一個根據(jù)機器不同的大小冤留,在寫這篇文章的機器上(i386)上是 4096。AutoreleasePoolPage
的成員變量大小加在一起也只有 56 字節(jié)树灶,但是 new 它一個居然要 4096 字節(jié)纤怒,這剩下的 4040 字節(jié)肯定就是存放被 autorelease
的對象的地方了。AutoreleasePoolPage
的實現(xiàn)中有這些個函數(shù):
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
bool lessThanHalfFull() {
return (next - begin() < (end() - begin()) / 2);
}
可以看到天通,begin
就是成員變量結束的地址(this+sizeof(*this)
)泊窘,end
就是整個申請的內存結束的地方了,其余的函數(shù)很好看懂像寒。對于成員變量 next
來說烘豹,可以看一下構造函數(shù):
AutoreleasePoolPage(AutoreleasePoolPage *newParent)
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
可以看到 next(begin())
,next
的初始值就是 begin
诺祸。結合上面對 next
的描述携悯,就能理解這個初始值的意義了。
autorelease
現(xiàn)在來研究一下 autorelease
是怎么實現(xiàn)的筷笨,autorelease
的入口是 objc_autorelease
函數(shù):
__attribute__((aligned(16)))
id
objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
就是很簡單的進行了判空和判斷 tagged pointer 后憔鬼,就將實現(xiàn)交給了 objc_object
結構體的 autorelease
函數(shù):
// Equivalent to [this autorelease], with shortcuts if there is no override
inline id
objc_object::autorelease()
{
if (isTaggedPointer()) return (id)this;
if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
走的也是 reatin
和 release
的老套路,如果沒有自定義的實現(xiàn)胃夏,就走默認實現(xiàn) rootAutorelease
轴或,否則直接給自定義實現(xiàn)發(fā)消息。繼續(xù)查看默認實現(xiàn):
// Base autorelease implementation, ignoring overrides.
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
其中 prepareOptimizedReturn
函數(shù)是 ARC 對 autorelease
的優(yōu)化构订,本篇文章不做研究侮叮,繼續(xù)查看 rootAutorelease2
:
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
果不其然,是調用了 AutoreleasePoolPage
里的實現(xiàn)(這不廢話嗎前面還講了那么多關于 AutoreleasePoolPage
手動捂臉)悼瘾。
繼續(xù)追查 autorelease
函數(shù):
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;
}
可以看到囊榜,實際工作交給了 autoreleaseFast
函數(shù),文章之后再對這個函數(shù)繼續(xù)分析亥宿。
push
結合文章最開始的分析卸勺,push
函數(shù)就是往 AutoreleasePoolPage
這一整個內存空間里壓入一個自動釋放池,看一下 push
的實現(xiàn):
# define POOL_BOUNDARY nil
static inline void *push()
{
id *dest = autoreleaseFast(POOL_BOUNDARY);
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
push
的實現(xiàn)里很有意思的往 autoreleaseFast
函數(shù)里傳入了一個叫 POOL_BOUNDARY
(池邊界)的東西烫扼,可以看到它其實就是 nil
曙求。新建一個自動釋放池為什么要和 autorelease
調用一樣的函數(shù)呢?接下來分析一下 autoreleaseFast
函數(shù)映企。
autoreleaseFast
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
在 autoreleaseFast
中悟狱,首先需要拿到一個 hot page,這個其實就是所在線程正在使用的 AutoreleasePoolPage
堰氓,hotPage
的實現(xiàn)有一點需要注意的地方:
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
hotPage
的實現(xiàn)很簡單挤渐,使用 tls_get_direct
獲得線程(TLS, Thread-local storage)的 AutoreleasePoolPage
對象,fastcheck
是對 magic
的檢查双絮,但是如果發(fā)現(xiàn)結果是 EMPTY_POOL_PLACEHOLDER
也就是 1 的話浴麻,也返回 nil
。
EMPTY_POOL_PLACEHOLDER
從注釋的說明可以知道囤攀,是對當只有一個自動釋放池創(chuàng)建了(push 了)并且沒有任何對象被 autorelease
時的優(yōu)化∪砻猓現(xiàn)在只需要知道它的存在就好。
回到 autoreleaseFast
函數(shù)焚挠,在拿到 page
后膏萧,需要對 page
的不同情況進行不同的處理。
先看最簡單的情況蝌衔,也就是有 hot page 并且它沒有滿的情況榛泛,這個時候調用了 page
的 add
方法:
id *add(id obj)
{
assert(!full());
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
return ret;
}
可以看到,這就是將 obj
存到 next
的位置胚委,并將 next
加 1挟鸠,典型的入棧操作。如果 obj
是一個對象(autorelease
方法的調用)亩冬,這就是將對象保存在自動釋放池了艘希,如果 obj
是 POOL_BOUNDARY
也就是 nil
(push
方法的調用)則這里便是自動釋放池的分界。
繼續(xù)看 page
為 nil
的情況硅急,也就是對 autoreleaseNoPage
函數(shù)的調用:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj == POOL_BOUNDARY) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
進入到這個函數(shù)覆享,會有兩種情況,注釋里也已經說明了营袜,也是剛才 hotPage
函數(shù)實現(xiàn)注意的地方撒顿。hot page 是 EMPTY_POOL_PLACEHOLDER
也會被當作是 no page,進入這個函數(shù)荚板。
我們先假設一種情況凤壁,是第一個自動釋放池創(chuàng)建時吩屹,首先對 haveEmptyPoolPlaceholder
函數(shù)的結果進行判斷:
static inline bool haveEmptyPoolPlaceholder()
{
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}
這個函數(shù)其實就是判斷 hot page 是不是 EMPTY_POOL_PLACEHOLDER
,因為我們現(xiàn)在假設為第一次創(chuàng)建自動釋放池拧抖,所以這個函數(shù)的返回值便是 false
煤搜,并且 obj
參數(shù)的值是 POOL_BOUNDARY
,因此 autoreleaseNoPage
會調用 setEmptyPoolPlaceholder
并返回唧席,而 setEmptyPoolPlaceholder
的實現(xiàn):
static inline id* setEmptyPoolPlaceholder()
{
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
就是將 hot page 設置為 EMPTY_POOL_PLACEHOLDER
擦盾。這樣,在第一次創(chuàng)建(push)一個自動釋放池時淌哟,并沒有生成 AutoreleasePoolPage
對象迹卢,而是使用了一個占位符。
現(xiàn)在進入第二種情況徒仓,在上面的情況發(fā)生完之后腐碱,有一個對象被 autorelease
了,流程也會進入 autoreleaseNoPage
蓬衡,但是現(xiàn)在 haveEmptyPoolPlaceholder
返回的是 true
了喻杈,將會把 pushExtraBoundary
也設置為 true
。
這樣在接下來的代碼中狰晚,會創(chuàng)建新的對象 page
并將它設置為 hot page筒饰,因為發(fā)現(xiàn) pushExtraBoundary
為 ture
,因此還需要 add
一個 POOL_BOUNDARY
壁晒。最后再將對象也加入瓷们,就完事了。
最后看到 page
滿了的情況秒咐,也就是對 autoreleaseFullPage
函數(shù)的調用:
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
思路很清晰谬晕,就是檢查 page
有沒有還沒滿的 child
(順鏈表往下查),沒有的話就新建一個携取,再使用 add
函數(shù)將 obj
記錄攒钳。
pop
其實現(xiàn)在大概能感覺到,自動釋放池其實就是個用鏈表實現(xiàn)的一個棧雷滋。繼續(xù)看 pop
的實現(xiàn)也就一個自動釋放池結束的時候:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
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);
}
}
page->releaseUntil(stop);
// memory: delete empty children
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();
}
}
}
參數(shù) token
不撑,傳入的是 push
返回值,其實就是 push
函數(shù)插入 POOL_BOUNDARY
的地址(指針)晤斩,在 pop
里表示要一直釋放到 token
指向的地址為止焕檬。
pop
函數(shù)一開始會檢查 token
是不是 EMPTY_POOL_PLACEHOLDER
。當 token
是 EMPTY_POOL_PLACEHOLDER
時澳泵,會繼續(xù)檢查是否有 hot page(理論上來說不應該會有 hot page实愚,一個疑問),如果沒有 hot page,則直接將 hot page 設置為 nil
腊敲,如果有 hot page击喂,則重新調用 pop
,傳入的 token
為 coldPage()-begin()
兔仰,coldPage
的實現(xiàn)如下:
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
很明顯茫负,所謂的 cold page 就是線程的第一個 AutoreleasePoolPage
蕉鸳。
當 pop
函數(shù)的 token
不是 EMPTY_POOL_PLACEHOLDER
時乎赴,進入正常的 pop 流程,首先要獲取到 token
也就是一個內存地址的所在 page潮尝,也就是 pageForPointer
函數(shù)的工作:
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
assert(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
因為 AutoreleasePoolPage
對象是根據(jù) SIZE
的大小來對齊的榕吼,所以使用地址 p
的值對 SIZE
取余就能獲取到 p
和所在 page 地址的偏移值(offset
),從而得到所在 page勉失,最后會對所在 page 的 magic
進行檢查羹蚣,也就是 fastcheck
所做的工作。
獲得了 page 以后乱凿,pop
函數(shù)還會檢查在 token
這個地址存儲的內容是否是 POOL_BOUNDRAY
:
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);
}
}
正常來說顽素,在這個地方 token
就應該得是 POOL_BOUNDARY
,因為 push
函數(shù)每次都是添加的 POOL_BOUNDARY
徒蟆。但這個地方進行了判斷胁出,其中如果如果 token
就是 page
的 begin
,并且 page
是第一個的話段审,則認為是正常情況(這其實是沒有 push
就直接 autorelease
了)全蝶。否則進入 badPop
流程,這個流程會在最新的 SDK (10.12, 10.0, 10.0, 3.0)上會直接產生 fatal寺枉。
接下來的正常流程抑淫,也就是 token
所指向的地址存儲的內容為 POOL_BOUNDARY
時,調用 releaseUntil
函數(shù):
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);
}
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
實現(xiàn)很容易看懂姥闪,就是循環(huán)直到到 stop
給每個對象調用 objc_release
也就等同于發(fā)送 release
消息始苇。其中每次都從 hot page 開始的原因注釋里進行了說明,是怕 release
方法里又 autorelease
了對象筐喳。
最后催式,pop
函數(shù)還要刪除不需要的空的 page:
// memory: delete empty children
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();
}
}
做了點小優(yōu)化,如果現(xiàn)在這個 page
只剩下不到一半的空間了疏唾,則多留一個 child
蓄氧。kill
的實現(xiàn)如下:
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->child = nil;
}
delete deathptr;
} while (deathptr != this);
}
其實就是沿著鏈表刪除。
總結
總的來看槐脏,自動釋放池的實現(xiàn)思想是很簡單的:
- 對每個線程來說喉童,用一個由
AutoreleasePoolPage
的組成的雙向鏈表維護一個棧,被autorelease
的對象記錄在這個棧中; - 使用
POOL_BOUNDARY
也就是nil
來對自動釋放池進行分隔堂氯。
當然蔑担,其中實現(xiàn)還是有著各種有趣的細節(jié)的。
本文原始地址:Objective-C 小記(8)autorelease