《OC高級(jí)編程》筆記2——block的使用和實(shí)質(zhì)探究

block的使用


block是什么

** block就是可以截獲局部變量的匿名函數(shù)壕翩。**

解釋一下:** block可以獲取被定義時(shí)詞法范圍內(nèi)的狀態(tài)(比如局部變量等)祸轮,并且在一定條件下(比如使用__block變量)可以修改這些狀態(tài)迷守。 **
比如蒜魄,在某方法中的一個(gè)block暖呕,是可以獲取到該方法內(nèi)的變量的斜做。

block的語法
block語法.jpg

比如下面定義了一個(gè):名為addBlock,參數(shù)列表是兩個(gè)int型的數(shù)據(jù)缰揪,返回值也為int型的block陨享。

    int (^addBlock)(int, int) = ^(int x, int y){
        return x + y;
    };
    int result = addBlock(2,4); // 傳入實(shí)參葱淳,執(zhí)行該block,返回了int型的結(jié)果抛姑。

block和其他變量一樣都可以局部變量赞厕,全局變量,靜態(tài)變量定硝,甚至方法參數(shù)等皿桑。
block既然是種變量,那它也就有自己所屬的類型蔬啡。決定一個(gè)block是什么類型的因素是返回值和參數(shù)诲侮。int (^addBlock)(int, int)代表返回值為int型,兩個(gè)int型參數(shù)箱蟆,名為addBlock沟绪。但這樣表示block有點(diǎn)不太好。其一是閱讀起來不太順暢空猜,其二是若我們要重構(gòu)或者修改原來定義的block绽慈,則要在每個(gè)使用該block的地方進(jìn)行手工修改。所以我們可以統(tǒng)一在一個(gè)地方對(duì)其進(jìn)行類型再定義辈毯。

// 把返回值為void型坝疼,倆int型參數(shù)的block統(tǒng)一再定義為MyBlock類型。
typedef void (^MyBlock)(int);

  ...
    MyBlock myBlock = ^(int x){
        NSLog(@"myBlock:rereult = %d", x);
    };

// 或者block作為方法參數(shù)時(shí)
- (void)doSomething:(MyBlock)myBlock param:(int)count
{
    // 調(diào)用myBlock
    myBlock(count);
}

注意:block的語法本身就比較怪異谆沃,再加上:定義block時(shí)(^blockName)括號(hào)里面的是block名字钝凶,但是通過typedef進(jìn)行類型再定義時(shí)(^blockClass)括號(hào)里表示代表該block的類型名⊙溆埃總之耕陷,block的語法比較別扭,別記錯(cuò)了夭咬。

截獲局部變量

開頭我們說了block是可以截獲局部變量的匿名函數(shù)啃炸。也就是說在某方法內(nèi)的block是可以獲取該方法定義的局部變量的。** 而且是只讀的卓舵,不可以對(duì)其進(jìn)行修改操作南用。若非要進(jìn)行修改,則得在局部變量前加上__block修飾符掏湾。**下面用三小段代碼分別來驗(yàn)證:

// block內(nèi)可以讀取局部變量

    int count = 10;

    void (^countBlock1)(void) = ^(void){
        NSLog(@"count----%d",count);
    };
    
    countBlock1();

// BlockWang[1534:689473] count----10
// 試圖在block內(nèi)修改局部變量

    int count = 10;
    
    void (^countBlock1)(void) = ^(void){
        count++;
    };
    
    countBlock1();

上面這段代碼編譯時(shí)會(huì)報(bào)錯(cuò):


試圖在block內(nèi)修改局部變量編譯時(shí)報(bào)錯(cuò).png
// 在局部變量前加上__block修飾符裹虫,后就可以在block內(nèi)部修改此局部變量了

    __block int count = 10;
    
    void (^countBlock1)(void) = ^(void){
        NSLog(@"count----%d",++count);
    };
    
    countBlock1();

// BlockWang[1534:689473] count----11

