詳解autoreleasepool

本文首發於我的博客html

前言

文章開始以前,先想一想下面三種場景,分別輸出什麼呢?c++

注意str的長度不能過短git

注意str的長度不能過短github

注意str的長度不能過短objective-c

@interface ViewController ()
{
    __weak NSString *string_weak;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 場景一
//    NSString *str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
//    string_weak = str;
    
    // 場景二
//    @autoreleasepool {
//        NSString *str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
//        string_weak = str;
//    }
//
//    // 場景三
    NSString *str = nil;
    @autoreleasepool {
        str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
        string_weak = str;
    }
    NSLog(@"string: %@ %s", string_weak,__func__);
}
- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}
複製代碼

這個問題,暫時先放下,繼續往下看。bash

autoreleasepool生成c++文件

有以下代碼數據結構

@autoreleasepool {
     NSObject *obj = [[NSObject alloc] init];
}

複製代碼

執行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp生成c++文件,其對應的代碼以下所示。app

{ __AtAutoreleasePool __autoreleasepool; 
        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    }
複製代碼

簡化一下也就是less

{
 __AtAutoreleasePool __autoreleasepool; 
  NSObject *obj = [[NSObject alloc] init];
 }
複製代碼

其中__AtAutoreleasePool是什麼呢?這是一個結構體,其內容以下,包含一個構造函數,在建立結構體的時候調用。一個析構函數,在結構體銷燬的時候調用。iphone

struct __AtAutoreleasePool {
	//構造函數,在建立結構體的時候調用
  __AtAutoreleasePool() {
  		atautoreleasepoolobj = objc_autoreleasePoolPush();
  }
  
  //析構函數,在結構體銷燬的時候調用
  ~__AtAutoreleasePool(){
 	 objc_autoreleasePoolPop(atautoreleasepoolobj);
  }
  
  void * atautoreleasepoolobj;
};
複製代碼

因此,放在一塊兒就是在開始的時候調用 objc_autoreleasePoolPush()結束時候調用objc_autoreleasePoolPop(atautoreleasepoolobj)

struct __AtAutoreleasePool {
	//構造函數,在建立結構體的時候調用
  __AtAutoreleasePool() {
  		atautoreleasepoolobj = objc_autoreleasePoolPush();
  }
  	// 寫在autoreleasepool內的代碼
    NSObject *obj = [[NSObject alloc] init];
  
  //析構函數,在結構體銷燬的時候調用
  ~__AtAutoreleasePool(){
 	 objc_autoreleasePoolPop(atautoreleasepoolobj);
  }
  
  void * atautoreleasepoolobj;
};
複製代碼

源碼分析

AutoreleasePoolPage

具體源碼能夠再Runtime源碼中查看,從源碼能夠看到objc_autoreleasePoolPush()objc_autoreleasePoolPop

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}


void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
複製代碼

也就是說,這兩個函數都是操做AutoreleasePoolPage來實現的。

AutoreleasePoolPage中代碼較多,篩選出主要代碼以下

class AutoreleasePoolPage 
{

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    
    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);
    }
    
    ...
}
複製代碼

能夠看出

AutoreleasePoolPage對象經過雙向鏈表的形式鏈接在一塊兒

其中

  • magic 用來校驗 AutoreleasePoolPage 的結構是否完整;
  • next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
  • thread 指向當前線程;說明了,AutoreleasePoolPage和線程一一對應的。
  • parent 指向父結點
  • child 指向子結點
  • depth 表明深度,從 0 開始,日後遞增 1;
  • hiwat 表明 high water mark 。

每一個AutoreleasePoolPage對象佔用4096字節

  • 每一個AutoreleasePoolPage對象佔用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象的地址
#define I386_PGBYTES 4096 /* bytes per 80386 page */

#define PAGE_SIZE I386_PGBYTES

static size_t const SIZE = PAGE_MAX_SIZE


  id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
 }
複製代碼

從上面的源碼中能夠看出來

  • 每一個AutoreleasePoolPage有是4096字節,
  • 以及begin指向的是開始存放autorelease對象的地方,
  • end指向結尾的位置

