使用 Block 的时候谨记以下几点:
1.Block类型:全局块(Global Block)和堆块(Heap Block),以及栈块(Stack Block)。
2.变量捕获: 默认无法修改变量,需要添加 __block
修饰符
3.避免循环引用。
推荐文章:
1.官方文档:
快速上手:Working with Blocks,进阶:Blocks Programming Topics
2.优秀博客:
Deep into Block: A look inside blocks: Episode 1, Episode 2, Episode 3
底层实现:谈 Objective-C Block 的实现 - By 唐巧
类型探讨:Objective-C Blocks Quiz
啥都有:对 Objective-C 中 Block 的追探
类型
之前看文档或是其他人写的文章都讲述了三种 Block 类型,但直到看到上面的测试,我才意识 Block 类型是如何决定的。经过一些实践,简单来说,在 Objective-C 中:
1.Block 中没有使用外围变量的话,在开启 ARC 的条件下,因为不需要依赖其他状态,其使用的内存区域在编译期就可以确定,将会被编译为全局块;而没有开启 ARC 时,总是被编译为栈块。
在唐巧的博客中使用 Clang 来研究 Block ,这里也学习了一下,但是基本上这些块都是被编译为栈块,按照唐巧的说法,在 ARC 下,是被编译为全局块的。
《Effective Objective-C 2.0》给出如下的例子:声明了一个 Block,但需要根据条件来选择合适的实现。
void (^block)();
if(条件 A) {block = ^{NSlog(@"Block A");}
}else{ block = ^{ NSLog(@"Block B"); } } block()
上面这段代码的问题在于,这两个块只在相应的 if 或 else 语句范围内有效,离开了相应的范围后,编译器有可能覆写块所在的内存。这样的代码可以编译,但在运行时可能出错。书中提出,这里可以使用 copy 操作将块拷贝到堆上,这样一来,块可以在定义它的范围外使用。正确写法如下:
void (^block)();
if(条件 A) {block = [^{NSlog(@"Block A");} copy]; }else{ block = [^{ NSLog(@"Block B"); } copy]; } block()
书中没有提及此处是否开启了 ARC;我将上述代码编写在 C 文件中,使用 Clang 将之转化为 cpp 实现后,发现这里是个栈块;而按照上一条的说法,开启 ARC 后,这里将会被编译为全局块。那么在开启 ARC 的条件下,这段代码是否有问题呢?答案是没有,因为原来的代码的问题在于块的内存在栈中,而开启 ARC 下,块编译为全局块,不存在这个问题。
在 Objective-C Blocks Quiz 的 Example C中,提到:
That’s correct. Since the block doesn’t capture any variables in its closure, it doesn’t need any state set up at runtime. it gets compiled as an NSGlobalBlock. It’s neither on the stack nor the heap, but part of the code segment, like any C function. This works both with and without ARC.
2.使用了外围变量的话,若开启了 ARC,则只会被编译为堆块,内存是分配在堆上的;若没有开启 ARC,函数中定义的 Block 将会编译为栈块(如今除了旧项目很少有不开启 ARC 的吧)。
总结下:开启 ARC 的条件下,将不会有栈块,这样可以省去不少麻烦,但是 Objective-C Blocks Quiz 的最后也提到LLVM的一位维护者说:
We consider this to be a compiler bug, and it has been fixed for months in the open-source clang repository. What that means for any hypothetical future Xcode release, I cannot say. :)
保险点,开启 ARC。
生命周期
栈块,顾名思义,离开了定义它的函数范围就被收回了;堆块,就像普通的对象一样采用引用计数机制;全局块,在应用的整个生命周期都存在。
变量捕获 + 循环引用
在声明块的范围内,所有变量都可以被块捕获(就是可以使用)。默认情况下,不可以在块里修改外围的变量,因为块拷贝了一份变量到它的内存中,对于对象则是拷贝了对象的地址;若想修改,需要在外围变量前面添加修饰符 __block
。是否添加 __block
修饰符,源代码会有很大不同,可以在唐巧的博客里看到。另外,Block 最著名的问题就是循环引用,就是由于互相保持着对方的引用,所以 ARC 拿这俩没办法,将会一直存在;复杂一点的,多个对象对其他对象的引用形成了一个圈,ARC 也是没办法。一般的解决办法是,在形成的引用圈的一处使用弱引用,这样就有机会打破强引用圈。
常见的循环引用陷阱是在类中定义了块变量,然后在块中使用了类实例的属性。而在访问属性的同时,块实质上隐式地捕获了当前实例,这样一来就造成了循环引用。
在 Objective-C 中,解决方法通常如下:
__weak myClass *weakSelf = self;
self.block = ^{__strong myClass *strongSelf = weakSelf;//防止当前 self 为空 if(strongSelf){ [strongSelf doSomeThing]; } }
又或者使用 typeof
,不过这种高级技巧,可读性就不敢保证了。今天这条微博引起了一些讨论:
这个代码的关键在于 typeof(self)
,因为它在块的内部出现,那么块是否捕获了 self
呢?答案是不会,因为 typeof
是个编译符号,在编译期间起作用,而不是运行时,因此这么写不会造成循环引用的问题。
在 Swift 中,针对这个问题有了更加优雅的解决方案:捕获列表(Capture List)。在闭包参数前添加列表,从属关系以及捕获对象成对为一组值,多组值用「,」隔开。
无参数:
var someClosure: () -> Void = {[unowned self, weak delegate = self.delegate!] in //closure body }
有参数:
var someClosure: (Int, String) -> String = {[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) in //closure body }