iOS block本質(zhì)詳解(一)

平時(shí)的開發(fā)中基本每天都要使用到block,其實(shí)對于block的底層實(shí)現(xiàn)并不是很清楚,今天主要寫下block的本質(zhì)处嫌。
主要分為以下幾個(gè)方面

* block的底層實(shí)現(xiàn)
* 捕獲變量
* block類型

一. block的底層實(shí)現(xiàn)

代碼如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };
        block(10, 10);
    }
    return 0;
}

為了探索Block的底層結(jié)構(gòu)师幕,我們將main.m文件轉(zhuǎn)化為C++的源碼振劳、我們打開命令行侠畔。cd到包含main.m文件的文件夾泣洞,然后輸入:clang -rewrite-objc main.m森书,這個(gè)時(shí)候在該文件夾的目錄下會生成main.cpp文件使兔。找到main函數(shù)

main函數(shù)編譯前后對比.png

//定義block __main_block_impl_0實(shí)際是調(diào)用的結(jié)構(gòu)體的構(gòu)造函數(shù)建钥。將__main_block_func_0函數(shù)指針,和__main_block_desc_0_DATA描述傳遞給結(jié)構(gòu)體虐沥,block地址->__main_block_impl_0結(jié)構(gòu)體地址
 void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// block的調(diào)用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);

這兩行代碼非常長熊经,看起來簡直吃力,下面進(jìn)行簡化代碼

變量前面的()一般是做強(qiáng)制類型轉(zhuǎn)換的欲险,比如在調(diào)用block這一行镐依,block前面有一個(gè)()是(__block_impl *),這就是進(jìn)行了一個(gè)強(qiáng)制類型轉(zhuǎn)換天试,將其轉(zhuǎn)換為一個(gè)_block_impl類型的結(jié)構(gòu)體指針槐壳,那像這樣的強(qiáng)制類型轉(zhuǎn)換非常妨礙我們理解代碼,我們可以暫時(shí)將這些強(qiáng)制類型轉(zhuǎn)換去掉喜每,這樣可以幫助我們理解代碼务唐。
簡化后的代碼如下:

//定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//調(diào)用block
block->FuncPtr(block);
先看定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

這句代碼的意思是調(diào)用_main_block_impl_0這個(gè)函數(shù),給這個(gè)函數(shù)傳進(jìn)兩個(gè)參數(shù)_main_block_func_0&_main_block_desc_0_DATA,然后得到這個(gè)函數(shù)的返回值带兜,取函數(shù)返回值的地址枫笛,賦值給block這個(gè)指針

分析下_main_block_impl_0
struct __main_block_impl_0 {
  struct __block_impl impl; // 結(jié)構(gòu)體
  struct __main_block_desc_0* Desc; // 結(jié)構(gòu)體
  // 構(gòu)造函數(shù)
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
再來看下__block_impl結(jié)構(gòu)體
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
再來看下__main_block_desc_0結(jié)構(gòu)體
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)};

這是一個(gè)C++的結(jié)構(gòu)體刚照。而且在這個(gè)結(jié)構(gòu)體內(nèi)還包含一個(gè)函數(shù)刑巧,這個(gè)函數(shù)的函數(shù)名和結(jié)構(gòu)體名稱一致,這在C語言中是沒有的无畔,這是C++特有的啊楚。

在C++的結(jié)構(gòu)體包含的函數(shù)稱為結(jié)構(gòu)體的構(gòu)造函數(shù),它就相當(dāng)于是OC中的init方法浑彰,用來初始化結(jié)構(gòu)體恭理。OC中的init方法返回的是對象本身,C++的結(jié)構(gòu)體中的構(gòu)造方法返回的也是結(jié)構(gòu)體對象闸昨。

那么再分析下__main_block_impl_0這個(gè)結(jié)構(gòu)體思路就很清晰了蚯斯,__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);返回的就是_main_block_impl_0這個(gè)結(jié)構(gòu)體對象,然后取結(jié)構(gòu)體對象的地址賦值給block指針饵较。換句話說,block指向的就是初始化后的_main_block_impl_0結(jié)構(gòu)體對象遭赂。

