谈Objective-C block的实现(转)
前言
这里有关于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在编译器中的实现方式。主要包括:
- block的内部实现数据结构介绍
- block的三种类型及其相关的内存管理方式
- block如何通过capture变量来达到访问函数外的变量
实现方式
数据结构定义
block的数据结构定义如下(图片来自这里):

对应的结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
通过该图,我们可以知道,一个block实例实际上由6部分构成:
- isa指针,所有对象都有该指针,用于实现对象相关的功能。
- flags,用于按bit位表示一些block的附加信息,本文后面介绍block copy的实现代码可以看到对该变量的使用。
- reserved,保留变量。
- invoke,函数指针,指向具体的block实现的函数调用地址。
- descriptor, 表示该block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。
- variables,capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
该数据结构和后面的clang分析出来的结构实际是一样的,不过仅是结构体的嵌套方式不一样。但这一点我一开始没有想明白,所以也给大家解释一下,如下2个结构体SampleA和SampleB在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在Objective-C语言中,一共有3种类型的block:
- _NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量。
- _NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
- _NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。
我们在下面会分别来查看它们各自的实现方式上的差别。
研究工具:clang
为了研究编译器是如何实现block的,我们需要使用clang。clang提供一个命令,可以将Objetive-C的源码改写成c语言的,借此可以研究block具体的源码实现方式。该命令是
1 |
|
NSConcreteGlobalBlock 类型的block的实现
我们先新建一个名为block1.c的源文件:
1 2 3 4 5 6 7 |
|
然后在命令行中输入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 |
|
下面我们就具体看一下是如何实现的。__main_block_impl_0就是该block的实现,从中我们可以看出:
- 一个block实际是一个对象,它主要由一个 isa 和 一个 impl 和 一个descriptor组成。
- 在本例中,isa指向 _NSConcreteGlobalBlock, 主要是为了实现对象的所有特性,在此我们就不展开讨论了。
- impl是实际的函数指针,本例中,它指向__main_block_func_0。这里的impl相当于之前提到的invoke变量,只是clang编译器对变量的命名不一样而已。
- 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 |
|
用之前提到的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 |
|
在本例中,我们可以看到:
- 本例中,isa指向_NSConcreteStackBlock,说明这是一个分配在栈上的实例。
- main_block_impl_0 中增加了一个变量a,在block中引用的变量a实际是在申明block时,被复制到main_block_impl_0结构体中的那个变量a。因为这样,我们就能理解,在block内部修改变量a的内容,不会影响外部的实际变量a。
- main_block_impl_0 中由于增加了一个变量a,所以结构体的大小变大了,该结构体大小被写在了main_block_desc_0中。
我们修改上面的源码,在变量前面增加__block关键字:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
生成的关键代码如下,可以看到,差异相当大:
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 |
|
从代码中我们可以看到:
- 源码中增加一个名为__Block_byref_i_0 的结构体,用来保存我们要capture并且修改的变量i。
- main_block_impl_0 中引用的是Block_byref_i_0的结构体指针,这样就可以达到修改外部变量的作用。
- __Block_byref_i_0结构体中带有isa,说明它也是一个对象。
- 我们需要负责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 |
|
变量的复制
对于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 |
|
我个人认为这么做的原因是,由于ARC已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。
转自唐巧的技术博客
参考链接
希望本文能加深你对于block的理解。我在学习中,查阅了以下文章,一并分享给大家。祝大家玩得开心~
- A look inside blocks: Episode 1
- A look inside blocks: Episode 2
- A look inside blocks: Episode 3
- 对Objective-C中Block的追探
- LLVM中block实现源码
- objective-c-blocks-quiz
- Blocks
谈Objective-C block的实现(转)的更多相关文章
- 浅谈Objective—C中的面向对象特性
Objective-C世界中的面向对象程序设计 面向对象称程序设计可能是现在最常用的程序设计模式.如何开发实际的程序是存在两个派系的-- 面向对象语言--在过去的几十年中,很多的面向对象语言被发明出来 ...
- Objective C block背后的黑魔法
前言 block在Objective C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用. "知其然而不知其所以然"是一件非常痛苦的事情,那 ...
- 浅谈objective—C管理内存
这段时间被导师催着论文,调试各种BUg,也是醉了,发现很大程度上,内存出错,栈溢出,各种悲剧.那么今天就和大家一起对OC的内存管理来个探微吧.Objective-C使用一个保留计数记录了我们所创建的所 ...
- iOS应用架构谈(三):网络层设计方案(上)
iOS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来讨论iOS应用架构中的种种问题,本文是其中的第三篇,主要讲网络层设计以及安全机制和优化方案. 前言 网络层在一个Ap ...
- iOS应用架构谈 网络层设计方案
网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大.另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking也被广泛使用.其它的ASIHttpRequ ...
- [转] iOS应用架构谈 网络层设计方案
原文地址:http://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.html iOS应用架构谈 开篇 iOS应用 ...
- iOS应用架构谈part3 网络层设计方案
前言 网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大.另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking也被广泛使用.其它的ASIHttpR ...
- 唐巧的iOS技术博客选摘
1. 那些被遗漏的objective-c保留字:http://blog.devtang.com/blog/2013/04/29/the-missing-objc-keywords/ 2. 使用cr ...
- DataSet装换为泛型集合 222
#region DataSet装换为泛型集合 /// <summary> /// 利用反射和泛型 /// </summary> /// <param name=" ...
- Automake
Automake是用来根据Makefile.am生成Makefile.in的工具 标准Makefile目标 'make all' Build programs, libraries, document ...
随机推荐
- list_for_each_entry()函数分析
list_for_each原型: #define list_for_each(pos, head) \ for (pos = (head)->next, prefetch(pos->nex ...
- Linux学习-Linux的账号与群组
使用者识别码: UID 与 GID Linux 主机并不会直接认识 你的"帐号名称"的,他仅认识 ID 啊 (ID 就是一组号码啦). 由于计算机仅认识 0 与 1,所 以主机对于 ...
- XP系统连接win10家庭版共享的打印机方法
1.高级共享设置.按照win7正常设置."家庭网络"公用网络”“工作网络”之类的注意根据当前配置设置! 2.由于控制面板无法开启Guest账户.需要用任务管理器,运行cmd(管理员 ...
- JSONP分享-- 在JavaScript中跨域请求
如果你正在开发一个现代的基于web的应用程序,那么你: 在客户端使用JavaScript. 需要集成那些没有完全在你控制之下的服务(或者那些来自不同的域). 在你的浏览器控制台中遇到过这个错误信息: ...
- 小甲鱼零基础入门PYTHON
000.愉快的开始 00:17:37 ☆ 001.我和Python的第一次亲密接触 00:13:26 ★ 002.用Python设计第一个游戏 00:24:00 ★ 003.小插曲之变量和字符 ...
- pip 设置国内源提高速度
临时使用: 可以在使用pip的时候加参数-i https://pypi.tuna.tsinghua.edu.cn/simple 例如:pip install -i https://pypi.tuna. ...
- cobbler常用目录/命令(三)
常用目录: /var/www/cobbler/ks_mirror/ cobbler distro文件目录 /var/lib/tftpboot/pxelinux.cfg/d ...
- Codeforces Round #473 (Div. 2)
A. Mahmoud and Ehab and the even-odd game time limit per test 1 second memory limit per test 256 meg ...
- Map容器——HashMap及常用API,及put,get方法解析,哈希码的产生和使用
Map接口 ① 映射(map)是一个存储键/值对的对象.给定一个键,可以查询到它的值,键和值都是对象; ② 键必须是唯一的,值可以重复; ③ 有些映射可以接收null键和null值,而有的 ...
- 【Mysql 优化 6】mysql优化的内容和思路
根据最近做mysql优化,以及参照的官方文档的一些知识点,总结一下,如何下手去优化mysql 数据库.PS:更多可能是我个人的笔记总结记录,仅供参考 一.优化的内容 可以优化的内容,从范围的大小,可以 ...