iOS面試之AutoreleasePool

原文鏈接

AutoreleasePool對于iOS開發者來說,可以說是"熟悉的陌生人"。熟悉是因為每個iOS程序都被包圍在一個autoreleasepool中,陌生是因為整個autoreleasepool是黑盒的,開發者看不到autoreleasepool中發生了什么,而且項目開發中直接用到autoreleasepool的地方不多。本文結合Runtime源碼,分析一下AutoreleasePool的內部實現。

iOS程序入口

我們都知道,iOS程序的入口是main.m文件中的main方法。在Xcode中新建一個iOS項目,Xcode會自動生成main.m文件。main.m文件中只有一個main方法,絕大多數情況下,不需要修改main.m中的代碼。

一個典型的main函數:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

可以看到,main函數的函數體是包含在一個autoreleasepool中的??上У氖?,通過command + 鼠標左鍵,并不能看到autoreleasepool的定義。不過我們可以使用clang,將main.m文件編譯成C++代碼,看看autoreleasepool發生了什么。

使用命令:

clang -rewrite-objc main.m

生成main.cpp文件。

生成的main.cpp文件很大,大概有10w行,不需要關注文件到底有多少行。將文件拖到最下面,看一下main函數變成了什么:

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

    }
}

整個函數的函數體被包圍在了__AtAutoreleasePoool __autoreleasepool中。而且前面有關于@autoreleasepool的注釋,因此可以猜測autoreleasepool被表示成了__AtAutoreleasePool。

在main.cpp中搜索一下,看看__AtAutoreleasePool是什么。

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool是一個結構體,結構體中包含構造函數和析構函數。構造函數中調用了

atautoreleasepoolobj = objc_autoreleasePoolPush();

析構函數中調用了

objc_autoreleasePoolPop(atautoreleasepoolobj);

于是,關注的重點就成了objc_autoreleasePoolPush和objc_autoreleasePoolPop函數。

objc_autoreleasePoolPush和objc_autoreleasePoolPop函數在Runtime源碼中可以找到,位于NSObject.mm文件中。

AutoreleasePoolPage

看一下Runtime源碼中objc_autoreleasePoolPush和objc_autoreleasePoolPop函數的實現。

void * objc_autoreleasePoolPush(void)
{
    // 調用了AutoreleasePoolPage中的push方法
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    // 調用了AutoreleasePoolPage中的pop方法
    AutoreleasePoolPage::pop(ctxt);
}

通過源碼可以看到分別調用了AutoreleasePoolPage的push方法和pop方法。

AutoreleasePoolPage的定義

AutoreleasePoolPage的定義位于NSObject.mm文件中:

// AutoreleasePoolPage的大小是4096字節
class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

    // 哨兵對象
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    // AutoreleasePoolPage的大小,通過宏定義,可以看到是4096字節
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    // 一個AutoreleasePoolPage中會存儲多個對象
    // next指向的是下一個AutoreleasePoolPage中下一個為空的內存地址(新來的對象會存儲到next處)
    id *next;
    // 保存了當前頁所在的線程(一個AutoreleasePoolPage屬于一個線程,一個線程中可以有多個AutoreleasePoolPage)
    pthread_t const thread;
    // AutoreleasePoolPage是以雙向鏈表的形式連接
    // 前一個節點
    AutoreleasePoolPage * const parent;
    // 后一個節點
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

除此之外,還定義了很多方法,方法的作用及實現下面會分析。

在上面的定義中,我已經加了些注釋。通過注釋可以得到:

  1. 一個AutoreleasePoolPage的大小是4096字節(和操作系統中一頁的大小一致)。
  2. parent指針和child指針特別有意思,指向的同樣是AutoreleasePoolPage,如果對數據結構比較熟悉的話,看到類似的定義,應該可以聯想到雙向鏈表或者樹結構。實際上也正是如此,下面我們會提到AutoreleasePoolPage組成的雙向鏈表。
  3. thread表示當前AutoreleasePoolPage所屬的線程。
  4. next指針指向了下一個空的地址。一個AutoreleasePoolPage中可以存儲多個對象地址,新來的對象地址會存放到next處,然后next移動到下一個地址。這樣的操作有沒有聯想到哪種數據結構?是不是和棧的top指針特別類似?

對AutoreleasePoolPage的定義有了基本的了解之后,來看一下push方法和pop方法。

