前言

这里 有关于 block 的 5 道测试题,建议你阅读本文之前先做一下测试。

先介绍一下什么是闭包。在 wikipedia 上,闭包的定义) 是:

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

翻译过来,闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

block 实际上就是 Objective-C 语言对于闭包的实现。
block 配合上 dispatch_queue,可以方便地实现简单的多线程编程和异步编程,关于这个,我之前写过一篇文章介绍:《使用 GCD》

本文主要介绍 Objective-C 语言的 block 在编译器中的实现方式。主要包括:

  1. block 的内部实现数据结构介绍
  2. block 的三种类型及其相关的内存管理方式
  3. block 如何通过 capture 变量来达到访问函数外的变量

实现方式

数据结构定义

block 的数据结构定义如下(图片来自 这里):

对应的结构体定义如下:

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
}; struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:

  1. isa 指针,所有对象都有该指针,用于实现对象相关的功能。
  2. flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
  3. reserved,保留变量。
  4. invoke,函数指针,指向具体的 block 实现的函数调用地址。
  5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
  6. variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

该数据结构和后面的 clang 分析出来的结构实际是一样的,不过仅是结构体的嵌套方式不一样。但这一点我一开始没有想明白,所以也给大家解释一下,如下 2 个结构体 SampleA 和 SampleB 在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。

struct SampleA {
int a;
int b;
int c;
}; struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};

在 Objective-C 语言中,一共有 3 种类型的 block:

  1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

我们在下面会分别来查看它们各自的实现方式上的差别。

研究工具:clang

为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是

clang -rewrite-objc block.c

NSConcreteGlobalBlock 类型的 block 的实现

我们先新建一个名为 block1.c 的源文件:

#include <stdio.h>

int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}

然后在命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现,我将 block1.cpp 中一些无关的代码去掉,将关键代码引用如下:

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("Hello, World!\n");
} 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 (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
return 0;
}

下面我们就具体看一下是如何实现的。__main_block_impl_0 就是该 block 的实现,从中我们可以看出:

  1. 一个 block 实际是一个对象,它主要由一个 isa 和 一个 impl 和 一个 descriptor 组成。
  2. 在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是为了实现对象的所有特性,在此我们就不展开讨论了。
  3. 由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型,具体可以看 《objective-c-blocks-quiz》 第二题的解释。
  4. impl 是实际的函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。
  5. descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。在该例子中我们还看不到相关 capture 的代码,后面将会看到。

NSConcreteStackBlock 类型的 block 的实现

我们另外新建一个名为 block2.c 的文件,输入以下内容:

#include <stdio.h>

int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2(); return 0;
}

用之前提到的 clang 工具,转换后的关键代码如下:

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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d\n", a);
} 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 a = 100;
void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2); return 0;
}

在本例中,我们可以看到:

  1. 本例中,isa 指向 _NSConcreteStackBlock,说明这是一个分配在栈上的实例。
  2. main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,被复制到main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a。
  3. main_block_impl_0 中由于增加了一个变量 a,所以结构体的大小变大了,该结构体大小被写在了main_block_desc_0 中。

我们修改上面的源码,在变量前面增加 __block 关键字:

#include <stdio.h>

int main()
{
__block int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
i = 1023;
};
block1();
return 0;
}

生成的关键代码如下,可以看到,差异相当大:

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 printf("%d\n", (i->__forwarding->i));
(i->__forwarding->i) = 1023;
} 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()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*block1)(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 *)block1)->FuncPtr)((__block_impl *)block1);
return 0;
}

从代码中我们可以看到:

  1. 源码中增加一个名为 __Block_byref_i_0 的结构体,用来保存我们要 capture 并且修改的变量 i。
  2. main_block_impl_0 中引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
  3. __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。
  4. 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。

NSConcreteMallocBlock 类型的 block 的实现

NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码 (来自 这里),可以看到,在第 8 步,目标的 block 类型被修改为 _NSConcreteMallocBlock。

static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; // 1
if (!arg) return NULL; // 2
aBlock = (struct Block_layout *)arg; // 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
} // 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
} // 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0; // 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1; // 8
result->isa = _NSConcreteMallocBlock; // 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
} return result;
}

变量的复制

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示(图片来自 这里):

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示(图片来自 这里):

LLVM 源码

在 LLVM 开源的关于 block 的实现源码,其内容也和我们用 clang 改写得到的内容相似,印证了我们对于 block 内部数据结构的推测。

ARC 对 block 类型的影响

在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <__NSMallocBlock__: 0x100109960>。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
@autoreleasepool {
int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
};
block1();
NSLog(@"%@", block1);
}
return 0;
}

