iOS中 __block關(guān)鍵字的實現(xiàn)原理

在block內(nèi)如何修改block外部變量?

1.默認情況下,在block中訪問的外部變量是復制過去的,即:\color{red}{寫操作不對原變量生效}.我們可以打印下來驗證:

    int a = 6;
    
    NSLog(@"修改前:%d",a);
    
    void (^car)(void) = ^ {
        
        NSLog(@"block中:%d",a);
    };
    
    a = 10;
    NSLog(@"修改后:%d",a);
    car();

打印如下:

2020-04-02 17:00:07.714371+0800 test[51434:15204430] 修改前:6
2020-04-02 17:00:07.714561+0800 test[51434:15204430] block中:6
2020-04-02 17:00:07.714674+0800 test[51434:15204430] 修改后:10

結(jié)論:寫操作不對原變量生效:這里是把棧中的a,復制了一份到堆中(只是一個簡單的值傳遞,堆中的暫且稱之為b,下面也這樣稱呼),兩個內(nèi)存空間,所以修改棧中的a,不影響堆中的b.

2.如果我們想要修改外部變量的值,我們可以加上__block來進行操作,示例:

    __block int a = 6;
    void (^car)(void) = ^{
        a = 8;
    };
    car();
    NSLog(@"%d",a);
2020-04-02 17:05:19.450439+0800 test[51562:15238586] 8

到這里,我們就要看下,為什么加上__block就生效了,它到底做了什么?

我們都知道:Block不允許修改外部變量的值,這里所說的外部變量的值,指的是\color{red}{棧中指針的內(nèi)存地址}.__block 所起到的作用就是只要觀察到該變量被 block 所持有,就會將a包裝成一個結(jié)構(gòu)體__Block_byref_a_0,初始化a=0,然后將該結(jié)構(gòu)體的指針傳遞到了堆中.因而在block內(nèi)部也可以修改外部變量的值.a在結(jié)構(gòu)體里變成了一個屬性(該結(jié)構(gòu)體持有局部的原始變量),也有__forwarding這個指針. 這時候棧里面結(jié)構(gòu)體的__forwarding 指向了堆中的結(jié)構(gòu)體,堆中結(jié)構(gòu)體的__forwarding 指向了自己,這就保證了無論是棧中的還是堆中的,都是訪問的同一個值a,如下圖所示.

__block結(jié)構(gòu)體

Block不允許修改外部變量的值這樣的設(shè)計,應(yīng)該是考慮到了block的特殊性,block也屬于“函數(shù)”的范疇,變量進入block,實際就是已經(jīng)改變了作用域.在幾個作用域之間進行切換時,如果不加上這樣的限制,變量的可維護性將大大降低.又比如我想在block內(nèi)聲明了一個與外部同名的變量,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現(xiàn).

我們可以打印下內(nèi)存地址來進行驗證:

    __block int a = 0;
    NSLog(@"定義前:%p", &a);         //棧區(qū)
    void (^car)(void) = ^{
      a = 1;
      NSLog(@"block內(nèi)部:%p", &a);    //堆區(qū)
    };
    NSLog(@"定義后:%p", &a);         //堆區(qū)
    car();
2020-04-02 17:10:54.586104+0800 test[51715:15274844] 定義前:0x7ffee5711148
2020-04-02 17:10:54.586330+0800 test[51715:15274844] 定義后:0x600000778018
2020-04-02 17:10:54.586449+0800 test[51715:15274844] block內(nèi)部:0x600000778018

可以看到,定義后block內(nèi)部內(nèi)存地址是一樣的,我們都知道 block 內(nèi)部的變量會被放到堆區(qū),“block內(nèi)部”打印的是堆地址,因而也就可以知道,“定義后”打印的也是堆的地址.
結(jié)論:a在定義前是在棧區(qū)(\color{red}{基本數(shù)據(jù)類型在棧區(qū)(先不不考慮全局等那些,重要的是在block中放到了堆區(qū))}),進入block區(qū)域后,變成了堆區(qū).這才是__block關(guān)鍵字的作用

再看下面這個例子

    NSMutableString *a = [NSMutableString stringWithString:@"Car"];
    NSLog(@"----定義前a指向的地址:%p;a的指針地址:%p----\n",a,&a);
    //&a在棧區(qū),a在堆區(qū)
    void (^car)(void) = ^{
        a.string = @"Bus";
        NSLog(@"----block內(nèi)部:a指向的地址:%p;a的指針地址:%p----\n", a, &a);
        //&a在棧區(qū),&b(&a的復制)在堆區(qū),a在堆區(qū):注意: 這時候棧區(qū)a和堆區(qū)的b指向的是同一個內(nèi)存空間
        //a = [NSMutableString stringWithString:@"Bar"];
    };
    car();
    NSLog(@"----定以后:a指向的地址:%p;a的指針地址:%p----\n", a, &a);
    //&a在棧區(qū),a在堆區(qū)
2020-04-02 17:24:23.086810+0800 test[52061:15362163] ----定義前a指向的地址:0x60000322c5d0;a的指針地址:0x7ffee8f3f148----

2020-04-02 17:24:23.087005+0800 test[52061:15362163] ----block內(nèi)部:a指向的地址:0x60000322c5d0;a的指針地址:0x600003212720----

2020-04-02 17:24:23.087172+0800 test[52061:15362163] ----定以后:a指向的地址:0x60000322c5d0;a的指針地址:0x7ffee8f3f148----

可以看到,a指向的地址始終沒變,只有a的指針地址:&a發(fā)生了變化,這也驗證了上面所說的這里所說的外部變量的值,指的是棧中指針的內(nèi)存地址.

這里由基本數(shù)據(jù)類型變成了對象類型(對象類型,在開始的時候,棧上存儲&a這個指針變量,它所指的對象在堆中),經(jīng)過block作用域的時候,copy棧中的指針到了堆中(實際上是把a變成了block結(jié)構(gòu)體的一個屬性,只是指針復制,指向的是同一份內(nèi)存空間),進行a.string,實際上是對堆中的對象進行操作的,是可以的
所以在上面的示例代碼中,block體內(nèi)修改的實際是a指向的堆中的內(nèi)容。

但如果我們嘗試像上面圖片中的a = [NSMutableString stringWithString:@"Bar"];這樣做,結(jié)果會編譯不通過,因為這里并不是簡單的指針引用(淺復制),而是一種(深復制)方式(括號上下分別對于基本數(shù)據(jù)類型和對象進行描述),而且不會改變該指針指向的內(nèi)容(依然指向了堆).所以在block內(nèi)部進行修改,無論是值修改,還是指針修改,對于棧中的自然沒有任何的影響.蘋果也不允許這樣做.簡單的理解為readonly.

注意:Block不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內(nèi)存地址。

參考下面這兩篇,講的挺詳細的
Block深層次總結(jié)和一些經(jīng)典的面試題
一篇文章剖析block底層源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容