AutoreleasePoolPage::push方法

AutoreleasePoolPage中的push方法,經過簡化之后如下:

static inline void *push() 
{
    id *dest;
    // POOL_BOUNDARY其實就是nil
    dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}

push方法中主要調用了autoreleaseFast方法,所傳入的參數是POOL_BOUNDARY,也就是nil。看你一下autoreleaseFast方法的實現。

static inline id *autoreleaseFast(id obj)
{
    // hotPage就是當前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不滿,將對象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage但是hotPage已滿
        // 使用autoreleaseFullPage初始化一個新頁,并將對象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 無hotPage
        // 使用autoreleaseNoPage創建一個hotPage,并將對象添加到新創建的page中
        return autoreleaseNoPage(obj);
    }
}

我在代碼中已經加入了注釋,再來看一下里面涉及到的一些方法。

hotPage方法
// 獲取正在使用的AutoreleasePoolPage
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可以理解成當前正在使用的page。上面也提到了,AutoreleasePoolPage中有parent和child指針,實際上AutoreleasePool就是由一個個AutoreleasePoolPage組成的雙向鏈表。這里得到的hotPage可以理解成鏈表最末尾的結點。

獲取hotPage的方法是tls_get_direct(key),key是AutoreleasePoolPage結構中定義的

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
setHotPage方法
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

將某個page設置成hotPage。

full方法
// 是否已滿
bool full() { 
    return next == end();
}

判斷當前的AutoreleasePoolPage是否已滿。判斷標準是next等于AutoreleasePoolPage的尾地址。上面已經提到了,AutoreleasePoolPage的大小是4096字節,既然大小是固定的,那么肯定有滿的一刻,full方法就是用來做這個得。

add方法
// 將對象添加到AutoreleasePoolPage中
id *add(id obj)
{
    id *ret = next;  // faster than `return next-1` because of aliasing
    // next = obj; next++;
    // 也就是將obj存放在next處,并將next指向下一個位置
    *next++ = obj;
    return ret;
}

add方法所做的操作也比較簡單,就是將當前對象存放在next指向的位置,并且將next指向下一個位置。可以理解成一個棧,next指針類似于棧的top指針。

autoreleaseFullPage方法
// 新建一個AutoreleasePoolPage,并將obj添加到新的AutoreleasePoolPage中
// 參數page是新AutoreleasePoolPage的父節點
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    do {
        // 如果page->child存在,那么使用page->child
        if (page->child) page = page->child;
        // 否則的話,初始化一個新的AutoreleasePoolPage
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    // 將找到的合適的page設置成hotPage
    setHotPage(page);
    // 將對象添加到hotPage中
    return page->add(obj);
}

autoreleaseFullPage所做的操作有三步:

  1. 首先找到一個合適的AutoreleasePoolPage,這里合適的page指的是不滿的page。具體找的過程是從傳過來的參數page的child開始找,如果page->child存在,則判斷page->child是否是合適的page;如果page->child不存在,則初始化一個新的AutoreleasePoolPage,這里使用的是AutoreleasePoolPage的構造函數,傳入的page是新的AutoreleasePoolPage的父節點。
  2. 將找到的AutoreleasePoolPage對象設置成hotPage
  3. 調用add方法,將對象添加到找到的page中
autoreleaseNoPage方法
// AutoreleasePool中還沒有AutoreleasePoolPage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // 初始化一個AutoreleasePoolPage
    // 當前內存中不存在AutoreleasePoolPage,則從頭開始構建AutoreleasePoolPage,也就是其parent為nil
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    // 將初始化的AutoreleasePoolPage設置成hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // 添加一個邊界對象(nil)
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // 將對象添加到AutoreleasePoolPage中
    return page->add(obj);
}

autoreleaseNoPage方法處理的是當前autoreleasePool中還沒有autoreleasePoolPage的情況。既然沒有,需要新建一個AutoreleasePoolPage,且該page的父指針指向空,然后將該page設置成hotPage。之后向該page中先是添加了POOL_BOUNDARY,然后在把對象obj添加到page中。

關于為什么需要添加POOL_BOUNDARY的原因,后面會說到。

現在已經把autoreleaseFast方法中涉及到的方法都弄明白了,再來看一下autoreleaseFast方法做的操作。

