iOS中block实现的探究

时间:2022-01-27 17:07:14

[0. Brief introduction of block]

Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。

用*的话来说,Block是Apple Inc.为C、C++以及Objective-C加入的特性,使得这些语言能够用类lambda表达式的语法来创建闭包

用Apple文档的话来说,A block is an anonymous inline collection of code, and sometimes also called a "closure".

关于闭包,我认为阮一峰的一句话解释简洁明了:闭包就是可以读取其他函数内部变量的函数。

这个解释用到block来也非常恰当:一个函数里定义了个block,这个block能够訪问该函数的内部变量。

一个简单的Block示比例如以下:

int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; };

假设用Python的lambda表达式来写,能够写成例如以下形式:

f = lambda x, y : x if x > y else y

只是由于Python自身的语言特性,在def定义的函数体中,能够非常自然地再用def语句定义内嵌函数,由于这些函数本质上都是对象。

假设用BNF来表示block的上下文无关文法,大致例如以下:

block_expression  ::=  ^  block_declare  block_statement
block_declare ::= block_return_type block_argument_list
block_return_type ::= return_type | 空
block_argument_list ::= argument_list | 空

[1. Why block]

Block除了可以定义參数列表、返回类型外,还可以获取被定义时的词法范围内的状态(比方局部变量),而且在一定条件下(比方使用__block变量)可以改动这些状态。此外,这些可改动的状态在同样词法范围内的多个block之间是共享的,即便出了该词法范围(比方栈展开,出了作用域),仍可以继续共享或者改动这些状态。

通常来说,block都是一些简短代码片段的封装,适用作工作单元,通经常使用来做并发任务、遍历、以及回调。

比方我们能够在遍历NSArray时做一些事情:

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;

当中将stop设为YES,就跳出循环,不继续遍历了。

而在非常多框架中,block越来越常常被用作回调函数,代替传统的回调方式。

  • 用block作为回调函数,能够使得程序猿在写代码更顺畅,不用中途跑到还有一个地方写一个回调函数,有时还要考虑这个回调函数放在哪里比較合适。採用block,能够在调用函数时直接写兴许处理代码,将其作为參数传递过去,供其任务运行结束时回调。
  • 还有一个优点,就是採用block作为回调,能够直接訪问局部变量。比方我要在一批用户中改动一个用户的name,改动完毕后通过回调更新相应用户的单元格UI。这时候我须要知道相应用户单元格的index,假设採用传统回调方式,要嘛须要将index带过去,回调时再回传过来;要嘛通过外部作用域记录当前操作单元格的index(这限制了一次仅仅能改动一个用户的name);要嘛遍历找到相应用户。而使用block,则能够直接訪问单元格的index。

这份文档中提到block的几种适用场合:

  • 任务完毕时回调处理
  • 消息监听回调处理
  • 错误回调处理
  • 枚举回调
  • 视图动画、变换
  • 排序

[2. About __block_impl]

Clang提供了中间代码展示的选项供我们进一步了解block的原理。

以一段非常easy的代码为例:

iOS中block实现的探究

使用-rewrite-objc选项编译:

iOS中block实现的探究

得到一份block0.cpp文件,在这份文件里能够看到例如以下代码片段:

iOS中block实现的探究

从命名能够看出这是block的实现,而且得知block在Clang编译器前端得到实现,能够生成C中间代码。非常多语言都能够仅仅实现编译器前端,生成C中间代码,然后利用现有的非常多C编译器后端。

从结构体的成员能够看出,Flags、Reserved能够先略过,isa指针表明了block能够是一个NSObject,而FuncPtr指针显然是block相应的函数指针。

由此,揭开了block的神奇面纱。

只是,block相关的变量放哪里呢?上面提到block能够capture词法范围内(或者说是外层上下文、作用域)的状态,即便是出了该范围,仍然能够改动这些状态。这是怎样做到的呢?

[3. Implementation of a simple block]

先看一个仅仅输出一句话的block是怎么样的。

iOS中block实现的探究

生成中间代码,得到片段例如以下:

iOS中block实现的探究

首先出现的结构体就是__main_block_impl_0,能够看出是依据所在函数(main函数)以及出现序列(第0个)进行命名的。假设是全局block,就依据变量名和出现序列进行命名。__main_block_impl_0中包括了两个成员变量和一个构造函数,成员变量各自是__block_impl结构体和描写叙述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。

接着出现的是__main_block_func_0函数,即block相应的函数体。该函数接受一个__cself參数,即相应的block自身。