AutoreleasePoolPage存不下了怎麼辦?

若是一個AutoreleasePoolPage存不下了,就會再建立一個AutoreleasePoolPage對象,第一個AutoreleasePoolPage對象child指向第二個AutoreleasePoolPage對象,第二個AutoreleasePoolPage對象parent指向第一個AutoreleasePoolPage對象。圖形表示就是以下

pushpopautorelease

AutoreleasePoolPage裏面有pushpop函數

  • 調用push方法會將一個POOL_BOUNDARY入棧,而且返回其存放的內存地址

  • 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最後一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY

  • id *next指向了下一個能存放autorelease對象地址的區域

push

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    

複製代碼
static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {//page沒有滿,就把obj對象加到page
            return page->add(obj);
        } else if (page) {//page滿了 建立新的page
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
複製代碼

pop

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);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else 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();
            }
        }
    }

複製代碼

autorelease

inline id 
objc_object::autorelease()
{
    if (isTaggedPointer()) return (id)this;
    // 調用rootAutorelease
    if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
複製代碼

rootAutorelease調用rootAutorelease2

inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
複製代碼

rootAutorelease2調用autorelease

__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
複製代碼

autorelease

static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    //再次調用autoreleaseFast
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}
複製代碼

POOL_BOUNDARY

上面的源碼能夠發現POOL_BOUNDARY是個很重要的角色,至關於一個哨兵,

  • 每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象(POOL_BOUNDARY),值爲0(也就是個nil)
  • objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)做爲入參,因而
    • 根據傳入的哨兵對象地址找到哨兵對象所處的page
    • 在當前page中,將晚於哨兵對象插入的全部autorelease對象都發送一次- release消息,並向回移動next指針到正確位置
    • 從最新加入的對象一直向前清理,能夠向前跨越若干個page,直到哨兵所在的page

@autoreleasepool的嵌套

若是多個@autoreleasepool嵌套會怎麼樣呢?

打印

源碼中有以下代碼