static inline id *autoreleaseFast(id obj)
{
    // hotPage就是當前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不滿,將對象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage但是hotPage已滿
        // 使用autoreleaseFullPage初始化一個新頁,并將對象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 無hotPage
        // 使用autoreleaseNoPage創建一個hotPage,并將對象添加到新創建的page中
        return autoreleaseNoPage(obj);
    }
}

autoreleaseFast方法首先找到hotPage,也就是當前的page,之后分為三種情況:

  1. 如果hotPage存在,且hotPage還不滿,則將對象添加到hotPage中
  2. 如果hotPage存在,但是hotPage已滿,則調用autoreleaseFullPage方法
    autoreleaseFullPage方法上面已經說了,做的操作就是從page開始找,找到一個不滿的page,將找到的page設置成hotPage,并且將對象添加到找到的page中。
  3. 如果hotPage不存在,則調用autoreleaseNoPage方法
    autoreleaseNoPage方法上面說了,做的操作就是新建一個AutoreleasePoolPage,并且將對象添加到新建的AutoreleasePoolPage中。

至此,AutoreleasePoolPage的push方法介紹完畢。

AutoreleasePoolPage::pop方法

AutoreleasePoolPage::pop方法的代碼經過簡化之后如下:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    page->releaseUntil(stop);
}

同理,還是先看一下里面調用到的方法的實現。不過,在介紹pop內部調用的方法之前,先來看一下pop方法中的參數到底是什么。

在文章開始處,我們是從clang重寫之后的main.cpp文件引入到Runtime源碼的,現在再回過去看一下main.cpp文件中的__AtAutoreleasePool結構體:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

objc_autoreleasePoolPop中的參數是atautoreleasepoolobj,而atautoreleasepoolobj是objc_autoreleasePoolPush方法返回的。也就是說,AutoreleasePoolPage中pop方法的參數是AutoreleasePoolPage中push方法返回的,比較拗口,可以多理解一下。那么,AutoreleasePoolPage中push方法返回的是什么呢?

上面已經介紹過push方法了,push方法內部分為了三種情況,無論哪種情況,最終都調用了add方法,并且返回了add方法的返回值。add方法的實現如下:

id *add(id obj)
{
    id *ret = next;  // faster than `return next-1` because of aliasing
    // next = obj; next++;
    // 也就是將obj存放在next處,并將next指向下一個位置
    *next++ = obj;
    return ret;
}

add方法返回的就是所要添加對象在AutoreleasePoolPage中的地址。

而在push方法中,添加的對象是哨兵對象POOL_BOUNDARY,所以,在pop方法中,參數也是哨兵對象POOL_BOUNDARY。

pageForPointer方法

pageForPointer的代碼如下:

static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    // 調用了pageForPointer方法
    return pageForPointer((uintptr_t)p);
}

// 根據內存地址,獲取指針所在的AutoreleasePage的首地址
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    // 偏移量
    uintptr_t offset = p % SIZE;
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();
    return result;
}

pageForPointer方法的作用是根據指針位置,找到該指針位于哪個AutoreleasePoolPage,并返回找到的AutoreleasePoolPage(之前已經提到了,AutoreleasePool是由一個個AutoreleasePoolPage組成的雙向鏈表,不止一個AutoreleasePoolPage)。

releaseUntil方法

releaseUntil方法的代碼如下:

// 釋放對象
// 這里需要注意的是,因為AutoreleasePool實際上就是由AutoreleasePoolPage組成的雙向鏈表
// 因此,*stop可能不是在最新的AutoreleasePoolPage中,也就是下面的hotPage,這時需要從hotPage
// 開始,一直釋放,直到stop,中間所經過的所有AutoreleasePoolPage,里面的對象都要釋放
void releaseUntil(id *stop) 
{
    // 釋放AutoreleasePoolPage中的對象,直到next指向stop
    while (this->next != stop) {
        // hotPage可以理解為當前正在使用的page
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        // 如果page為空的話,將page指向上一個page
        // 注釋寫到猜測這里可以使用if,我感覺也可以使用if
        // 因為根據AutoreleasePoolPage的結構,理論上不可能存在連續兩個page都為空
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        // obj = page->next; page->next--;
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));

        // POOL_BOUNDARY為nil,是哨兵對象
        if (obj != POOL_BOUNDARY) {
            // 釋放obj對象
            objc_release(obj);
        }
    }
    // 重新設置hotPage
    setHotPage(this);
}

