深入研究Block捕獲外部變量和__block實現(xiàn)原理

前言

很早開始使用Block的時候只記得以下簡單的用法:block中能夠直接訪問和修改全局變量励两; 但是, 只能訪問局部變量, 不能修改局部變量; 如果想在block 中修改局部變量需要在局部變量的定義之前加上__block修飾。那么今天就來深入探究一下废赞。

Blocks是C語言的擴充功能湾戳,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這個新功能“Blocks”贤旷。從那開始,Block就出現(xiàn)在iOS和Mac系統(tǒng)各個API中砾脑,并被大家廣泛使用遮晚。一句話來形容Blocks,帶有自動變量(局部變量)的匿名函數(shù)拦止。

Block在OC中的實現(xiàn)如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

從結構圖中很容易看到isa县遣,所以OC處理Block是按照對象來處理的。在iOS中汹族,isa常見的就是_NSConcreteStackBlock萧求,_NSConcreteMallocBlock,_NSConcreteGlobalBlock這3種(另外只在GC環(huán)境下還有3種使用的_NSConcreteFinalizingBlock顶瞒,_NSConcreteAutoBlock夸政,_NSConcreteWeakBlockVariable,本文暫不談論這3種榴徐,有興趣的看看官方文檔)

1守问、block的內部對于外部及內部變量的處理

我們先根據(jù)這4種類型
自動變量
靜態(tài)變量
靜態(tài)全局變量
全局變量
寫出Block測試代碼。

#import <Foundation/Foundation.h>

int global_i = 1;

static int static_global_j = 2;

int main(int argc, const char * argv[]) {

    static int static_k = 3;
    int val = 4;

    void (^myBlock)(void) = ^{
        global_i ++;
        static_global_j ++;
        static_k ++;
        NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    };

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);

    myBlock();

    return 0;
}

運行結果

Block 外  global_i = 2,static_global_j = 3,static_k = 4,val = 5
Block 中  global_i = 3,static_global_j = 4,static_k = 5,val = 4

由此產(chǎn)生兩個問題:
1.為什么在Block里面不加__bolck不允許更改變量坑资?
2.為什么自動變量的值沒有增加耗帕,而其他幾個變量的值是增加的?自動變量是什么狀態(tài)下被block捕獲進去的袱贮?

使用clang分析源碼

int global_i = 1;

static int static_global_j = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_k;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


int main(int argc, const char * argv[]) {

    static int static_k = 3;
    int val = 4;

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

首先全局變量global_i和靜態(tài)全局變量static_global_j的值增加仿便,以及它們被Block捕獲進去,這一點很好理解,因為是全局的嗽仪,作用域很廣荒勇,所以Block捕獲了它們進去之后,在Block里面進行++操作闻坚,Block結束之后沽翔,它們的值依舊可以得以保存下來。

接下來仔細看看自動變量和靜態(tài)變量的問題窿凤。 在__main_block_impl_0中仅偎,可以看到靜態(tài)變量static_k和自動變量val,被Block從外面捕獲進來卷玉,成為__main_block_impl_0這個結構體的成員變量了。

接著看構造函數(shù)喷市,

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)

這個構造函數(shù)中相种,自動變量和靜態(tài)變量被捕獲為成員變量追加到了構造函數(shù)中。

main里面的myBlock閉包中的__main_block_impl_0結構體品姓,初始化如下

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));


impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_impl_0; 
Desc = &__main_block_desc_0_DATA;
*_static_k = 4寝并;
val = 4;

到此,__main_block_impl_0結構體就是這樣把自動變量捕獲進來的腹备。也就是說雹锣,在執(zhí)行Block語法的時候鸭限,Block語法表達式所使用的自動變量的值是被保存進了Block的結構體實例中,也就是Block自身中。

這里值得說明的一點是饲齐,如果Block外面還有很多自動變量,靜態(tài)變量嫂沉,等等蹬屹,這些變量在Block里面并不會被使用到。那么這些變量并不會被Block捕獲進來卸留,也就是說并不會在構造函數(shù)里面?zhèn)魅胨鼈兊闹怠?/p>

Block捕獲外部變量僅僅只捕獲Block閉包里面會用到的值走越,其他用不到的值,它并不會去捕獲耻瑟。

