在block內(nèi)如何修改block外部變量?
1.默認情況下,在block中訪問的外部變量是復制過去的,即:.我們可以打印下來驗證:
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不允許修改外部變量的值,這里所說的外部變量的值,指的是
.
__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ū)(),進入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底層源碼