天天看點

Autorelease的學習 我是前言 Autorelease對象什麼時候釋放? Autorelease原理 Autorelease傳回值的快速釋放機制 其他Autorelease相關知識點

我是前言

Autorelease機制是iOS開發者管理對象記憶體的好夥伴,MRC中,調用

[obj autorelease]

來延遲記憶體的釋放是一件簡單自然的事,ARC下,我們甚至可以完全不知道Autorelease就能管理好記憶體。而在這背後,objc和編譯器都幫我們做了哪些事呢,它們是如何協作來正确管理記憶體的呢?刨根問底,一起來探究下黑幕背後的Autorelease機制。

Autorelease對象什麼時候釋放?

這個問題拿來做面試題,問過很多人,沒有幾個能答對的。很多答案都是“目前作用域大括号結束時釋放”,顯然木有正确了解Autorelease機制。

在沒有手加Autorelease Pool的情況下,Autorelease對象是在目前的

runloop

疊代結束時釋放的,而它能夠釋放的原因是系統在每個runloop疊代中都加入了自動釋放池Push和Pop

小實驗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      
__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    // str是一個autorelease對象,設定一個weak的引用來觀察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@", reference); // Console: (null)
}
      

這個實驗同時也證明了

viewDidLoad

viewWillAppear

是在同一個runloop調用的,而

viewDidAppear

是在之後的某個runloop調用的。

由于這個vc在loadView之後便add到了window層級上,是以

viewDidLoad

viewWillAppear

是在同一個runloop調用的,是以在

viewWillAppear

中,這個autorelease的變量依然有值。 

當然,我們也可以手動幹預Autorelease對象的釋放時機: 

1
2
3
4
5
6
7
8
      
- (void)viewDidLoad
{
    [super viewDidLoad];
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    }
    NSLog(@"%@", str); // Console: (null)
}
      

Autorelease原理

AutoreleasePoolPage

ARC下,我們使用

@autoreleasepool{}

來使用一個AutoreleasePool,随後編譯器将其改寫成下面的樣子: 

1
2
3
      
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
      

而這兩個函數都是對

AutoreleasePoolPage

的簡單封裝,是以自動釋放機制的核心就在于這個類。 

AutoreleasePoolPage是一個C++實作的類

Autorelease的學習 我是前言 Autorelease對象什麼時候釋放? Autorelease原理 Autorelease傳回值的快速釋放機制 其他Autorelease相關知識點
  • AutoreleasePool并沒有單獨的結構,而是由若幹個AutoreleasePoolPage以

    雙向連結清單

    的形式組合而成(分别對應結構中的parent指針和child指針)
  • AutoreleasePool是按線程一一對應的(結構中的thread指針指向目前線程)
  • AutoreleasePoolPage每個對象會開辟4096位元組記憶體(也就是虛拟記憶體一頁的大小),除了上面的執行個體變量所占空間,剩下的空間全部用來儲存autorelease對象的位址
  • 上面的

    id *next

    指針作為遊标指向棧頂最新add進來的autorelease對象的下一個位置
  • 一個AutoreleasePoolPage的空間被占滿時,會建立一個AutoreleasePoolPage對象,連接配接連結清單,後來的autorelease對象在新的page加入

是以,若目前線程中隻有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象位址時記憶體如下圖:

Autorelease的學習 我是前言 Autorelease對象什麼時候釋放? Autorelease原理 Autorelease傳回值的快速釋放機制 其他Autorelease相關知識點

圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page對象,與這一頁連結清單連接配接完成後,新page的

next

指針被初始化在棧底(begin的位置),然後繼續向棧頂添加新對象。

是以,向一個對象發送

- autorelease

消息,就是将這個對象加入到目前AutoreleasePoolPage的棧頂next指針指向的位置

釋放時刻

每當進行一次

objc_autoreleasePoolPush

調用時,runtime向目前的AutoreleasePoolPage中add進一個

哨兵對象

,值為0(也就是個nil),那麼這一個page就變成了下面的樣子: 

Autorelease的學習 我是前言 Autorelease對象什麼時候釋放? Autorelease原理 Autorelease傳回值的快速釋放機制 其他Autorelease相關知識點

objc_autoreleasePoolPush

的傳回值正是這個哨兵對象的位址,被

objc_autoreleasePoolPop(哨兵對象)

作為入參,于是:

  1. 根據傳入的哨兵對象位址找到哨兵對象所處的page
  2. 在目前page中,将晚于哨兵對象插入的所有autorelease對象都發送一次

    - release

    消息,并向回移動

    next

    指針到正确位置
  3. 補充2:從最新加入的對象一直向前清理,可以向前跨越若幹個page,直到哨兵所在的page

剛才的objc_autoreleasePoolPop執行後,最終變成了下面的樣子: 

Autorelease的學習 我是前言 Autorelease對象什麼時候釋放? Autorelease原理 Autorelease傳回值的快速釋放機制 其他Autorelease相關知識點

嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常簡單了,pop的時候總會釋放到上次push的位置為止,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣,每次一層,互不影響。

【附加内容】

Autorelease傳回值的快速釋放機制

值得一提的是,ARC下,runtime有一套對autorelease傳回值的優化政策。

比如一個工廠方法: 

1
2
3
4
5
      
+ (instancetype)createSark {
    return [self new]; 
}
// caller
Sark *sark = [Sark createSark];
      

秉着誰建立誰釋放的原則,傳回值需要是一個autorelease對象才能配合調用方正确管理記憶體,于是乎編譯器改寫成了形如下面的代碼:

1
2
3
4
5
6
7
8
      
+ (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提供的方法實作: 

1
2
      
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是調用這個函數的外層函數的傳回值位址,以此類推。

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

看上去也沒啥厲害的,不過要知道,函數的傳回值位址,也就對應着調用者結束這次調用的位址(或者相差某個固定的偏移量,根據編譯器決定)

也就是說,被調用的函數也有翻身做地主的機會了,可以反過來對主調方幹點壞事。

回到上面的問題,如果一個函數傳回前知道調用方是ARC還是非ARC,就有機會對于不同情況做不同的處理

黑魔法之反查彙編指令

通過上面的__builtin_return_address加某些偏移量,被調方可以定位到主調方在傳回值後面的

彙編指令

: 

1
2
3
4
5
      
// caller 
int ret = [sark foo];
// 記憶體中接下來的彙編指令(x86,我不懂彙編,瞎寫的)
movq ??? ???
callq ???
      

而這些彙編指令在記憶體中的值是固定的,比如movq對應着0x48。

于是乎,就有了下面的這個函數,入參是調用方__builtin_return_address傳入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
      
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 *s[email protected](%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: 

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

當然,在普通for循環和for in循環中沒有,是以,還是新版的block版本枚舉器更加友善。for循環中周遊産生大量autorelease變量時,就需要手加局部AutoreleasePool咯。

繼續閱讀