void 
_objc_autoreleasePoolPrint(void)
{
    AutoreleasePoolPage::printAll();
}


 static void printAll()
    {        
        _objc_inform("##############");
        _objc_inform("AUTORELEASE POOLS for thread %p", pthread_self());

        AutoreleasePoolPage *page;
        ptrdiff_t objects = 0;
        for (page = coldPage(); page; page = page->child) {
            objects += page->next - page->begin();
        }
        _objc_inform("%llu releases pending.", (unsigned long long)objects);

        if (haveEmptyPoolPlaceholder()) {
            _objc_inform("[%p] ................ PAGE (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
            _objc_inform("[%p] ################ POOL (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
        }
        else {
            for (page = coldPage(); page; page = page->child) {
                page->print();
            }
        }

        _objc_inform("##############");
    }
複製代碼

也就是說_objc_autoreleasePoolPrint函數能夠用來打印一些日誌

一層@autoreleasepool

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {

        _objc_autoreleasePoolPrint();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

輸出以下,只有一個哨兵對象(POOL)

objc[32644]: ##############
objc[32644]: AUTORELEASE POOLS for thread 0x11b111d40
objc[32644]: 3 releases pending.
objc[32644]: [0x7fcabf802000]  ................  PAGE  (hot) (cold)
objc[32644]: [0x7fcabf802038]    0x600003f70500  __NSArrayI
objc[32644]: [0x7fcabf802040]    0x600000950f00  __NSSetI
objc[32644]: [0x7fcabf802048]  ################ POOL 0x7fcabf802048
objc[32644]: ##############

複製代碼

三層@autoreleasepool

若是有三個@autoreleasepool呢?

extern void _objc_autoreleasePoolPrint(void);

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

輸出以下,有三個POOL,說明有三個哨兵。

objc[32735]: ##############
objc[32735]: AUTORELEASE POOLS for thread 0x114f02d40
objc[32735]: 5 releases pending.
objc[32735]: [0x7f91fd005000]  ................  PAGE  (hot) (cold)
objc[32735]: [0x7f91fd005038]    0x600001bbd380  __NSArrayI
objc[32735]: [0x7f91fd005040]    0x600002da4eb0  __NSSetI
objc[32735]: [0x7f91fd005048]  ################ POOL 0x7f91fd005048
objc[32735]: [0x7f91fd005050]  ################ POOL 0x7f91fd005050
objc[32735]: [0x7f91fd005058]  ################ POOL 0x7f91fd005058
objc[32735]: ##############

複製代碼

銷燬一個@autoreleasepool

若是上面代碼中最裏面的@autoreleasepool退出以後再打印呢?

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            @autoreleasepool {
                
            }
            //打印的時候,最裏面的@autoreleasepool已經退出了
             _objc_autoreleasePoolPrint();
        }
       
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

輸出爲以下,只有兩個哨兵(POOL)

objc[32812]: ##############
objc[32812]: AUTORELEASE POOLS for thread 0x1142b1d40
objc[32812]: 4 releases pending.
objc[32812]: [0x7fe86e800000]  ................  PAGE  (hot) (cold)
objc[32812]: [0x7fe86e800038]    0x600003edb0c0  __NSArrayI
objc[32812]: [0x7fe86e800040]    0x6000008dd680  __NSSetI
objc[32812]: [0x7fe86e800048]  ################ POOL 0x7fe86e800048
objc[32812]: [0x7fe86e800050]  ################ POOL 0x7fe86e800050
objc[32812]: ##############
複製代碼

進一步說明了,

  • 調用push方法會將一個POOL_BOUNDARY入棧,而且返回其存放的內存地址

  • 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最後一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY

開頭的問題

如今咱們回過頭看文章開頭的問題,應該很好回答了。

@interface ViewController ()
{
    __weak NSString *string_weak;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 場景一
//    NSString *str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
//    string_weak = str;
    
    // 場景二
//    @autoreleasepool {
//        NSString *str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
//        string_weak = str;
//    }
//
//    // 場景三
    NSString *str = nil;
    @autoreleasepool {
        str =  [NSString stringWithFormat:@"https://ityongzhen.github.io/"];
        string_weak = str;
    }
    NSLog(@"string: %@ %s", string_weak,__func__);
}
- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}
複製代碼

輸出結果以下:

// 場景一
iOS定時器[24714:332118] string: https://ityongzhen.github.io/ -[ViewController viewDidLoad]
iOS定時器[24714:332118] string: https://ityongzhen.github.io/ -[ViewController viewWillAppear:]
iOS定時器[24714:332118] string: (null) -[ViewController viewDidAppear:]

場景二
iOS定時器[24676:331168] string: (null) -[ViewController viewDidLoad]
iOS定時器[24676:331168] string: (null) -[ViewController viewWillAppear:]
iOS定時器[24676:331168] string: (null) -[ViewController viewDidAppear:]

場景三
iOS定時器[24505:328544] string: https://ityongzhen.github.io/ -[ViewController viewDidLoad]
iOS定時器[24505:328544] string: (null) -[ViewController viewWillAppear:]
iOS定時器[24505:328544] string: (null) -[ViewController viewDidAppear:]
複製代碼

場景一

當使用 [NSString stringWithFormat:@"https://ityongzhen.github.io/"] 建立一個對象時,這個對象的引用計數爲 1 ,而且這個對象被系統自動添加到了當前的 autoreleasepool 中。當使用局部變量 str 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。由於在 ARC 下 NSString *str本質上就是 __strong NSString *str 。因此在 viewDidLoad 方法返回前,這個對象是一直存在的,且引用計數爲 2 。而當viewDidLoad 方法返回時,局部變量 str 被回收,指向了 nil 。所以,其所指向對象的引用計數 -1 ,變成了 1 。

而在 viewWillAppear 方法中,咱們仍然能夠打印出這個對象的值,在viewDidAppear方法中,這個值爲空,這個就要牽扯到RunLoop的知識了。詳解RunLoop之源碼分析一文講述了RunLoop的底層,這裏說一下,咱們的iOS處理事件是以RunLoop一直循環執行的。viewDidLoadviewWillAppear在同一個RunLoop循環中,因此在 viewWillAppear 方法中,咱們仍然能夠打印出這個對象的值,可是viewDidLoad的時候,那個RunLoop循環已經執行完了,這個對象才被完全的釋放。

場景二

當經過 [NSString stringWithFormat:@"https://ityongzhen.github.io/"] 建立一個對象時,這個對象的引用計數爲 1 。而當使用局部變量 str 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。而出了當前做用域時,局部變量 str 變成了 nil ,因此其所指向對象的引用計數變成 1 。另外,咱們知道當出了 @autoreleasepool {}的做用域時,當前 autoreleasepool 被 drain ,其中的 autoreleased 對象被 release 。因此這個對象的引用計數變成了 0 ,對象最終被釋放

場景三

當出了 @autoreleasepool {} 的做用域時,其中的 autoreleased 對象被 release ,對象的引用計數變成 1 。當出了局部變量 str 的做用域,即 viewDidLoad 方法返回時,str 指向了 nil ,其所指向對象的引用計數變成 0 ,對象最終被釋放

注意點

前面說了注意str的長度不能過短是爲何呢?

是由於若是str太短。例如

@interface ViewController ()
{
    __weak NSString *string_weak;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 場景一
//    NSString *str =  [NSString stringWithFormat:@"abc"];
//    string_weak = str;
    
    // 場景二
//    @autoreleasepool {
//        NSString *str =  [NSString stringWithFormat:@"abc"];
//        string_weak = str;
//    }
//
//    // 場景三
    NSString *str = nil;
    @autoreleasepool {
        str =  [NSString stringWithFormat:@"abc"];
        string_weak = str;
    }
    NSLog(@"string: %@ %s", string_weak,__func__);
}
- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"string: %@ %s", string_weak,__func__);
}
複製代碼

