@autoreleasePool 自動釋放池

@autoreleasepool 自動釋放池

引言

在主程序運行時,會看到以下的代碼:

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

那麼@autoreleasepool 究竟做了什麼呢?

自動釋放池的主要工作

MRC時代,如果不知道一個對象什麼時候釋放,可以在初始化時加上autorelease,如:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"tutuge"] autorelease];
//use str...
[pool release];

也就是說,在創建時,可以給對象發送自動釋放的消息,當NSAutoreleasePool結束時,標記過“autorelease”的對象就會被release

ARC時代,我們不用手動的發送autoRelease消息,ARC會自動的幫我們加上這些,而這時候,@autoreleasepool做的事情,就和NSAutoreleasePool一樣,自動的釋放池中的對象。

@autoreleasepool原理

AutoreleasePoolPage
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage * child;
uint32_t const depth;
uint32_t hiwat;

在ARC下,我們使用@autoreleasepool{}來創建自動釋放池,隨後編譯器將其改寫成:

oid *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);

這兩個類都是對AutoreleasePoolPage的簡單封裝。所以自動釋放機制的核心在這個類。
- AutoreleasePoolPage沒有單獨的結構,室友若干個AutoreleasePoolPage雙向鏈表的形式組合而成的。
- AutoreleasePool是按線程一一對應的。
- AutoreleasePoolPage每個對象會開闢4096字節內存(虛擬內存一頁的大小)。除了上面的實力變量所佔空間,剩下的空間全部用來儲存autorelease對象的地址。
- AutoReleasePoolPage存有棧頂的下一個autorelease對象的下一個位置。
- 當一個AutoReleasePoolPage被沾滿時,會新建一個對象,連接鏈表,後面的autorelease對象在新的page中加入。新pagenext指針被初始化在棧底,然後繼續向棧頂添加新對象。
所以,向一個對象發送- autorelease消息,就是將這個對象加入到當前的AutoReleasePoolPage的棧頂next指針指向的位置。

調用釋放池push

每當執行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象,值爲0。
objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作爲入參,於是:
1. 根據傳入的哨兵對象地址找到哨兵對象所處的page
2. 在當前page中,將晚於哨兵對象插入的所有autorelease對象都發送一次- release消息,並向回移動next指針到正確位置。
3. 補充2:從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page

以下內容由於不理解,暫時先粘貼在此處

Autorelease返回值的快速釋放機制

值得一提的是,ARC下,runtime有一套對autorelease返回值的優化策略。
比如一個工廠方法:

+ (instancetype)createSark {
    return [self new];
}
// caller
Sark *sark = [Sark createSark];

秉着誰創建誰釋放的原則,返回值需要是一個autorelease對象才能配合調用方正確管理內存,於是乎編譯器改寫成了形如下面的代碼:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 代替我們調用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相當於代替我們調用了release

一切看上去都很好,不過既然編譯器知道了這麼多信息,幹嘛還要勞煩autorelease這個開銷不小的機制呢?於是乎,runtime使用了一些黑魔法將這個問題解決了。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內存作爲某個線程專有的存儲,以key-value的形式進行讀寫,比如在非arm架構下,使用pthread提供的方法實現:

void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);

說它是黑魔法可能被懂pthread的笑話- -

在返回值身上調用objc_autoreleaseReturnValue方法時,runtime將這個返回值object儲存在TLS中,然後直接返回這個object(不調用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue裏,發現TLS中正好存了這個對象,那麼直接返回這個object(不調用retain)。
於是乎,調用方和被調方利用TLS做中轉,很有默契的免去了對返回值的內存管理。

於是問題又來了,假如被調方和主調方只有一邊是ARC環境編譯的該咋辦?(比如我們在ARC環境下用了非ARC編譯的第三方庫,或者反之)
只能動用更高級的黑魔法。

黑魔法之__builtin_return_address

這個內建函數原型是

char *__builtin_return_address(int level)

作用是得到函數的返回地址,參數表示層數,如__builtin_return_address(0)表示當前函數體返回地址,傳1是調用這個函數的外層函數的返回值地址,以此類推。

- (int)foo {
    NSLog(@"%p", __builtin_return_address(0)); // 根據這個地址能找到下面ret的地址
    return 1;
}
// caller
int ret = [sark foo];

看上去也沒啥厲害的,不過要知道,函數的返回值地址,也就對應着調用者結束這次調用的地址(或者相差某個固定的偏移量,根據編譯器決定)
也就是說,被調用的函數也有翻身做地主的機會了,可以反過來對主調方乾點壞事。
回到上面的問題,如果一個函數返回前知道調用方是ARC還是非ARC,就有機會對於不同情況做不同的處理

黑魔法之反查彙編指令

通過上面的__builtin_return_address加某些偏移量,被調方可以定位到主調方在返回值後面的彙編指令:

// caller
int ret = [sark foo];
// 內存中接下來的彙編指令(x86,我不懂彙編,瞎寫的)
movq ??? ???
callq ???

而這些彙編指令在內存中的值是固定的,比如movq對應着0x48
於是乎,就有了下面的這個函數,入參是調用方__builtin_return_address傳入值

static bool callerAcceptsFastAutorelease(const void * const ra0) {
    const uint8_t *ra1 = (const uint8_t *)ra0;
    const uint16_t *ra2;
    const uint32_t *ra4 = (const uint32_t *)ra1;
    const void **sym;
    // 48 89 c7    movq  %rax,%rdi
    // e8          callq symbol
    if (*ra4 != 0xe8c78948) {
        return false;
    }
    ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;
    ra2 = (const uint16_t *)ra1;
    // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
    if (*ra2 != 0x25ff) {
        return false;
    }
    ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);
    sym = (const void **)ra1;
    if (*sym != objc_retainAutoreleasedReturnValue)
    {
        return false;
    }
    return true;
}

它檢驗了主調方在返回值之後是否緊接着調用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC環境,反之就走沒被優化的老邏輯。

其他Autorelease相關知識點

使用容器的block版本的枚舉器時,內部會自動添加一個AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 這裏被一個局部@autoreleasepool包圍着
}];

當然,在普通for循環和for in循環中沒有,所以,還是新版的block版本枚舉器更加方便。for循環中遍歷產生大量autorelease變量時,就需要手加局部AutoreleasePool咯。

看不懂的部分完

添加@autoreleasepool的場景

  1. 在基於命令行的程序,即沒有UI框架,比如AppKitCocoa框架。
  2. 在寫循環時,循環中包含了大量的臨時創建的對象。
  3. 創建了新的線程
  4. 長時間在後臺運行的任務

參考
@autoreleasepool的Apple文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章