我个人认为这么做的原因是,由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。

参考链接

1.http://blog.devtang.com/2013/07/28/a-look-inside-blocks/

2.http://www.jianshu.com/p/51d04b7639f1

3.http://my.oschina.net/leejan97/blog/268536

希望本文能加深你对于 block 的理解。我在学习中,查阅了以下文章,一并分享给大家。祝大家玩得开心~

Block 实现 浅析的更多相关文章

  1. iOS开发——项目实战总结&Block使用注意点浅析

    Block使用注意点浅析 1.在使用block前需要对block指针做判空处理. 不判空直接使用,一旦指针为空直接产生崩溃. if (!self.isOnlyNet) { if (succBlock ...

  2. 浅析Block闭包

    浅析Block闭包 简单来说,block就是将函数及其上下文封装起来的对象,从功能上可以把它看作是C++中的匿名函数,也可称之为块. Block类型写法: 返回值+(^块名)+(参数)= ^(参数){ ...

  3. iOS Block浅析

    Block 的使用有两种:1.独立Block .2.内联Block .   <一>独立Block 使用方式   一.定义一个Block Object,并调用.   1.定义   // 定义 ...

  4. ruby中Block, Proc 和 Lambda 浅析

    Block 与Proc的区别: Block是代码块,Proc是对象: 参数列表中最多只能有一个Block, 但是可以有多个Proc或Lambda; Block可以看成是Proc的一个类实例. Proc ...

  5. block 浅析

    最近讲了一个关于block的例子 block 可以作为一个参数 进行传递 需要注意的地方是 :block 虽然作为一个参数 但是在函数方法执行的时候 block 是不会在定义它的地方调用 除非你在后边 ...

  6. Block浅析一

    1.在Block结构体中含有isa指针,这就证明了Block其实就是对象,并具有一般对象的所有功能. 2.Block是OC中的一种数据类型,在iOS开发中被广泛使用. 3.block的应用 (1)遍历 ...

  7. 【转】Python yield 使用浅析

    转载地址: www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/ Python yield 使用浅析 初学 Python 的开发者经 ...

  8. 浅析I/O模型及其设计模式

    前言 I/O在软件开发中的重要性无需多言,无论是在操作系统.网络协议.DBMS这种底层支撑软件还是在移动APP,大型网站服务器等应用软件的开发中都是最核心最重要的部分.特别是现在软件服务使用量和数据量 ...

  9. Python yield 使用浅析(转)

    Python yield 使用浅析 初学 Python 的开发者经常会发现很多 Python 函数中用到了 yield 关键字,然而,带有 yield 的函数执行流程却和普通函数不一样,yield 到 ...

随机推荐

  1. Dot Product

    These are vectors: They can be multiplied using the "Dot Product" (also see Cross Product) ...

  2. ios 字符串的操作汇总

    //将NSData转化为NSString        NSString* str = [[NSString alloc] initWithData:response encoding:NSUTF8S ...

  3. 酷比魔方 iwork10 旗舰版

    平板电脑自带的win10的一键恢复不是一般的慢,简直慢出个性了,历经5个小时...必须是个有耐心的人才敢一键恢复,不然强制关机就变砖了...

  4. 转载-ACPI的知识

    ACPI – the Advanced Configuration & Power Interface. ACPI是OS,BIOS和硬件之间的抽象层.它允许OS和平台独立的发展,比如新的OS可 ...

  5. JPA 系列教程19-jpa-uuid主键生成策略

    ddl语句 CREATE TABLE `t_user` ( `id` varchar(32) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY K ...

  6. AutoTile 自动拼接 番外篇(自动融合技术)

    http://pan.baidu.com/s/1dDQyfSl 密码:ttud 先睹为快吧. 之后 还差一个 智能替换 技术.

  7. maven依赖本地宝

    http://www.mamicode.com/info-detail-169419.html 引用本地的jar包

  8. 第五节 面向连接传输:TCP

    第五节 面向连接传输:TCP   TCP概述RFCs:793,1122,1323,2018,2581   点对点:   一个发送方,一个接收方   可靠,按序的字节流:   无“报文边界”,无结构但有 ...

  9. hdu_3555_Bomb(数位DP)

    题目连接:hdu_3555_Bomb 题意:和2089一样,只是数据大了点,不过道理是一样的 #include<cstdio> #include<cstring> #defin ...

  10. Memcached缓存

    Memcached是"分布式"的内存对象缓存系统,那么不需要"分布"的.不需要共享的或者干脆规模小到只有一台服务器的应用,Memcached不会带来任何好处,相 ...