《OC高级编程》笔记2——block的使用和实质探究

block的使用


block是什么

** block就是可以截获局部变量的匿名函数。**

解释一下:** block可以获取被定义时词法范围内的状态(比如局部变量等),并且在一定条件下(比如使用__block变量)可以修改这些状态。 **
比如,在某方法中的一个block,是可以获取到该方法内的变量的。

block的语法
block语法.jpg

比如下面定义了一个:名为addBlock,参数列表是两个int型的数据,返回值也为int型的block。

    int (^addBlock)(int, int) = ^(int x, int y){
        return x + y;
    };
    int result = addBlock(2,4); // 传入实参,执行该block,返回了int型的结果。

block和其他变量一样都可以局部变量,全局变量,静态变量,甚至方法参数等。
block既然是种变量,那它也就有自己所属的类型。决定一个block是什么类型的因素是返回值和参数。int (^addBlock)(int, int)代表返回值为int型,两个int型参数,名为addBlock。但这样表示block有点不太好。其一是阅读起来不太顺畅,其二是若我们要重构或者修改原来定义的block,则要在每个使用该block的地方进行手工修改。所以我们可以统一在一个地方对其进行类型再定义

// 把返回值为void型,俩int型参数的block统一再定义为MyBlock类型。
typedef void (^MyBlock)(int);

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

// 或者block作为方法参数时
- (void)doSomething:(MyBlock)myBlock param:(int)count
{
    // 调用myBlock
    myBlock(count);
}

注意:block的语法本身就比较怪异,再加上:定义block时(^blockName)括号里面的是block名字,但是通过typedef进行类型再定义时(^blockClass)括号里表示代表该block的类型名。总之,block的语法比较别扭,别记错了。

截获局部变量

开头我们说了block是可以截获局部变量的匿名函数。也就是说在某方法内的block是可以获取该方法定义的局部变量的。** 而且是只读的,不可以对其进行修改操作。若非要进行修改,则得在局部变量前加上__block修饰符。**下面用三小段代码分别来验证:

// block内可以读取局部变量

    int count = 10;

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

// BlockWang[1534:689473] count----10
// 试图在block内修改局部变量

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

上面这段代码编译时会报错:


试图在block内修改局部变量编译时报错.png
// 在局部变量前加上__block修饰符,后就可以在block内部修改此局部变量了

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

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

需要小心下面这段代码:我们在定义一个block后再修改了count值为2,然后再执行该block。执行的打印结果是count----10,这就说明block“截获局部变量”的处理是在定义这个block时,而且似乎所谓“截获局部变量”就是在block中有了个和count相应的独立的数据,不然我们当修改count值时,为什么打印出的block内的该变量没变化呢?这个疑问在后面block的实现中我们慢慢分析。

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

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

block的实质


接下来我们会把代码通过Clang命令转换为中间代码来观察block的实现,探索它的本质。

block的实现结构:

首先我们研究只打印字符串的,最简单的block:

#include "BlockClang.h"

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

打开终端,进入项目路径,然后敲入Clang的命令clang -rewrite-objc BlockClang.c。此时,Finder里多了个文件BlockClang.cpp,它正是转换后的中间代码。
小小的一段代码转换为BlockClang.cpp后竟然有超500多行,我们只提取出对我们有意义的部分:

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的结构实现是结构体,其中__main_block_impl_0结构体代表block的结构。它有一个__block_impl类型的impl成员和__main_block_desc_0 *类型的成员Desc(顾名思义,它俩分别代表block的实现和描述信息)。以及一个构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)通过该构造函数,分别给block的成员赋值。那block的两个成员变量的结构又是怎么样的,它们里面都有哪些成员呢?

// __block_impld结构体的结构

struct __block_impl {
  void *isa; // block的类型
  int Flags; // 标志位
  int Reserved; // 保留位
  void *FuncPtr; // block的实现,函数指针,指向__main_block_func_0
};
// __main_block_desc_0结构体的结构

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函数,即block的实现体。该函数接受一个__cself参数,即对应的block自身。(** 思考:为什么要传一个自身作为参数? **)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  printf("this is a block");
 }

最后看main函数里block的实现和调用:

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

可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。

block为什么能截获局部变量?

我们来看个截获局部变量的block,并转换为中间代码,观察代码,以尝试解答这个问题。

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

转换后的代码。只列出发生了变化的代码:
可以看到__main_block_impl_0结构体中多了count这个成员变量。并且构造函数的参数中也多了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;
  }
};

可以看到block的实现体中__main_block_func_0多了int count = __cself->count;这一句。
** block之所以可以截获局部变量就是因为__cself访问了该block里面的count成员变量,而block的count成员的值是在实现该block时赋得的。** 此时,前面我们的疑问:这个函数“为什么要传一个自身作为参数?的问题也迎刃而解,不言而喻了。”之所以该方法要传代表block结构的__main_block_impl_0结构体为参数,就是为了读取该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为什么只能读取局部变量,而不能修改局部变量呢?

因为main函数中的局部变量count和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量count还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了。(不过既然如此,我们可以推断出静态局部变量之所以可以在block修改就是通过——指针。因为静态局部变量存在于内存数据段,不存在栈展开后非法访存的风险。见下一段。)
所以,对于auto类型的局部变量,不允许block进行修改是合理的。

block为什么可以又可以修改静态变量和全局变量呢?

因为它们不存在栈展开后非法访存的风险。所以可以通过** 指针 ** 来传递静态变量的。
可以看出静态变量在main内实现block时,捕获的是count的地址&count。以及在__main_block_impl_0结构体中成员变量变成了指针类型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();
}

转换中间代码后,看到比以前多了很多东西。

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结构体成员变量count变为了__Block_byref_count_0 *类型。而相应的__main_block_func_0函数中count也变为了__Block_byref_count_0 *类型。

__Block_byref_count_0也是一个结构体。它的构成是:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding; // 指向另外一个变量,这儿的具体实现思路不太懂
 int __flags;
 int __size;
 int count;
};

** 但是问题照样存在,我们修改的变量count它是位于栈上的。若当block被回调执行时,栈早已被展开,早没count了。这该如何是好?**

上面的代码中我们可以注意到:__main_block_desc_0函数中多了两个成员函数,分别指向__main_block_copy_0__main_block_dispose_0函数。

当block从栈上被copy到堆上时,会调用__main_block_copy_0__block类型的成员变量count从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。


资料参考:

iOS中block实现的探究
C语言中闭包的探究及比较
C语言中闭包的探究及比较
对Objective-C中Block的追探
谈Objective-C block的实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容