需要小心下面這段代碼:我們?cè)诙x一個(gè)block后再修改了count值為2,然后再執(zhí)行該block融击。執(zhí)行的打印結(jié)果是count----10筑公,這就說明block“截獲局部變量”的處理是在定義這個(gè)block時(shí),而且似乎所謂“截獲局部變量”就是在block中有了個(gè)和count相應(yīng)的獨(dú)立的數(shù)據(jù)尊浪,不然我們當(dāng)修改count值時(shí)匣屡,為什么打印出的block內(nèi)的該變量沒變化呢封救?這個(gè)疑問在后面block的實(shí)現(xiàn)中我們慢慢分析。

    int count = 10;
    
    void (^countBlock)(void) = ^(void){
        NSLog(@"count----%d",count);
    };
    
    count = 2;
    countBlock(); // 執(zhí)行block

// BlockWang[1534:689473] count----10

block的實(shí)質(zhì)


接下來我們會(huì)把代碼通過Clang命令轉(zhuǎn)換為中間代碼來觀察block的實(shí)現(xiàn)捣作,探索它的本質(zhì)誉结。

block的實(shí)現(xiàn)結(jié)構(gòu):

首先我們研究只打印字符串的,最簡(jiǎn)單的block:

#include "BlockClang.h"

int main()
{
    void (^myBlock)(void) = ^(void){
        printf("this is a block");
    };
    
    myBlock();
    
    return 0;
}

打開終端券躁,進(jìn)入項(xiàng)目路徑惩坑,然后敲入Clang的命令clang -rewrite-objc BlockClang.c。此時(shí)也拜,F(xiàn)inder里多了個(gè)文件BlockClang.cpp以舒,它正是轉(zhuǎn)換后的中間代碼。
小小的一段代碼轉(zhuǎn)換為BlockClang.cpp后竟然有超500多行慢哈,我們只提取出對(duì)我們有意義的部分:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  printf("this is a block");
 }

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()
{
 void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

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

 return 0;
}

我們可以看到蔓钟,block的結(jié)構(gòu)實(shí)現(xiàn)是結(jié)構(gòu)體,其中__main_block_impl_0結(jié)構(gòu)體代表block的結(jié)構(gòu)卵贱。它有一個(gè)__block_impl類型的impl成員和__main_block_desc_0 *類型的成員Desc(顧名思義奋刽,它倆分別代表block的實(shí)現(xiàn)和描述信息)。以及一個(gè)構(gòu)造函數(shù)__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)通過該構(gòu)造函數(shù)艰赞,分別給block的成員賦值。那block的兩個(gè)成員變量的結(jié)構(gòu)又是怎么樣的肚吏,它們里面都有哪些成員呢方妖?

// __block_impld結(jié)構(gòu)體的結(jié)構(gòu)

struct __block_impl {
  void *isa; // block的類型
  int Flags; // 標(biāo)志位
  int Reserved; // 保留位
  void *FuncPtr; // block的實(shí)現(xiàn),函數(shù)指針罚攀,指向__main_block_func_0
};
// __main_block_desc_0結(jié)構(gòu)體的結(jié)構(gòu)

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

然后就是__main_block_desc_0函數(shù)党觅,即block的實(shí)現(xiàn)體。該函數(shù)接受一個(gè)__cself參數(shù)斋泄,即對(duì)應(yīng)的block自身杯瞻。(** 思考:為什么要傳一個(gè)自身作為參數(shù)? **)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  printf("this is a block");
 }

最后看main函數(shù)里block的實(shí)現(xiàn)和調(diào)用:

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

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

可以看出執(zhí)行block就是調(diào)用一個(gè)以block自身作為參數(shù)的函數(shù)炫掐,這個(gè)函數(shù)對(duì)應(yīng)著block的執(zhí)行體魁莉。

block為什么能截獲局部變量?

