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

前言

这里有关于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的数据结构定义如下(图片来自这里):

对应的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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具体的源码实现方式。该命令是

1
clang -rewrite-objc block.c

NSConcreteGlobalBlock 类型的block的实现

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

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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的文件,输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2();
return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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关键字:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
__block int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
i = 1023;
};
block1();
return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。

参考链接

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

谈Objective-C Block的实现的更多相关文章

  1. 浅谈Objective—C中的面向对象特性

    Objective-C世界中的面向对象程序设计 面向对象称程序设计可能是现在最常用的程序设计模式.如何开发实际的程序是存在两个派系的-- 面向对象语言--在过去的几十年中,很多的面向对象语言被发明出来 ...

  2. Objective C block背后的黑魔法

    前言 block在Objective C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用. "知其然而不知其所以然"是一件非常痛苦的事情,那 ...

  3. 浅谈objective—C管理内存

    这段时间被导师催着论文,调试各种BUg,也是醉了,发现很大程度上,内存出错,栈溢出,各种悲剧.那么今天就和大家一起对OC的内存管理来个探微吧.Objective-C使用一个保留计数记录了我们所创建的所 ...

  4. iOS应用架构谈(三):网络层设计方案(上)

    iOS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来讨论iOS应用架构中的种种问题,本文是其中的第三篇,主要讲网络层设计以及安全机制和优化方案. 前言 网络层在一个Ap ...

  5. iOS应用架构谈 网络层设计方案

    网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大.另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking也被广泛使用.其它的ASIHttpRequ ...

  6. [转] iOS应用架构谈 网络层设计方案

    原文地址:http://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.html iOS应用架构谈 开篇 iOS应用 ...

  7. iOS应用架构谈part3 网络层设计方案

    前言 网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大.另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking也被广泛使用.其它的ASIHttpR ...

  8. 唐巧的iOS技术博客选摘

    1. 那些被遗漏的objective-c保留字:http://blog.devtang.com/blog/2013/04/29/the-missing-objc-keywords/   2. 使用cr ...

  9. DataSet装换为泛型集合 222

    #region DataSet装换为泛型集合 /// <summary> /// 利用反射和泛型 /// </summary> /// <param name=" ...

  10. Automake

    Automake是用来根据Makefile.am生成Makefile.in的工具 标准Makefile目标 'make all' Build programs, libraries, document ...

随机推荐

  1. dede模板完全控制攻略

    1.使用php代码 @me代表了当前字段的值 {dede:tagname runphp='yes'} @me = "123456";//如果使用了自定义函数 则@me得到的是函数返 ...

  2. CentOS 6.5系统上安装SVN服务器端的方法及步骤

    VN其实就是Subversion,分为服务器端和客户端.之前在网上搜了很多方法,都有各种问题,经过自己搜集整理以及实际尝试,总算有个比较靠谱的方法.本文主要介绍CentOS 6.5系统上安装SVN服务 ...

  3. Unity3D的杂记

    刷新帧的不同控制函数 FixedUpdate 可以多次调用: 不饿能用于帧频很高的情况: Update 仅一次调用(每帧): LateUpdate 每帧调用一次: Corountine 用startC ...

  4. ABBYY PDF Transformer+从文件选项中创建PDF文档的教程

    可使用OCR文字识别软件ABBYY PDF Transformer+从Microsoft Word.Microsoft Excel.Microsoft PowerPoint.HTML.RTF.Micr ...

  5. 在同步中调用异步方法[.net 4.5]

    using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNet.Ident ...

  6. WebApiThrottle限流框架

    ASP.NET Web API Throttling handler is designed to control the rate of requests that clients can make ...

  7. HDFS 文件读写过程

    HDFS 文件读写过程 HDFS 文件读取剖析 客户端通过调用FileSystem对象的open()来读取希望打开的文件.对于HDFS来说,这个对象是分布式文件系统的一个实例. Distributed ...

  8. Eclipse换常用的快捷键

    还是喜欢ctrl+tab键来切换窗口,ctrl+f6实在不好使. 修改方法:在eclipse中Window -> Perferences -> General -> Keys -&g ...

  9. CentOS 7.0体验与之前版本的不同

    RHEL7和CentOS7出来有一段时间了,拿出点时间研究下,有几个地方跟6和5系列相比改变比较大,估计不少童鞋有点不太习惯.下面简要举例说明改变比较大的要点: 一.CentOS的Services使用 ...

  10. JS URL 使用base64加密与解密

    JS编码方式: <script type="text/javascript"> document.write(encodeURI("http://www.w3 ...