_main_block_impl_0這個(gè)結(jié)構(gòu)體對象的地址從哪來的呢循诉?會不會是也是其他函數(shù)的地址呢? 我們再看一下初始化_main_block_impl_0結(jié)構(gòu)體傳進(jìn)去的參數(shù):

第一個(gè)參數(shù) _main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

           NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
           NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_1);
           NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_2);
           NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_3);
}

這個(gè)函數(shù)其實(shí)就是把我們Block中要執(zhí)行的代碼封裝到這個(gè)函數(shù)內(nèi)部了撇他。我們可以看到這個(gè)函數(shù)內(nèi)部有四行打印的代碼就是OC block那幾個(gè)NSLog茄猫。
把這個(gè)函數(shù)指針傳給_main_block_impl_0的構(gòu)造函數(shù)的第一個(gè)參數(shù)狈蚤,然后用這個(gè)函數(shù)指針去初始化_main_block_impl_0這個(gè)結(jié)構(gòu)體的第一個(gè)成員變量impl的成員變量FuncPtr。也就是說FuncPtr這個(gè)指針指向_main_block_func_0這個(gè)函數(shù)划纽。

第二個(gè)參數(shù) &_main_block_desc_0_DATA脆侮。

我們看一下這個(gè)結(jié)構(gòu):

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)};

在結(jié)構(gòu)體的構(gòu)造函數(shù)中,0賦值給了reserved勇劣,sizeof(struct __main_block_impl_0)是賦值給了Block_size靖避,可以看出這個(gè)結(jié)構(gòu)體存放的是_main_block_impl_0這個(gè)結(jié)構(gòu)體的信息。在_main_block_impl_0的構(gòu)造函數(shù)中我們可以看到比默,_main_block_desc_0這個(gè)結(jié)構(gòu)體的地址被賦值給了_main_block_impl_0的第二個(gè)成員變量Desc這個(gè)結(jié)構(gòu)體指針幻捏。也就是說Desc這個(gè)結(jié)構(gòu)體指針指向_main_block_desc_0_DATA這個(gè)結(jié)構(gòu)體。

所以第一句定義block的代碼

void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

總結(jié)起來就是:

1.創(chuàng)建一個(gè)函數(shù)_main_block_func_0,這個(gè)函數(shù)的作用就是將我們block中要執(zhí)行的代碼封裝到函數(shù)內(nèi)部命咐,方便調(diào)用篡九。
2.創(chuàng)建一個(gè)結(jié)構(gòu)體_main_block_desc_0,這個(gè)結(jié)構(gòu)體中主要包含_main_block_impl_0這個(gè)結(jié)構(gòu)體占用的存儲空間大小等信息。
3.將1中創(chuàng)建的_main_block_func_0這個(gè)函數(shù)的地址醋奠,和2中創(chuàng)建的_main_block_desc_0這個(gè)結(jié)構(gòu)體的地址傳給_main_block_impl_0的構(gòu)造函數(shù)榛臼。
4.利用_main_block_func_0初始化_main_block_impl_0結(jié)構(gòu)體的第一個(gè)成員變量impl的成員變量FuncPtr。這樣_main_bck_impl_0這個(gè)結(jié)構(gòu)體也就得到了block中那個(gè)代碼塊的地址窜司。
5.利用_mian_block_desc_0_DATA去初始化_mian_block_impl_0的第二個(gè)成員變量Desc讽坏。
以上過程總結(jié)圖示:
定義block圖示.png
block底層結(jié)構(gòu)圖示:
block底層結(jié)構(gòu)圖示.png
調(diào)用block:
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
分析
((__block_impl *)block)->FuncPtr
這行代碼是將block轉(zhuǎn)化成(__block_impl *)類型再調(diào)用FuncPtr,得到之前存儲block中的代碼例证。為什么block可以直接轉(zhuǎn)化成(__block_impl *)這種類型呢路呜?因?yàn)?code>block指向的是_main_block_impl_0這個(gè)結(jié)構(gòu)體的首地址,而_main_block_impl_0的第一個(gè)成員變量是struct __block_impl impl;织咧,所以impl_main_block_impl_0的首地址是一樣的胀葱,因此指向_main_block_impl_0的首地址的指針也就可以被強(qiáng)制轉(zhuǎn)換為指向impl的首地址的指針。
之前說過笙蒙,FuncPtr這個(gè)指針在構(gòu)造函數(shù)中是被初始化為指向_mian_block_func_0這個(gè)函數(shù)的地址抵屿。因此通過block->FuncPtr調(diào)用也就獲取了_main_block_func_0這個(gè)函數(shù)的地址,然后對_main_block_func_0進(jìn)行調(diào)用捅位,也就是執(zhí)行block中的代碼了轧葛。這中間block又被當(dāng)做參數(shù)傳進(jìn)了_main_block_func_0這個(gè)函數(shù)。
參數(shù)
((__block_impl *)block, 10, 10)
二. 捕獲變量
捕獲變量又分為三種:捕獲-auto變量| 捕獲-static變量 | 捕獲-全局變量
1.捕獲-auto變量

