iOS面試之AutoreleasePool

原文連接git

AutoreleasePool對於iOS開發者來講,能夠說是"熟悉的陌生人"。熟悉是由於每一個iOS程序都被包圍在一個autoreleasepool中,陌生是由於整個autoreleasepool是黑盒的,開發者看不到autoreleasepool中發生了什麼,並且項目開發中直接用到autoreleasepool的地方很少。本文結合Runtime源碼,分析一下AutoreleasePool的內部實現。github

iOS程序入口

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

一個典型的main函數:bash

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文件。oop

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

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。this

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

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會銷燬。
相關文章
相關標籤/搜索