經過__block的做用深刻研究block

block普通引用

默認狀況下,在block中訪問外部變量是經過複製一個變量來操做的,既能夠讀,可是寫操做不對原變量生效,下面經過代碼來舉證函數

NSString *a = @"testa";
    NSLog(@"block前,a在堆中的地址%p,a在棧中的地址%p",a,&a);
    void(^testBlock)(void) = ^(void){
        NSLog(@"block內,a在堆中的地址%p,a在棧中的地址%p",a,&a);
    };
    NSLog(@"block後,a在堆中的地址%p,a在棧中的地址%p",a,&a);
    testBlock();

能夠看出變量在堆中的地址實際上是一直不變的,在棧中的地址,在block外是不變的,block內從新開闢了一個空間來存放。
那麼來計算下兩個地址變化
block前,a指向堆中的地址0x1000ec0b0,a在棧中的地址0x16fd19f58
block後,a指向堆中的地址0x1000ec0b0,a在棧中的地址0x16fd19f58
block內,a指向堆中的地址0x1000ec0b0,a在棧中的地址0x17404e0c0
十六進制的0x16fd19f5八、0x17404e0c0轉換爲十進制數爲,617097608八、6241444032。二者相差5546832956字節,再轉換爲MB爲5289.8mb。已知 IOS中一個進程的棧區內存只有1M,Mac也只有8M(等有空找官方文檔求證下),又由於堆地址要小於棧地址,因此在block內調用變量,在不使用__block的狀況下,是在堆中新建了一個變量地址指向原變量,block做用域結束則銷燬,不影響原變量。

 

咱們都知道:Block不容許修改外部變量的值,這裏所說的外部變量的值,指的是棧中指針的內存地址。在block內調用變量,在不使用__block的狀況下,是在堆中新建了一個變量地址指向原變量,block做用域結束則銷燬,不影響原變量。工具

__block關鍵字

由前所知,經過__block修飾,block內部不只僅能夠對外部變量進行讀操做,也能夠進行寫操做了,那這是爲何呢?一樣用代碼研究二者區別學習

__block NSString *a = @"testa";
    NSLog(@"block前,a指向堆中的地址%p,a在棧中的地址%p",a,&a);
    void(^testBlock)(void) = ^(void){
        NSLog(@"block內,a指向堆中的地址%p,a在棧中的地址%p",a,&a);
    };
    testBlock();
    NSLog(@"block後,a指向堆中的地址%p,a在棧中的地址%p",a,&a);

去掉時間戳後,打印的結果是spa

block前,a指向堆中的地址0x10007c0d0,a在棧中的地址0x16fd89f58
block內,a指向堆中的地址0x10007c0d0,a在棧中的地址0x1740533a8
block後,a指向堆中的地址0x10007c0d0,a在棧中的地址0x1740533a8

 

根據內存地址變化可見,__block所起到的做用就是隻要觀察到該變量被 block 所持有,就將「外部變量」在棧中的內存地址放到了堆中。進而在block內部也能夠修改外部變量的值。原先地址是否直接拋棄不用再繼續研究設計

Block不容許修改外部變量的值,Apple這樣設計,應該是考慮到了block的特殊性,block也屬於「函數」的範疇,變量進入block,實際就是已經改變了做用域。在幾個做用域之間進行切換時,若是不加上這樣的限制,變量的可維護性將大大下降。又好比我想在block內聲明瞭一個與外部同名的變量,此時是容許呢仍是不容許呢?只有加上了這樣的限制,這樣的情景才能實現。指針

編譯器作了什麼?

通常使用的話,到這個程度已經足夠了。咱們已經知道了加上__block關鍵字以後,編譯器經過將外部變量同block一塊兒copy到了堆區,而且將「外部變量」在棧中的內存地址改成了堆中的新地址。
若是多問一個爲何?編譯器是怎麼作到這樣的呢?咱們經過clang將 OC 代碼轉換爲 C++ 文件:code

clang -rewrite-objc 源代碼文件名

轉譯的時候遇到了幾個問題:對象

#import <UIKit/UIKit.h> ** ^** 1 error generated. 

經過Objective-C編譯成C++代碼報錯文中的方式能夠轉譯,可是又出現了新的問題;
2.clang: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
這個問題沒能解決,而後換了個思路轉譯,
新代碼以下blog

//坑爹的是NSLog都不能使用,否則會報NSLog錯誤。說白了仍是工具不熟悉,爲何會出現這個狀況都不清楚。有機會再看吧
int main() {
        int  a = 1;
        void(^testBlock)(void) = ^(void){
            
        };
        testBlock();
    
        __block int b = 2;
        void(^testBlockb)(void) = ^(void){
            b = 3;
        };
        testBlockb();
        return 0;
}