代碼中我已經加了注釋,releaseUntil做的操作就是持續釋放AutoreleasePoolPage中的對象,直到next = stop。

回過頭來再來看一下pop方法:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    page->releaseUntil(stop);
}

pop方法中主要做了兩步:

  1. 根據token,也就是哨兵對象找到該哨兵對象所處的page
  2. 從hotPage開始,一直刪除到第一步找到的page.next==stop(哨兵對象)的位置

至此,關于AutoreleasePoolPage以及其中的關鍵方法就全部介紹完畢了。如果到這里,關于AutoreleasePool、AutoreleasePoolPage、哨兵對象還有點蒙的話,不要著急,繼續往下看。

AutoreleasePool和AutoreleasePoolPage的關系

實際上,Runtime中并沒有AutoreleasePool結構的定義,AutoreleasePool是由AutoreleasePoolPage組成的雙向鏈表,如下圖:

image

在autoreleasepool的開始處,會調用AutoreleasePoolPage的push方法;在autoreleasepool的結束處,會調用AutoreleasePoolPage的pop方法。在AutoreleasePoolPage的push方法中,會往AutoreleasePoolPage中插入哨兵對象,之后的對象依次插入到AutoreleasePoolPage中。如下表示:

AutoreleasePoolPage::push(); // 這里會向AutoreleasePoolPage中插入哨兵對象
*** // 開發者自己寫的代碼,代碼中的對象會依次插入到AutoreleasePoolPage中
***
AutoreleasePoolPage::pop(nil);

當AutoreleasePoolPage滿之后,會新建一個AutoreleasePoolPage,繼續將對象添加到新的AutoreleasePoolPage中。

通過上面的介紹,可以知道,AutoreleasePool由多個AutoreleasePoolPage組成,且AutoreleasePool的開始處必定是一個哨兵對象。到這里,哨兵對象的作用也就清楚了,哨兵對象是用來分隔不同的AutoreleasePool的。

當調用AutoreleasePoolPage::pop(nil)方法時,會從某個autoreleasepool開始,一直釋放到參數哨兵對象所屬的autoreleasepool??梢允峭粋€autoreleasepool,也可以不是同一個autoreleasepool,當不是同一個autoreleasepool時,可以理解成是嵌套autoreleasepool釋放。

到這里,AutoreleasePool、AutoreleasePoolPage、哨兵對象之間的關系應該就理解了。

關于AutoreleasePool的一些面試題

AutoreleasePool在面試中出現的頻率也非常高,接下來分享幾道關于AutoreleasePool的面試題。

AutoreleasePool和線程的關系

確切地說,應該是AutoreleasePoolPage和線程的關系。AutoreleasePool是由AutoreleasePoolPage組成的雙向鏈表,根據AutoreleasePoolPage的定義,每一個AutoreleasePoolPage都屬于一個特定的線程。也就是說,一個線程可以有多個AutoreleasePoolPage,但是一個AutoreleasePoolPage只能屬于一個線程。

AutoreleasePool和Runloop的關系

Runloop,即運行循環。從直觀上看,RunLoop和AutoreleasePool似乎沒什么關系,其實不然。在一個完整的RunLoop中,RunLoop開始的時候,會創建一個AutoreleasePool,在RunLoop運行期間,autorelease對象會加入到自動釋放池中。在RunLoop結束之前,AutoreleasePool會被銷毀,也就是調用AutoreleasePoolPage::pop方法,在該方法中,自動釋放池中的所有對象會收到release消息。正常情況下,AutoreleasePool中的對象發送完release消息后,引用計數應該為0,會被釋放,如果引用計數不為0,則發生了內存泄露。

AutoreleasePool中對象什么時候釋放?

其實上面已經說過了,AutoreleasePool銷毀時,AutoreleasePool中的所有對象都會發送release消息,對象會釋放。那么,AutoreleasePool什么時候銷毀呢?分兩種情況:

  1. 一種情況就是上面提到的,當前RunLoop結束之前,AutoreleasePool會銷毀。這種情況適用于系統自動生成的AutoreleasePool。
  2. 第二種情況是開發者自己寫的AutoreleasePool,常見于for循環中,將循環體包在一個AutoreleasePool中。這種情況下,在AutoreleasePool作用域之后(也就是大括號),AutoreleasePool會銷毀。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容