auto變量是聲明在函數(shù)內(nèi)部的變量艇搀,比如int a = 0;這句代碼聲明在函數(shù)內(nèi)部尿扯,那a就是auto變量,等價(jià)于auto int a = 0;auto變量時(shí)分配在棧區(qū)焰雕,當(dāng)超出作用域時(shí)衷笋,其占用的內(nèi)存會被系統(tǒng)自動銷毀并生成。下面看一段代碼:

int a = 10;
void (^block)(void) = ^{
      NSLog(@"%d", a);
};
a = 20;
block();
輸出:10

自動變量a的值明明已經(jīng)變?yōu)榱?0矩屁,為什么輸出結(jié)果還是10呢辟宗?我們把這段代碼轉(zhuǎn)化為C++的源碼看看爵赵。

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
}

對比一下上面分析的沒有捕獲自動變量的源代碼,我們發(fā)現(xiàn)這里_main_block_impl_0中傳入的參數(shù)多了一個(gè)a泊脐。然后我們往上翻看看_main_block_impl_0的結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; //這里也多了一個(gè)a
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
_main_block_impl_0這個(gè)結(jié)構(gòu)體中我們發(fā)現(xiàn)多了一個(gè)int類型的成員變量a空幻,在結(jié)構(gòu)體的構(gòu)造函數(shù)中多了一個(gè)參數(shù)int _a,并且用這個(gè)int _a去初始化成員變量a容客。所以在void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);中傳入了自動變量a用來初始化_main_block_impl_0的成員變量a秕铛。那這個(gè)時(shí)候_main_block_impl_0的成員變量a就被賦值為10了。由于上面這一步是值傳遞耘柱,所以當(dāng)執(zhí)行a = 20時(shí)如捅,_main_block_impl_0結(jié)構(gòu)體的成員變量a的值是不會隨之改變的,仍然是10调煎。然后我們再來看一下_main_block_func_0的結(jié)構(gòu)有何變化:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_fb5f0d_mi_0, a);
        }

可以看到镜遣,通過傳入的_main_block_impl_0這個(gè)結(jié)構(gòu)體獲得其成員變量a的值。

2.捕獲-static變量

上面講的捕獲的是自動變量士袄,在函數(shù)內(nèi)部聲明的變量默認(rèn)為自動變量悲关,即默認(rèn)用auto修飾。那么如果在函數(shù)內(nèi)部聲明的變量用static修飾娄柳,又會帶來哪些不同呢寓辱?static變量和auto變量的不同之處在于變量的內(nèi)存的回收時(shí)機(jī)。auto變量在其作用域結(jié)束時(shí)就會被系統(tǒng)自動回收赤拒,而static變量在變量的作用域結(jié)束時(shí)并不會被系統(tǒng)自動回收秫筏。
先看一段代碼:

 static int a = 10;
 void (^block)(void) = ^{
    NSLog(@"%d", a);
 };
 a = 20;
 block();
輸出:20

結(jié)果是20,這個(gè)和2中的打印結(jié)果不一樣挎挖,為什么局部變量從auto變成了static結(jié)果會不一樣呢这敬?我們還是從源碼來分析:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
       static int a = 10;
       void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
       a = 20;
       ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
       return 0;
    }
}

和2不一樣的是,這里傳入_main_block_impl_0的是&a蕉朵,

WeWork Helper20200115114353.png

