1. 程式人生 > >iOS中__block 關鍵字的底層實現原理

iOS中__block 關鍵字的底層實現原理

《iOS面試題集錦(附答案)》 中有這樣一道題目:
在block內如何修改block外部變數?(38題)答案如下:

預設情況下,在block中訪問的外部變數是複製過去的,即:寫操作不對原變數生效。但是你可以加上 __block 來讓其寫操作生效,示例程式碼如下:

123456 __block inta=0;void(^foo)(void)=^{a=1;};foo();//這裡,a的值被修改為1

這是 微博@唐巧_boy的《iOS開發進階》中的第11.2.3章節中的描述。你同樣可以在面試中這樣回答,但你並沒有答到“點子上”。真正的原因,並沒有書這本書裡寫的這麼“神奇”,而且這種說法也有點牽強。面試官肯定會追問“為什麼寫操作就生效了?”真正的原因是這樣的:

我們都知道:Block不允許修改外部變數的值,這裡所說的外部變數的值,指的是棧中指標的記憶體地址。__block 所起到的作用就是隻要觀察到該變數被 block 所持有,就將“外部變數”在棧中的記憶體地址放到了堆中。進而在block內部也可以修改外部變數的值。

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

我們可以列印下記憶體地址來進行驗證:

12345678 __block inta=0;NSLog(@"定義前:%p",&a);//棧區void(^foo)(void)=^{a=1;NSLog(@"block內部:%p",&a);//堆區};NSLog(@"定義後:%p",&a);//堆區foo();
123 2016-05-1702:03:33.559LeanCloudChatKit-iOS[1505:713679]定義前:0x16fda86f82016-05-1702:03:33.559LeanCloudChatKit-iOS[1505:713679]定義後:0x155b22fc82016-05-1702:03:33.559LeanCloudChatKit-iOS[1505:713679]block內部:0x155b22fc8

“定義後”和“block內部”兩者的記憶體地址是一樣的,我們都知道 block 內部的變數會被 copy 到堆區,“block內部”列印的是堆地址,因而也就可以知道,“定義後”列印的也是堆的地址。

那麼如何證明“block內部”列印的是堆地址?

把三個16進位制的記憶體地址轉成10進位制就是:

  1. 定義後前:6171559672
  2. block內部:5732708296
  3. 定義後後:5732708296

中間相差438851376個位元組,也就是 418.5M 的空間,因為堆地址要小於棧地址,又因為iOS中一個程序的棧區記憶體只有1M,Mac也只有8M,顯然a已經是在堆區了。

這也證實了:a 在定義前是棧區,但只要進入了 block 區域,就變成了堆區。這才是 __block 關鍵字的真正作用。

理解到這是因為堆疊地址的變更,而非所謂的“寫操作生效”,這一點至關重要,要不然你如何解釋下面這個現象:

以下程式碼編譯可以通過,並且在block中成功將a的從Tom修改為Jerry。

123456789101112 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在棧區

這裡的a已經由基本資料型別,變成了物件型別。物件型別,block會對物件型別的指標進行copy,copy到堆中,但並不會改變該指標所指向的堆中的地址,所以在上面的示例程式碼中,block體內修改的實際是a指向的堆中的內容。

但如果我們嘗試像上面圖片中的65行那樣做,結果會編譯不通過,那是因為此時你在修改的就不是堆中的內容,而是棧中的內容。

上文已經說過:Block不允許修改外部變數的值,這裡所說的外部變數的值,指的是棧中指標的記憶體地址。