默認狀況下,在block中訪問外部變量是經過複製一個變量來操做的,既能夠讀,可是寫操做不對原變量生效,下面經過代碼來舉證函數
NSString *a = @"testa"; NSLog(@"block前,a在堆中的地址%p,a在棧中的地址%p",a,&a); void(^testBlock)(void) = ^(void){ NSLog(@"block內,a在堆中的地址%p,a在棧中的地址%p",a,&a); }; NSLog(@"block後,a在堆中的地址%p,a在棧中的地址%p",a,&a); testBlock();
咱們都知道:Block不容許修改外部變量的值,這裏所說的外部變量的值,指的是棧中指針的內存地址。在block內調用變量,在不使用__block的狀況下,是在堆中新建了一個變量地址指向原變量,block做用域結束則銷燬,不影響原變量。工具
由前所知,經過__block修飾,block內部不只僅能夠對外部變量進行讀操做,也能夠進行寫操做了,那這是爲何呢?一樣用代碼研究二者區別學習
__block NSString *a = @"testa"; NSLog(@"block前,a指向堆中的地址%p,a在棧中的地址%p",a,&a); void(^testBlock)(void) = ^(void){ NSLog(@"block內,a指向堆中的地址%p,a在棧中的地址%p",a,&a); }; testBlock(); NSLog(@"block後,a指向堆中的地址%p,a在棧中的地址%p",a,&a);
去掉時間戳後,打印的結果是spa
block前,a指向堆中的地址0x10007c0d0,a在棧中的地址0x16fd89f58
block內,a指向堆中的地址0x10007c0d0,a在棧中的地址0x1740533a8
block後,a指向堆中的地址0x10007c0d0,a在棧中的地址0x1740533a8
根據內存地址變化可見,
__block
所起到的做用就是隻要觀察到該變量被 block 所持有,就將「外部變量」在棧中的內存地址放到了堆中。進而在block內部也能夠修改外部變量的值。原先地址是否直接拋棄不用再繼續研究設計
Block不容許修改外部變量的值,Apple這樣設計,應該是考慮到了block的特殊性,block也屬於「函數」的範疇,變量進入block,實際就是已經改變了做用域。在幾個做用域之間進行切換時,若是不加上這樣的限制,變量的可維護性將大大下降。又好比我想在block內聲明瞭一個與外部同名的變量,此時是容許呢仍是不容許呢?只有加上了這樣的限制,這樣的情景才能實現。指針
通常使用的話,到這個程度已經足夠了。咱們已經知道了加上__block
關鍵字以後,編譯器經過將外部變量同block一塊兒copy到了堆區,而且將「外部變量」在棧中的內存地址改成了堆中的新地址。
若是多問一個爲何?編譯器是怎麼作到這樣的呢?咱們經過clang
將 OC 代碼轉換爲 C++ 文件:code
clang -rewrite-objc 源代碼文件名
轉譯的時候遇到了幾個問題:對象
#import <UIKit/UIKit.h> ** ^** 1 error generated.
經過Objective-C編譯成C++代碼報錯文中的方式能夠轉譯,可是又出現了新的問題;
2.clang: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
這個問題沒能解決,而後換了個思路轉譯,
新代碼以下blog
//坑爹的是NSLog都不能使用,否則會報NSLog錯誤。說白了仍是工具不熟悉,爲何會出現這個狀況都不清楚。有機會再看吧 int main() { int a = 1; void(^testBlock)(void) = ^(void){ }; testBlock(); __block int b = 2; void(^testBlockb)(void) = ^(void){ b = 3; }; testBlockb(); return 0; }
轉譯後代碼以下生命週期
int main() { int a = 1; void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock); __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2}; void(*testBlockb)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_b_0 *)&b, 570425344)); ((void (*)(__block_impl *))((__block_impl *)testBlockb)->FuncPtr)((__block_impl *)testBlockb); return 0; }
代碼變的很長很長,咱們的目的是研究__block加上去以後編譯器的操做,精簡下就是
//加__block前的聲明變量是這樣的 int a = 1; //加__block後的聲明變量是這樣的 __attribute__((__blocks__(byref))) __Block_byref_b_0 b = { (void*)0, (__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};
能夠看到增長了__block
修飾以後,編譯器作了很多工做,修飾詞中有__Block_byref_b_0
重複出現,這是一個與block同樣的結構體類型的自動變量實例!!!!
此時咱們在block內部訪問val變量則須要經過一個叫__forwarding的成員變量來間接訪問val變量。
講__forwarding以前,須要先討論一下block的存儲域及copy操做。
前面提到,block內部的做用域是在堆上的,而且調用變量時會將變量copy到堆上,那麼block自己是存儲在堆上仍是棧上呢?
咱們先來看看一個由C/C++/OBJC編譯的程序佔用內存分佈的結構:
實際上,block有三種類型,
堆塊(_NSConcreteMallocBlock)
這三種block各自的存儲域以下圖:
簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。(這聽起來彷佛與文章上半部分的說明有衝突呢?其實並否則)
那麼,咱們如何判斷這個block的存儲位置呢?
(1)Block不訪問外界變量(包括棧中和堆中的變量)
Block 既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此。此時爲全局塊。(_NSConcreteGlobalBlock)
(2)Block訪問外界變量
MRC 環境下:訪問外界變量的 Block 默認存儲棧中。
ARC 環境下:訪問外界變量的 Block 默認存儲在堆中(實際是放在棧區,而後ARC狀況下自動又拷貝到堆區),自動釋放。
ARC下,訪問外界變量的 Block爲何要自動從棧區拷貝到堆區呢?
棧上的Block,若是其所屬的變量做用域結束,該Block就被廢棄,如同通常的自動變量。固然,Block中的__block變量也同時被廢棄。以下圖:
根據表得知,Block在堆中copy會形成引用計數增長,這與其餘Objective-C對象是同樣的。雖然Block在棧中也是以對象的身份存在,可是棧塊沒有引用計數,由於不須要,咱們都知道棧區的內存由編譯器自動分配釋放。
無論Block存儲域在何處,用copy方法複製都不會引發任何問題。在不肯定時調用copy方法便可。
在ARC有效時,屢次調用copy方法徹底沒有問題:
blk = [[[[blk copy] copy] copy] copy]; // 通過屢次複製,變量blk仍然持有Block的強引用,該Block不會被廢棄。
在copy操做以後,既然__block變量也被copy到堆上去了, 那麼訪問該變量是訪問棧上的仍是堆上的呢?__forwarding 終於要閃亮登場了,以下圖:
經過__forwarding, 不管是在block中仍是 block外訪問__block變量, 也無論該變量在棧上或堆上, 都能順利地訪問同一個__block變量。
值得注意的是,在ARC下,使用 __block 也有可能帶來的循環引用,
本文非原創,僅用來學習總結記錄用,參考文章:
1.iOS中__block 關鍵字的底層實現原理
2.iOS Block詳解
3.Objective-C中的Block