也即是a這個(gè)變量的地址值崔涂。那么這個(gè)&a是賦值給誰了呢?我們上翻找到_main_block_impl_0的結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
這里我們可以看到結(jié)構(gòu)體多了一個(gè)指針類型的成員變量int *a,然后在構(gòu)造函數(shù)中始衅,將傳遞過來的&a冷蚂,賦值給這個(gè)指針變量。也就是說汛闸,在_main_block_impl_0這個(gè)結(jié)構(gòu)體中多了一個(gè)成員變量蝙茶,這個(gè)成員變量是指針,指向a這個(gè)變量蛉拙。所以當(dāng)a變量的值發(fā)生變化時(shí)尸闸,能夠得到最新的值
3.捕獲-全局變量

2和3分析了兩種類型的局部變量孕锄,auto局部變量和static局部變量吮廉。這一部分則分析全局變量。全局變量會不會像局部變量一樣被block所捕獲呢畸肆?我們還是看一下實(shí)例:

int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"%d %d", height, weight);
        };
        height = 30;
        weight = 40;
        block();
        return 0;
    }
}
打踊侣:30 40

查看源碼

int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        height = 30;
        weight = 40;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
}

這里我們可以看到,height和weight這兩個(gè)全局變量沒有作為參數(shù)傳入_main_block_impl_0中去轴脐。然后我們再查看一下_main_block_impl_0的結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到调卑,_main_block_impl_0中并沒有增加成員變量。然后我們再看_main_block_func_0的結(jié)構(gòu):

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_46c51b_mi_0, height, weight);
        }
可以看到大咱,這個(gè)地方在調(diào)用的時(shí)候是直接調(diào)用的全局變量height和weight, 所以block并不會捕獲全局變量

總結(jié):


WeWork Helper20200115115519.png
思考: 為什么對于不同類型的變量恬涧,block的處理方式不同呢?

這是由變量的生命周期決定的碴巾。對于auto變量溯捆,當(dāng)作用域結(jié)束時(shí),會被系統(tǒng)自動回收厦瓢,而block很可能是在超出auto變量作用域的時(shí)候去執(zhí)行提揍,如果之前沒有捕獲auto變量,那么后面執(zhí)行的時(shí)候煮仇,auto變量已經(jīng)被回收了劳跃,得不到正確的值。對于static局部變量浙垫,它的生命周期不會因?yàn)樽饔糜蚪Y(jié)束而結(jié)束刨仑,所以block只需要捕獲這個(gè)變量的地址,在執(zhí)行的時(shí)候通過這個(gè)地址去獲取變量的值夹姥,這樣可以獲得變量的最新的值杉武。而對于全局變量,在任何位置都可以直接讀取變量的值佃声。

思考: 為什么對于auto變量block捕獲的是數(shù)值而 對于static局部變量捕獲的是地址艺智?

還是由變量的生命周期決定的,對于auto變量圾亏,當(dāng)作用域結(jié)束時(shí)十拣,會被系統(tǒng)自動回收,地址就會變成空的志鹃,造成壞地址訪問夭问。對于static局部變量,它的生命周期不會因?yàn)樽饔糜蚪Y(jié)束而結(jié)束曹铃,所以block只需要捕獲這個(gè)變量的地址缰趋,在執(zhí)行的時(shí)候通過這個(gè)地址去獲取變量的值。

思考: static局部變量生命周期什么時(shí)候結(jié)束?
說明:
在局部變量的說明前再加上static說明符就構(gòu)成靜態(tài)局部變量秘血。例如:static int a,b; static float array[5]={1,2,3,4,5}味抖;
靜態(tài)局部變量屬于靜態(tài)存儲方式,它具有以下特點(diǎn):