再研究一下源碼旨指,我們注意到__main_block_func_0這個函數(shù)的實現(xiàn)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

*我們可以發(fā)現(xiàn),系統(tǒng)自動給我們加上的注釋喳整,bound by copy谆构,自動變量val雖然被捕獲進來了,但是是用 __cself->val來訪問的框都。Block僅僅捕獲了val的值低淡,并沒有捕獲val的內存地址。所以在__main_block_func_0這個函數(shù)中即使我們重寫這個自動變量val的值,依舊沒法去改變Block外面自動變量val的值蔗蹋。

OC可能是基于這一點何荚,在編譯的層面就防止開發(fā)者可能犯的錯誤,因為自動變量沒法在Block中改變外部變量的值猪杭,所以編譯過程中就報編譯錯誤餐塘。

小結一下: 到此為止,上面提出的第二個問題就解開答案了皂吮。自動變量是以值傳遞方式傳遞到Block的構造函數(shù)里面去的戒傻。Block只捕獲Block中會用到的變量。由于只捕獲了自動變量的值蜂筹,并內存地址需纳,所以Block內部不能改變自動變量的值。Block捕獲的外部變量可以改變值的是靜態(tài)變量艺挪,靜態(tài)全局變量不翩,全局變量。上面例子也都證明過了麻裳。

回到上面的例子上面來口蝠,4種變量里面只有靜態(tài)變量,靜態(tài)全局變量津坑,全局變量這3種是可以在Block里面被改變值的妙蔗。仔細觀看源碼,我們能看出這3個變量可以改變值的原因疆瑰。

1眉反、靜態(tài)全局變量,全局變量由于作用域的原因穆役,于是可以直接在Block里面被改變禁漓。他們也都存儲在全局區(qū)。
2孵睬、靜態(tài)變量傳遞給Block是內存地址值播歼,所以能在Block里面直接改變值。

總結一下在Block中改變變量值有2種方式掰读,一是傳遞內存地址指針到Block中秘狞,二是改變存儲區(qū)方式(__block)。

2蹈集、Block中__block實現(xiàn)原理

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {

   __block int i = 0;

   void (^myBlock)(void) = ^{
       i ++;
       NSLog(@"%d",i);
   };

   myBlock();

   return 0;
}

把上述代碼用clang轉換成源碼烁试。

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        (i->__forwarding->i) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

??

從源碼我們能發(fā)現(xiàn),帶有 __block的變量也被轉化成了一個結構體__Block_byref_i_0,這個結構體有5個成員變量拢肆。第一個是isa指針减响,第二個是指向自身類型的__forwarding指針靖诗,第三個是一個標記flag,第四個是它的大小支示,第五個是變量值刊橘,名字和變量名同名。

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

源碼中是這樣初始化的颂鸿。__forwarding指針初始化傳遞的是自己的地址促绵。然而這里__forwarding指針真的永遠指向自己么?我們來做一個實驗嘴纺。

//以下代碼在MRC中運行
    __block int i = 0;
    NSLog(@"%p",&i);

    void (^myBlock)(void) = [^{
        i ++;
        NSLog(@"這是Block 里面%p",&i);
    }copy];

我們把Block拷貝到了堆上败晴,這個時候打印出來的2個i變量的地址就不同了。

0x7fff5fbff818
<__NSMallocBlock__: 0x100203cc0>
這是Block 里面 0x1002038a8

地址不同就可以很明顯的說明__forwarding指針并沒有指向之前的自己了栽渴。那__forwarding指針現(xiàn)在指向到哪里了呢尖坤?

Block里面的__block的地址和Block的地址就相差1052。我們可以很大膽的猜想闲擦,__block現(xiàn)在也在堆上了慢味。

出現(xiàn)這個不同的原因在于這里把Block拷貝到了堆上。

由第二章里面詳細分析的佛致,堆上的Block會持有對象贮缕。我們把Block通過copy到了堆上辙谜,堆上也會重新復制一份Block俺榆,并且該Block也會繼續(xù)持有該__block。當Block釋放的時候装哆,__block沒有被任何對象引用罐脊,也會被釋放銷毀。