再以下是__main_block_desc_0结构体,当中比較有价值的信息是block大小。

最后就是main函数中对block的创建和调用,能够看出运行block就是调用一个以block自身作为參数的函数,这个函数相应着block的运行体。

这里,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。相同地,还有_NSConcreteMallocBlock_NSConcreteGlobalBlock

因为block也是NSObject,我们能够对其进行retain操作。只是在将block作为回调函数传递给底层框架时,底层框架须要对其copy一份。例如说,假设将回调block作为属性,不能用retain,而要用copy。我们一般会将block写在栈中,而须要回调时,往往回调block已经不在栈中了,使用copy属性能够将block放到堆中。或者使用Block_copy()和Block_release()。

[4. Capture local variable]

再看一个訪问局部变量的block是如何的。

iOS中block实现的探究

生成中间代码,得到片段例如以下:

iOS中block实现的探究

能够看出这次的block结构体__main_block_impl_0多了个成员变量i,用来存储使用到的局部变量i(值为1024);而且此时能够看到__cself參数的作用,类似C++中的this和Objective-C的self。

假设我们尝试改动局部变量i,则会得到例如以下错误:

iOS中block实现的探究

错误信息非常具体,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。

为什么不能给变量i赋值呢?

由于main函数中的局部变量i和函数__main_block_func_0不在同一个作用域中,调用过程中仅仅是进行了值传递。当然,在上面代码中,我们能够通过指针来实现局部变量的改动。只是这是由于在调用__main_block_func_0时,main函数栈还没展开完毕,变量i还在栈中。可是在非常多情况下,block是作为參数传递以供兴许回调运行的。通常在这些情况下,block被运行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针訪问就⋯⋯。

所以,对于auto类型的局部变量,不同意block进行改动是合理的。

[5. Modify static local variable]

于是我们也能够判断出,静态局部变量是怎样在block运行体中被改动的——通过指针。

由于静态局部变量存在于数据段中,不存在栈展开后非法訪存的风险。

iOS中block实现的探究

上面中间代码片段与前一个片段的区别主要在于main函数里传递的是i的地址(&i,以及__main_block_impl_0结构体中成员i变成指针类型(int
*
)。

然后在运行block时,通过指针改动值。

当然,全局变量、静态全局变量都能够在block运行体内被改动。更准确地讲,block能够改动它被调用(这里是__main_block_func_0)时所处作用域内的变量。比方一个block作为成员变量时,它也能够訪问同一个对象里的其他成员变量。

[6. Implementation of __block variable]

那么,__block类型变量是怎样支持改动的呢?

iOS中block实现的探究

我们为int类型变量加上__block指示符,使得变量i能够在block函数体中被改动。

此时再看中间代码,会多出非常多信息。首先是__block变量相应的结构体:

iOS中block实现的探究

由第一个成员__isa指针也能够知道__Block_byref_i_0也能够是NSObject。

第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,仅仅能说有时候须要指向还有一个__Block_byref_i_0结构。

最后一个成员是目标存储变量i。

此时,__main_block_impl_0结构例如以下:

iOS中block实现的探究

__main_block_impl_0的成员变量i变成了__Block_byref_i_0 *类型。

相应的函数__main_block_func_0例如以下:

iOS中block实现的探究

亮点是__Block_byref_i_0指针类型变量i,通过其成员变量__forwarding指针来操作还有一个成员变量。 :-)

而main函数例如以下:

iOS中block实现的探究

通过这样看起来有点复杂的改变,我们能够改动变量i的值。可是问题相同存在:__Block_byref_i_0类型变量i仍然处于栈上,当block被回调运行时,变量i所在的栈已经被展开,怎么办?

在这样的关键时刻,__main_block_desc_0站出来了:

iOS中block实现的探究

此时,__main_block_desc_0多了两个成员函数:copy和dispose,分别指向__main_block_copy_0__main_block_dispose_0

当block从栈上被copy到堆上时,会调用__main_block_copy_0将__block类型的成员变量i从栈上拷贝到堆上;而当block被释放时,对应地会调用__main_block_dispose_0来释放__block类型的成员变量i。

一会在栈上,一会在堆上,那假设栈上和堆上同一时候对该变量进行操作,怎么办?

这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被拷贝到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。

/* ---------------------------------------------------------------------------------------------------- */

本来还想继续写下去,结果发现文章有点长了。先到此。

原文链接:http://blog.csdn.net/jasonblog/article/details/7756763

Jason Lee @ Hangzhou