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


在 《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進制就是:

 

  1. 定義后前:6171559672

  2. block內部:5732708296

  3. 定義后后: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不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內存地址。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM