在 《iOS面試題集錦(附答案)》 中有這樣一道題目:
在block內如何修改block外部變量?(38題)答案如下:
默認情況下,在block中訪問的外部變量是復制過去的,即:寫操作不對原變量生效。但是你可以加上 __block 來讓其寫操作生效,示例代碼如下:
__block int a = 0;
void (^foo)(void) = ^{
a = 1;
};
foo();
//這里,a的值被修改為1
這是 微博@唐巧_boy的《iOS開發進階》中的第11.2.3章節中的描述。你同樣可以在面試中這樣回答,但你並沒有答到“點子上”。真正的原因,並沒有書這本書里寫的這么“神奇”,而且這種說法也有點牽強。面試官肯定會追問“為什么寫操作就生效了?”真正的原因是這樣的:
我們都知道:Block不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內存地址。__block 所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中。進而在block內部也可以修改外部變量的值。
Block不允許修改外部變量的值Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函數”的范疇,變量進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變量的可維護性將大大降低。又比如我想在block內聲明了一個與外部同名的變量,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。
我們可以打印下內存地址來進行驗證:
__block int a = 0;
NSLog(@"定義前:%p", &a); //棧區
void (^foo)(void) = ^{
a = 1;
NSLog(@"block內部:%p", &a); //堆區
};
NSLog(@"定義后:%p", &a); //堆區
foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block內部: 0x155b22fc8
“定義后”和“block內部”兩者的內存地址是一樣的,我們都知道 block 內部的變量會被 copy 到堆區,“block內部”打印的是堆地址,因而也就可以知道,“定義后”打印的也是堆的地址。
那么如何證明“block內部”打印的是堆地址?
把三個16進制的內存地址轉成10進制就是:
-
定義后前:6171559672
-
block內部:5732708296
-
定義后后:5732708296
中間相差438851376個字節,也就是 418.5M 的空間,因為堆地址要小於棧地址,又因為iOS中一個進程的棧區內存只有1M,Mac也只有8M,顯然a已經是在堆區了。
這也證實了:a 在定義前是棧區,但只要進入了 block 區域,就變成了堆區。這才是 __block 關鍵字的真正作用。
理解到這是因為堆棧地址的變更,而非所謂的“寫操作生效”,這一點至關重要,要不然你如何解釋下面這個現象:
以下代碼編譯可以通過,並且在block中成功將a的從Tom修改為Jerry。
NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
NSLog(@"\n 定以前:------------------------------------\n\
a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區
void (^foo)(void) = ^{
a.string = @"Jerry";
NSLog(@"\n block內部:------------------------------------\n\
a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區
a = [NSMutableString stringWithString:@"William"];
};
foo();
NSLog(@"\n 定以后:------------------------------------\n\
a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a);
這里的a已經由基本數據類型,變成了對象類型。對象類型,block會對對象類型的指針進行copy,copy到堆中,但並不會改變該指針所指向的堆中的地址,所以在上面的示例代碼中,block體內修改的實際是a指向的堆中的內容。
但如果我們嘗試像上面圖片中的65行那樣做,結果會編譯不通過,那是因為此時你在修改的就不是堆中的內容,而是棧中的內容。
上文已經說過:Block不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內存地址。