轉譯後代碼以下生命週期

int main() {
        int a = 1;

        void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);





    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};

        void(*testBlockb)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_b_0 *)&b, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlockb)->FuncPtr)((__block_impl *)testBlockb);

        return 0;
}

代碼變的很長很長,咱們的目的是研究__block加上去以後編譯器的操做,精簡下就是

//加__block前的聲明變量是這樣的
int a = 1;
//加__block後的聲明變量是這樣的
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {
  (void*)0,
  (__Block_byref_b_0 *)&b, 
  0,
   sizeof(__Block_byref_b_0), 
  2};

能夠看到增長了__block修飾以後,編譯器作了很多工做,修飾詞中有__Block_byref_b_0重複出現,這是一個與block同樣的結構體類型的自動變量實例!!!!
此時咱們在block內部訪問val變量則須要經過一個叫__forwarding的成員變量來間接訪問val變量。
講__forwarding以前,須要先討論一下block的存儲域及copy操做。

1.Block的存儲域及copy操做

前面提到,block內部的做用域是在堆上的,而且調用變量時會將變量copy到堆上,那麼block自己是存儲在堆上仍是棧上呢?
咱們先來看看一個由C/C++/OBJC編譯的程序佔用內存分佈的結構:




實際上,block有三種類型,

  • 全局塊(_NSConcreteGlobalBlock)
  • 棧塊(_NSConcreteStackBlock)
  • 堆塊(_NSConcreteMallocBlock)
    這三種block各自的存儲域以下圖:




  • 簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。(這聽起來彷佛與文章上半部分的說明有衝突呢?其實並否則)

那麼,咱們如何判斷這個block的存儲位置呢?
(1)Block不訪問外界變量(包括棧中和堆中的變量)
Block 既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此。此時爲全局塊。(_NSConcreteGlobalBlock)
(2)Block訪問外界變量
MRC 環境下:訪問外界變量的 Block 默認存儲棧中。
ARC 環境下:訪問外界變量的 Block 默認存儲在堆中(實際是放在棧區,而後ARC狀況下自動又拷貝到堆區),自動釋放。

ARC下,訪問外界變量的 Block爲何要自動從棧區拷貝到堆區呢?
棧上的Block,若是其所屬的變量做用域結束,該Block就被廢棄,如同通常的自動變量。固然,Block中的__block變量也同時被廢棄。以下圖:




爲了解決棧塊在其變量做用域結束以後被廢棄(釋放)的問題,咱們須要把Block複製到堆中,延長其生命週期。開啓ARC時,大多數狀況下編譯器會恰當地進行判斷是否有須要將Block從棧複製到堆,若是有,自動生成將Block從棧上覆制到堆上的代碼。Block的複製操做執行的是copy實例方法。Block只要調用了copy方法,棧塊就會變成堆塊。
以下圖:



在非ARC狀況下則須要開發者調用copy方法手動複製,因爲開發中幾乎都是ARC模式,因此手動複製內容再也不過多研究。
將Block從棧上覆制到堆上至關消耗CPU,因此當Block設置在棧上也可以使用時,就不要複製了,由於此時的複製只是在浪費CPU資源。
Block的複製操做執行的是copy實例方法。不一樣類型的Block使用copy方法的效果以下表:


根據表得知,Block在堆中copy會形成引用計數增長,這與其餘Objective-C對象是同樣的。雖然Block在棧中也是以對象的身份存在,可是棧塊沒有引用計數,由於不須要,咱們都知道棧區的內存由編譯器自動分配釋放。
無論Block存儲域在何處,用copy方法複製都不會引發任何問題。在不肯定時調用copy方法便可。

在ARC有效時,屢次調用copy方法徹底沒有問題:

blk = [[[[blk copy] copy] copy] copy];
// 通過屢次複製,變量blk仍然持有Block的強引用,該Block不會被廢棄。
2.__block變量與__forwarding

在copy操做以後,既然__block變量也被copy到堆上去了, 那麼訪問該變量是訪問棧上的仍是堆上的呢?__forwarding 終於要閃亮登場了,以下圖:

經過__forwarding, 不管是在block中仍是 block外訪問__block變量, 也無論該變量在棧上或堆上, 都能順利地訪問同一個__block變量。
值得注意的是,在ARC下,使用 __block 也有可能帶來的循環引用,

本文非原創,僅用來學習總結記錄用,參考文章:
1.iOS中__block 關鍵字的底層實現原理
2.iOS Block詳解
3.Objective-C中的Block

相關文章
相關標籤/搜索