我們來看個(gè)截獲局部變量的block募胃,并轉(zhuǎn)換為中間代碼旗唁,觀察代碼,以嘗試解答這個(gè)問題痹束。

int main()
{
    int count = 10;
    void (^myBlock)(void) = ^(void){
        printf("count = %d", count);
    };
    
    myBlock();
    
    return 0;
}

轉(zhuǎn)換后的代碼检疫。只列出發(fā)生了變化的代碼:
可以看到__main_block_impl_0結(jié)構(gòu)體中多了count這個(gè)成員變量。并且構(gòu)造函數(shù)的參數(shù)中也多了count這一項(xiàng)祷嘶。

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

可以看到block的實(shí)現(xiàn)體中__main_block_func_0多了int count = __cself->count;這一句屎媳。
** block之所以可以截獲局部變量就是因?yàn)?code>__cself訪問了該block里面的count成員變量夺溢,而block的count成員的值是在實(shí)現(xiàn)該block時(shí)賦得的。** 此時(shí)烛谊,前面我們的疑問:這個(gè)函數(shù)“為什么要傳一個(gè)自身作為參數(shù)风响?的問題也迎刃而解,不言而喻了晒来〕睿”之所以該方法要傳代表block結(jié)構(gòu)的__main_block_impl_0結(jié)構(gòu)體為參數(shù),就是為了讀取該block捕獲的局部變量湃崩。

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

  printf("count = %d", count);
 }
int main()
{
 int count = 10;
 void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));

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

 return 0;
}
block為什么只能讀取局部變量荧降,而不能修改局部變量呢?

因?yàn)閙ain函數(shù)中的局部變量count和函數(shù)__main_block_func_0不在同一個(gè)作用域中攒读,調(diào)用過程中只是進(jìn)行了值傳遞朵诫。當(dāng)然,在上面代碼中薄扁,我們可以通過指針來實(shí)現(xiàn)局部變量的修改剪返。不過這是由于在調(diào)用__main_block_func_0時(shí),main函數(shù)棧還沒展開完成邓梅,變量count還在棧中脱盲。但是在很多情況下,block是作為參數(shù)傳遞以供后續(xù)回調(diào)執(zhí)行的日缨。通常在這些情況下钱反,block被執(zhí)行時(shí),定義時(shí)所在的函數(shù)棧已經(jīng)被展開匣距,局部變量已經(jīng)不在棧中了面哥。(不過既然如此,我們可以推斷出靜態(tài)局部變量之所以可以在block修改就是通過——指針毅待。因?yàn)殪o態(tài)局部變量存在于內(nèi)存數(shù)據(jù)段尚卫,不存在棧展開后非法訪存的風(fēng)險(xiǎn)。見下一段尸红。)
所以吱涉,對(duì)于auto類型的局部變量,不允許block進(jìn)行修改是合理的驶乾。

block為什么可以又可以修改靜態(tài)變量和全局變量呢邑飒?

因?yàn)樗鼈儾淮嬖跅U归_后非法訪存的風(fēng)險(xiǎn)。所以可以通過** 指針 ** 來傳遞靜態(tài)變量的级乐。
可以看出靜態(tài)變量在main內(nèi)實(shí)現(xiàn)block時(shí)疙咸,捕獲的是count的地址&count。以及在__main_block_impl_0結(jié)構(gòu)體中成員變量變成了指針類型int *count;风科。即通過指針修改(它們是址傳遞)撒轮。

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

  printf("count = %d", ++(*count));
 }
int main()
{
 static int count = 10;
 void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &count));

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

 return 0;
}
為什么被__block修飾的局部變量在block中卻又是可以修改的乞旦?

我們來一段局部變量前加了__block的代碼例子:

#include "BlockClang.h"

int main()
{
    __block int count = 10;
    void (^myBlock)(void) = ^(void){
        printf("count = %d",++count);
    };
    
    myBlock();
}

轉(zhuǎn)換中間代碼后,看到比以前多了很多東西题山。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};



struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};



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



struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};



static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref

  printf("count = %d",++(count->__forwarding->count));
 }




static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}



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



int main()
{
 __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 10};
 void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));

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

可以看到__main_block_impl_0結(jié)構(gòu)體成員變量count變?yōu)榱?code>__Block_byref_count_0 *類型兰粉。而相應(yīng)的__main_block_func_0函數(shù)中count也變?yōu)榱?code>__Block_byref_count_0 *類型。

__Block_byref_count_0也是一個(gè)結(jié)構(gòu)體顶瞳。它的構(gòu)成是:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding; // 指向另外一個(gè)變量玖姑,這兒的具體實(shí)現(xiàn)思路不太懂
 int __flags;
 int __size;
 int count;
};

** 但是問題照樣存在,我們修改的變量count它是位于棧上的慨菱。若當(dāng)block被回調(diào)執(zhí)行時(shí)焰络,棧早已被展開,早沒count了符喝。這該如何是好闪彼?**

上面的代碼中我們可以注意到:__main_block_desc_0函數(shù)中多了兩個(gè)成員函數(shù),分別指向__main_block_copy_0协饲,__main_block_dispose_0函數(shù)畏腕。

當(dāng)block從棧上被copy到堆上時(shí),會(huì)調(diào)用__main_block_copy_0__block類型的成員變量count從棧上復(fù)制到堆上茉稠;而當(dāng)block被釋放時(shí)描馅,相應(yīng)地會(huì)調(diào)用__main_block_dispose_0來釋放__block類型的成員變量i。
一會(huì)在棧上而线,一會(huì)在堆上流昏,那如果棧上和堆上同時(shí)對(duì)該變量進(jìn)行操作,怎么辦吞获?
這時(shí)候,__forwarding的作用就體現(xiàn)出來了:當(dāng)一個(gè)__block變量從棧上被復(fù)制到堆上時(shí)谚鄙,棧上的那個(gè)__Block_byref_i_0結(jié)構(gòu)體中的__forwarding指針也會(huì)指向堆上的結(jié)構(gòu)各拷。


資料參考:

iOS中block實(shí)現(xiàn)的探究
C語言中閉包的探究及比較
C語言中閉包的探究及比較
對(duì)Objective-C中Block的追探
談Objective-C block的實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市闷营,隨后出現(xiàn)的幾起案子烤黍,更是在濱河造成了極大的恐慌,老刑警劉巖傻盟,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件速蕊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娘赴,警方通過查閱死者的電腦和手機(jī)规哲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诽表,“玉大人唉锌,你說我怎么就攤上這事隅肥。” “怎么了袄简?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵腥放,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我绿语,道長(zhǎng)秃症,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任吕粹,我火速辦了婚禮种柑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昂芜。我一直安慰自己莹规,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布泌神。 她就那樣靜靜地躺著良漱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪欢际。 梳的紋絲不亂的頭發(fā)上母市,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音损趋,去河邊找鬼患久。 笑死,一個(gè)胖子當(dāng)著我的面吹牛浑槽,可吹牛的內(nèi)容都是我干的蒋失。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼桐玻,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼篙挽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镊靴,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤铣卡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后偏竟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煮落,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年踊谋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蝉仇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖量淌,靈堂內(nèi)的尸體忽然破棺而出骗村,到底是詐尸還是另有隱情,我是刑警寧澤呀枢,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布胚股,位于F島的核電站,受9級(jí)特大地震影響裙秋,放射性物質(zhì)發(fā)生泄漏琅拌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一摘刑、第九天 我趴在偏房一處隱蔽的房頂上張望进宝。 院中可真熱鬧,春花似錦枷恕、人聲如沸党晋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽未玻。三九已至,卻和暖如春胡控,著一層夾襖步出監(jiān)牢的瞬間扳剿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工昼激, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庇绽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓橙困,卻偏偏與公主長(zhǎng)得像瞧掺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子凡傅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

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