(1)靜態(tài)局部變量在函數(shù)內(nèi)定義灰粮,但不象自動變量那樣仔涩,當(dāng)調(diào)用時(shí)就存在,退出函數(shù)時(shí)就消失粘舟。靜態(tài)局部變量始終存在著熔脂,也就是說它的生存期為整個(gè)源程序。
(2)靜態(tài)局部變量的生存期雖然為整個(gè)源程序柑肴,但是其作用域仍與自動變量相同霞揉,即只能在定義該變量的函數(shù)內(nèi)使用該變量。退出該函數(shù)后晰骑,盡管該變量還繼續(xù)存在适秩,但不能使用它。
(3)允許對構(gòu)造類靜態(tài)局部量賦初值些侍。若未賦以初值隶症,則由系統(tǒng)自動賦以0值。
(4)對基本類型的靜態(tài)局部變量若在說明時(shí)未賦以初值岗宣,則系統(tǒng)自動賦予0值蚂会。而對自動變量不賦初值,則其值是不定的耗式。根據(jù)靜態(tài)局部變量的特點(diǎn)胁住,可以看出它是一種生存期為整個(gè)源程序的量。雖然離開定義它的函數(shù)后不能使用刊咳,但如再次調(diào)用定義它的函數(shù)時(shí)彪见,它又可繼續(xù)使用,而且保存了前次被調(diào)用后留下的值娱挨。因此余指,當(dāng)多次調(diào)用一個(gè)函數(shù)且要求在調(diào)用之間保留某些變量的值時(shí),可考慮采用靜態(tài)局部變量跷坝。雖然用全局變量也可以達(dá)到上述目的酵镜,但全局變量有時(shí)會造成意外的副作用,因此仍以采用局部靜態(tài)變量為宜柴钻。

補(bǔ)充:靜態(tài)全局變量

全局變量(外部變量)的說明之前再冠以static 就構(gòu)成了靜態(tài)的全局變量淮韭。全局變量本身就是靜態(tài)存儲方式,靜態(tài)全局變量當(dāng)然也是靜態(tài)存儲方式贴届。這兩者在存儲方式上并無不同靠粪。這兩者的區(qū)別雖在于非靜態(tài)全局變量的作用域是整個(gè)源程序蜡吧,當(dāng)一個(gè)源程序由多個(gè)源文件組成時(shí),非靜態(tài)的全局變量在各個(gè)源文件中都是有效的占键。而靜態(tài)全局變量則限制了其作用域昔善,即只在定義該變量的源文件內(nèi)有效,在同一源程序的其它源文件中不能使用它捞慌。由于靜態(tài)全局變量的作用域局限于一個(gè)源文件內(nèi)耀鸦,只能為該源文件內(nèi)的函數(shù)公用柬批,因此可以避免在其它源文件中引起錯(cuò)誤啸澡。從以上分析可以看出, 把局部變量改變?yōu)殪o態(tài)變量后是改變了它的存儲方式即改變了它的生存期氮帐。把全局變量改變?yōu)殪o態(tài)變量后是改變了它的作用域嗅虏,限制了它的使用范圍。因此static 這個(gè)說明符在不同的地方所起的作用是不同的上沐。應(yīng)予以注意皮服。

4.變量捕獲-self變量
@implementation Person
- (void)test{
    void(^block)(void) = ^{
        NSLog(@"%@", self);
    };
}
@end

這個(gè)Person類中只有一個(gè)東西,就是test這個(gè)函數(shù)参咙,那么這個(gè)block有沒有捕獲self變量呢龄广?
要搞清這個(gè)問題,我們只需要知道搞清楚這里self變量是局部變量還是全局變量蕴侧,如果是局部變量择同,那么是一定會捕獲的,而如果是全局變量净宵,則一定不會被捕獲敲才。
我們把這個(gè)Person.m文件轉(zhuǎn)化為c++的源碼,然后找到test函數(shù)在c++中的表示:

static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
}

我們可以看到择葡,本來Person.m中紧武,這個(gè)test函數(shù)我是沒有傳任何參數(shù)的,但是轉(zhuǎn)化為c++的代碼后敏储,這里傳入了兩個(gè)參數(shù)阻星,一個(gè)是self參數(shù),一個(gè)是_cmd已添。self很常見妥箕,_cmd表示test函數(shù)本身。所以我們就很清楚了酝碳,self是作為參數(shù)傳進(jìn)來矾踱,也就是局部變量,那么block應(yīng)該是捕獲了self變量疏哗,事實(shí)是不是這樣呢呛讲?我們只需要查看一下_Person_test_block_impl_0的結(jié)構(gòu)就可以知道了。

_Person_test_block_impl_0的結(jié)構(gòu):
struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
可以看到,self確實(shí)是作為成員變量被捕獲了贝搁。
三. Block的類型