__forwarding指針這里的作用就是針對堆的Block蜕琴,把原來__forwarding指針指向自己萍桌,換成指向_NSConcreteMallocBlock上復制之后的__block自己。然后堆上的變量的__forwarding再指向自己凌简。這樣不管__block怎么復制到堆上上炎,還是在棧上,都可以通過(i->__forwarding->i)來訪問到變量值雏搂。

特別說明:ARC環(huán)境下藕施,一旦Block賦值就會觸發(fā)copy,__block就會copy到堆上凸郑,Block也是__NSMallocBlock裳食。ARC環(huán)境下也是存在__NSStackBlock的時候,這種情況下芙沥,__block就在棧上诲祸。 MRC環(huán)境下浊吏,只有copy,__block才會被復制到堆上救氯,否則找田,__block一直都在棧上,block也只是NSStackBlock径密,這個時候\forwarding指針就只指向自己了午阵。

最后

關于Block捕獲外部變量有很多用途,用途也很廣享扔,只有弄清了捕獲變量和持有的變量的概念以后底桂,之后才能清楚的解決Block循環(huán)引用的問題。

再次回到文章開頭惧眠,5種變量籽懦,自動變量,函數(shù)參數(shù) 氛魁,靜態(tài)變量暮顺,靜態(tài)全局變量,全局變量秀存,如果嚴格的來說捶码,捕獲是必須在Block結構體__main_block_impl_0里面有成員變量的話,Block能捕獲的變量就只有帶有自動變量和靜態(tài)變量了或链。捕獲進Block的對象會被Block持有惫恼。
帶__block的自動變量 和 靜態(tài)變量 就是直接地址訪問。所以在Block里面可以直接改變變量的值澳盐。

而剩下的靜態(tài)全局變量祈纯,全局變量,函數(shù)參數(shù)叼耙,也是可以在直接在Block中改變變量值的腕窥,但是他們并沒有變成Block結構體__main_block_impl_0的成員變量,因為他們的作用域大筛婉,所以可以直接更改他們的值簇爆。

值得注意的是,靜態(tài)全局變量爽撒,全局變量入蛆,函數(shù)參數(shù)他們并不會被Block持有,也就是說不會增加retainCount值匆浙。

參考鏈接:
https://juejin.im/post/57ccab0ba22b9d006ba26de1

http://www.reibang.com/p/8995a60384fd

http://www.reibang.com/p/a19f6dbb14da

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末安寺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子首尼,更是在濱河造成了極大的恐慌挑庶,老刑警劉巖言秸,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異迎捺,居然都是意外死亡举畸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門凳枝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抄沮,“玉大人,你說我怎么就攤上這事岖瑰∨崖颍” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵蹋订,是天一觀的道長率挣。 經(jīng)常有香客問我,道長露戒,這世上最難降的妖魔是什么椒功? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮智什,結果婚禮上动漾,老公的妹妹穿的比我還像新娘。我一直安慰自己荠锭,他們只是感情好旱眯,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著节沦,像睡著了一般键思。 火紅的嫁衣襯著肌膚如雪础爬。 梳的紋絲不亂的頭發(fā)上甫贯,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音看蚜,去河邊找鬼叫搁。 笑死,一個胖子當著我的面吹牛供炎,可吹牛的內容都是我干的渴逻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼音诫,長吁一口氣:“原來是場噩夢啊……” “哼惨奕!你這毒婦竟也來了?” 一聲冷哼從身側響起竭钝,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤梨撞,失蹤者是張志新(化名)和其女友劉穎雹洗,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卧波,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡时肿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了港粱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片螃成。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖查坪,靈堂內的尸體忽然破棺而出寸宏,到底是詐尸還是另有隱情,我是刑警寧澤偿曙,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站遥昧,受9級特大地震影響覆醇,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一鞋仍、第九天 我趴在偏房一處隱蔽的房頂上張望常摧。 院中可真熱鬧,春花似錦威创、人聲如沸落午。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溃斋。三九已至,卻和暖如春吸申,著一層夾襖步出監(jiān)牢的瞬間梗劫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工截碴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梳侨,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓日丹,卻偏偏與公主長得像走哺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哲虾,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內容