在面试的时候,我们会碰到 Block
相关的问题:MRC 下为什么要用 copy 修饰? 为什么 ARC 下就不要这样处理呢?,所以打算把堆栈的内容和 Block
一起讲解。
堆栈存储域
- 堆 堆的空间需要程序员手动申请分配,它的内存是动态分配的,我们需要手动管理这部分内存,用完了要销毁掉,这部分内存属于
dirty memory
。简单点,就是 malloc 与 free。 - 栈 栈的内存是系统编译管理,跟程序员无关,它一般用来存储局部变量等。
在 OC
中我们创建一个对象,它最终会调用 calloc
方法在堆上创建内存。那有没有对象是创建在栈上的呢?先来看看栈对象的优缺点。
栈对象
优点
- 快速 在栈上创建对象很快。因为很多东西在编译时就确定了,在运行时,需要做的事情很少,相对而言,在堆上创建对象就比较耗时。
- 简单 它的声明周期是确定的,对象出栈以后就会被释放,不会发生内存泄露。
缺点
- 生命周期固定 一旦函数返回,它的栈帧就会被摧毁,那么对象也会被释放。如果这个对象通过方法调用需要传递到别的方法里面去,那被释放掉了,会产生野指针,此时
retain
也不起作用,它不适合引用计数内存管理方法。 - 大小固定 栈空间有限,创建时的长度是固定好的,没法扩展。
OC 中的栈对象
这就是我们今天的主角了: Block,它就是典型的栈对象。先从几段官方文档中获取相关信息
- “Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary.” 得知
Blocks
是对象。 - “As an optimization, block storage starts out on the stack—just like blocks themselves do. “ 和 “The initial allocation is done on the stack, but the runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1.” 得知 Block 对象与一般类的实例对象不同,它默认是分配在栈上的,一般类的实例对象则在堆上分配。
而在 ARC
下,Block
会被拷贝到堆中。所以下面的代码只有在 ARC
下才能正确执行。
1 | typedef NSInteger (^blk)(NSInteger); |
Block
那什么是 Block
呢?它是带有自动变量(局部变量)的匿名函数。它的语法格式为^ 返回值类型 参数列表 表达式
。它的申明可以查看 How Do I Declare A Block in Objective-C?。我们一步步来看看它的实现,先看一个简单的 Block
(这里会查看对应编译代码和汇编代码)。
简单 Block
1 | int main(int argc, char * argv[]) { |
代码分析
变换后的源码,Blocks
变成了 C 函数__main_block_func_0
,它有一个结构体指针的入参struct __main_block_impl_0 *
,相关注释已经写在编译后的代码里面了。相关调用方法为 blk() -> ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk)
,去掉相关转换部分为 (*blk->FuncPtr)(blk)
,这就是一个函数指针调用函数啊,而在初始化 &__main_block_impl_0
时赋值给 FuncPtr
就是 __main_block_func_0
,所以这里就是调用 __main_block_func_0
,Block
最终还是调用函数,打印 “Block”字符串。
捕获自动变量
1 | int main(int argc, char * argv[]) { |
代码分析
跟上面比,代码有点不同,__main_block_impl_0
多了一个成员变量 val
(用来存储 Block
里面所使用的变量)。在 __main_block_impl_0
初始化的时候,将外部的变量值 val
赋值给 __main_block_impl_0
的 val
(捕获自动变量的原理),然后调用 Block
的时候获取 __main_block_impl_0
的 val
再打印。
__block 修饰符
如果在Block
里面对所捕获的变量进行赋值时,编译时会报错,提示我们加上__block
修饰符。
1 | int main(int argc, char * argv[]) { |
记住,只是赋值才会有问题,调用变更对象的方法是没问题。例如,下面的代码就没问题。
1 | int main(int argc, char * argv[]) { |
__block
说明符类似于 static
、auto
和 register
说明符,它们用于指定将变量的值设置在哪个存储域。例如,auto
表示自动变量存储在栈中,static
表示作为静态变量存储在数据区中。
1 | int main(int argc, char * argv[]) { |
源码分析
原本的 __block
变量竟然变成了一个结构体 __Block_byref_val_0
实例。这里的赋值操作比较怪异,明明直接 val->val = 30
就行了,为什么还要 val->__forwarding->val) = 30
。这个 __forwarding
成员变量是用来指向堆上的结构体实例的,因为这里发生了拷贝行为,将 __block
变量从栈上拷贝到了堆上。Block 有三种类型,
- _NSConcreteStackBlock 该类的 Block 变量设置在栈上
- _NSConcreteGlobalBlock 该类的 Block 变量设置在数据区域(.data区)上
- _NSConcreteMallocBlock 该类的 Block 变量设置在有 malloc 函数分配的内存块(即堆)中
_NSConcreteStackBlock
类型如以下代码所示。
1 | dispatch_block_t blk = ^{ |
我们现在只有 _NSConcreteMallocBlock
没有看到了,先看一段代码。
1 | typedef NSInteger (^blk)(NSInteger); |
但是在终端用 clang 编译的时候会报错,
1 | /var/folders/hn/g9n1gw8d6p3655k44zsrnh_h0000gn/T/main-a7f5c2.mi:46504:12: error: returning block that lives on the local stack |
报错信息表示 func
函数返回的是在栈上的 Block
对象,函数返回后,它的作用域结束,所以栈上的 Block
也被废弃,这样提示没毛病啊,但是 Xcode
运行确正常啊,说明系统(ARC 情况下)帮我们拷贝到了堆上,因为你把main.m
文件编译的时候加上-fno-objc-arc
,它就会报错上面一样的错误。那来看看它的汇编代码 Xcode -> Product -> Perform Action -> Assemble"main.m"
,汇编代码如下
1 | .section __TEXT,__text,regular,pure_instructions |
我们看到了 _objc_retainBlock
和 _objc_autoreleaseReturnValue
,在 runtime
源码中,
1 | id objc_retainBlock(id x) { |
说 ARC
帮我们进行了拷贝处理,将 Block
从栈上拷贝到了推上,返回了 autorelease
对象,所以 Xcode
运行才会没毛病。在Objective-C Automatic Reference Counting (ARC) - Blocks
里面也有提及。
1 | With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call. |
现在,文章开头的问题解决了吧,但是,为什么我们现在声明 Block
属性的时候还是用 copy
修饰符呢?,算是为了解决历史遗留问题吧,给开发者提个醒吧。在Programming with Objective-C : Working with Blocks
苹果官方文档中,
1 | You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior。 |