前面已經(jīng)說過了吗氏,Block的本質(zhì)就是一個(gè)OC對象,既然它是OC對象雷逆,那么它就有類型弦讽。
在搞清楚Block的類型之前,先把ARC關(guān)掉膀哲,因?yàn)锳RC幫我們做了太多的事往产,不方便我們觀察結(jié)果。關(guān)掉ARC的方法在Build Settings里面搜索Objective-C Automatic Reference Counting某宪,把這一項(xiàng)置為NO仿村。
代碼如下

int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void (^block)(void) = ^{
            NSLog(@"%d %d", height, age);
        };
        NSLog(@"%@\n %@\n %@\n %@", [block class], [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);
        return 0;
    }
}

打印

 __NSStackBlock__
 __NSStackBlock
 NSBlock
 NSObject

這說明上面定義的這個(gè)block的類型是NSStackBlock,并且它最終繼承自NSObject也說明Block的本質(zhì)是OC對象兴喂。
Block有三種類型蔼囊,分別是NSGlobalBlock, MallocBlock, NSStackBlock
代碼舉例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 內(nèi)部沒有調(diào)用外部變量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 內(nèi)部調(diào)用外部變量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接調(diào)用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}
// 輸出
__NSGlobalBlock__   __NSMallocBlock__   __NSStackBlock__

這三種類型的Block對象的存儲區(qū)域如下:

Block對象的存儲區(qū)域.png

數(shù)據(jù)段中的__NSGlobalBlock__直到程序結(jié)束才會被回收衣迷,不過我們很少使用到__NSGlobalBlock__類型的block畏鼓,因?yàn)檫@樣使用block并沒有什么意義。

__NSStackBlock__類型的block存放在棧中壶谒,我們知道棧中的內(nèi)存由系統(tǒng)自動分配和釋放云矫,作用域執(zhí)行完畢之后就會被立即釋放,而在相同的作用域中定義block并且調(diào)用block似乎也多此一舉佃迄。

__NSMallocBlock__是在平時(shí)編碼過程中最常使用到的泼差。存放在堆中需要我們自己進(jìn)行內(nèi)存管理。

Block類型的解釋說明
截獲了自動變量的Block是NSStackBlock類型呵俏,沒有截獲自動變量的Block則是NSGlobalStack類型,NSStackBlock類型的Block進(jìn)行copy操作之后其類型變成了NSMallocBlock類型堆缘。

每一種類型的block調(diào)用copy后的結(jié)果如下所示


經(jīng)過copy之后的改變.png
思考1:NSStackBlock類型的Block進(jìn)行copy操作后Block對象為什么從棧復(fù)制到了堆這樣做有什么道理?我們首先來看一段代碼
void (^block)(void);
void test() {
    int age = 10;
    block = ^{
        NSLog(@"age=%d", age);
    };
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        test();
        block();
        return 0;
    }
}

打印結(jié)果可能該是10普碎,那么結(jié)果是不是這樣呢吼肥?我們打印看一下:

age=-411258824

很奇怪,打印了一個(gè)這么奇怪的數(shù)字麻车。這是為什么呢缀皱?
通過上面的總結(jié)可知,block捕獲了了自動變量age动猬,所以它是NSStackBlock類型的啤斗,因此block是存放在棧區(qū)age是被捕獲作為結(jié)構(gòu)體的成員變量赁咙,其值也是被保存在棧區(qū)钮莲。所以當(dāng)test這個(gè)函數(shù)調(diào)用完畢后免钻,它棧上的東西就有可能被銷毀了,一旦銷毀了崔拥,age值就不確定是多少了极舔。通過打印結(jié)果也可以看到,確實(shí)是影響到了block的執(zhí)行链瓦。
如果我們對block執(zhí)行copy操作拆魏,會是什么結(jié)果呢?

void (^block)(void);
void test() {
    int age = 10;
    block = [^{
        NSLog(@"age=%d", age);
    } copy]; // 調(diào)用一下copy方法
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        test();
        block();
        return 0;
    }
}

打印結(jié)果:

age=10

這個(gè)時(shí)候得出了正確的輸出慈俯。
因?yàn)閷?code>block進(jìn)行copy操作后渤刃,block從棧區(qū)被復(fù)制到了堆區(qū),它的成員變量age也隨之被復(fù)制到了堆區(qū)肥卡,這樣test函數(shù)執(zhí)行完之后溪掀,它的棧區(qū)被銷毀并不影響block,因此能得出正確的輸出步鉴。