結果以下:

// 場景一
iOS定時器[24714:332118] string: abc -[ViewController viewDidLoad]
iOS定時器[24714:332118] string: abc -[ViewController viewWillAppear:]
iOS定時器[24714:332118] string: abc -[ViewController viewDidAppear:]

場景二
iOS定時器[24676:331168] string: abc -[ViewController viewDidLoad]
iOS定時器[24676:331168] string: abc -[ViewController viewWillAppear:]
iOS定時器[24676:331168] string: abc -[ViewController viewDidAppear:]

場景三
iOS定時器[24505:328544] string: abc -[ViewController viewDidLoad]
iOS定時器[24505:328544] string: abc -[ViewController viewWillAppear:]
iOS定時器[24505:328544] string: abc -[ViewController viewDidAppear:]
複製代碼

這是由於,字符串的abc採用的是Tagged Pointer技術,不是一個標準的OC對象。不存在說再堆上開闢空間存儲對象什麼的。關於Tagged Pointer能夠參考這篇文章iOS中的引用計數,這裏不作贅述。

總結

  • 自動釋放池的主要底層數據結構是:__AtAutoreleasePoolAutoreleasePoolPage

  • 調用了autorelease的對象最終都是經過AutoreleasePoolPage對象來管理的

  • 每一個AutoreleasePoolPage對象佔用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象的地址

  • 全部的AutoreleasePoolPage對象經過雙向鏈表的形式鏈接在一塊兒

  • 調用push方法會將一個POOL_BOUNDARY入棧,而且返回其存放的內存地址

  • 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最後一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY

  • id *next指向了下一個能存放autorelease對象地址的區域

  • iOS在主線程的Runloop中註冊了2個Observer

    • 第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
    • 第2個Observer
      • 監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
      • 監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()
  • 在當次RunLoop將要結束的時候,調用objc_autoreleasePoolPop()

參考資料

黑幕背後的Autorelease

Runtime源碼

Objective-C Autorelease Pool 的實現原理

詳解RunLoop之源碼分析

iOS底層原理

iOS中的引用計數

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索