補(bǔ)充.ARC環(huán)境下自動為Block進(jìn)行copy操作的情況
void (^block)(void);

void test() {
    int age = 10;
    block = ^{
        NSLog(@"age=%d", age);
    };
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        test();
        block();
        return 0;
    }
}

這種使用方式其實(shí)非常常見,我們在使用的時(shí)候也沒有發(fā)現(xiàn)有什么問題璃哟,那為什么在MRC環(huán)境下就有問題呢氛琢?因?yàn)樵贏RC環(huán)境下編譯器為我們做了很多copy操作。其中有一個(gè)規(guī)則就是如果Block被強(qiáng)指針指著随闪,那么編譯器就會對其進(jìn)行copy操作阳似。我們看到這里:

^{
        NSLog(@"age=%d", age);
    };

這個(gè)Block塊是被強(qiáng)指針指著(上面說過void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);),所以它會進(jìn)行copy操作铐伴,由于其使用了自動變量撮奏,所以是棧區(qū)的Block。經(jīng)過復(fù)制以后就到了堆區(qū)当宴,這樣由于Block在堆區(qū)畜吊,所以就不受Block執(zhí)行完成的影響,隨時(shí)可以獲取age的正確值户矢。

總結(jié)一下ARC環(huán)境下自動進(jìn)行copy操作的情況一共有以下幾種:
  • block作為函數(shù)返回值時(shí)玲献。
  • 將block賦值給__strong指針時(shí)。
  • block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)梯浪。
  • GCD中的API捌年。
block作為函數(shù)返回值時(shí)
typedef void(^Block)(void);

Block test() {
    int age = 10;
    return ^{
        NSLog(@"age=%d", age);
    };    
}

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

        Block block = test();
        block();

        return 0;
    }
}

test函數(shù)的返回值是一個(gè)block,那這種情況的時(shí)候挂洛,在棧區(qū)的

^{
        NSLog(@"age=%d", age);
    };

因?yàn)楸灰粋€(gè)強(qiáng)指針指著礼预,所以這個(gè)block會被復(fù)制到堆區(qū)

block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
NSArray *array = [[NSArray alloc] init];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"%d", idx);
        }];

enumerateObjectsUsingBlock:這個(gè)函數(shù)中的block會進(jìn)行copy操作

GCD中的API

GCD中的很多API的參數(shù)都有block,這個(gè)時(shí)候都會對block進(jìn)行一次copy操作虏劲,比如下面這個(gè)dispatch_after函數(shù):

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"wait");
        });

更多精彩內(nèi)容

我的簡書主頁
我的博客主頁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末托酸,一起剝皮案震驚了整個(gè)濱河市荠藤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌获高,老刑警劉巖哈肖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異念秧,居然都是意外死亡淤井,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門摊趾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來币狠,“玉大人,你說我怎么就攤上這事砾层′雒啵” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵肛炮,是天一觀的道長止吐。 經(jīng)常有香客問我,道長侨糟,這世上最難降的妖魔是什么碍扔? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮秕重,結(jié)果婚禮上不同,老公的妹妹穿的比我還像新娘。我一直安慰自己溶耘,他們只是感情好二拐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凳兵,像睡著了一般百新。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上留荔,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天吟孙,我揣著相機(jī)與錄音,去河邊找鬼聚蝶。 笑死杰妓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碘勉。 我是一名探鬼主播巷挥,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼验靡!你這毒婦竟也來了倍宾?” 一聲冷哼從身側(cè)響起雏节,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎高职,沒想到半個(gè)月后钩乍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怔锌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年寥粹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埃元。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涝涤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岛杀,到底是詐尸還是另有隱情阔拳,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布类嗤,位于F島的核電站糊肠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏土浸。R本人自食惡果不足惜罪针,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望黄伊。 院中可真熱鬧,春花似錦派殷、人聲如沸还最。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拓轻。三九已至,卻和暖如春经伙,著一層夾襖步出監(jiān)牢的瞬間扶叉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工帕膜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枣氧,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓垮刹,卻偏偏與公主長得像达吞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子荒典,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容