block是我在项目中比较常用,也比较爱用的一项技术。原因有两点:
- 使代码更紧凑,可读性更强
- 可capture上下文中的变量
当然,block使用不恰当的话,也会引起一些难以发现和追踪的问题:
- 循环引用,以及
- 会延长其capture的上下文中的变量的生命周期。
至于其他的,比如代码紧凑带来的代码增长,这些就见仁见智了。
一直以为针对block的使用和理解没什么问题,最近在看一篇技术博文时发现仍有些细节不太清楚,趁此机会学习和研究了block在编译器内部的实现方式,便有了此文。
block的内部实现数据结构
本质上block就是一段代码,再加上它所访问的上下文中的变量。clang中block的结构是类似于这样的:
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; } };
其中,impl代表的是block的实现。具体结构如下:
struct __block\_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
Desc是block的描述。具体结构如下:
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) }
a是block capture的上下文变量。
_main_block_impl_0()函数具体显示了block被创建时是如何capture 变量的。本例中为按值拷贝到_main_block_impl_0结构体中的那个变量a. 因此,在block内部修改是不能修改外部a的值的。
block如何存储
block分成三种类型:
- NSGlobalBlock, 既不在stack上也不在heap上,而是代码片段。 没有capture上下文变量的block为此类型
- NSStackBlock, 存储在stack上。 capture上下文变量的block默认为次类型。当对NSStackBlock发生Block_Copy时,拷贝将为NSMallocBlock类型。
- NSMallocBlock, 存储在heap上
顺便说下什么是stack和heap。可以这么理解,stack是内存中用于存储local variables的区域;每个线程有自己的一个stack。每当有函数调用时,就在调用线程的stack上分配一个栈帧,用来存储函数的局部数据;函数调用结束时弹栈。 heap则可以简单理解为内存中其他数据的存储区域。oc对象都是存放在heap上的(block除外,可以存放在stack上)。详见mikeash的博客
关于block的存储,有一个有意思的测试
block如何capture上下文中的变量
当capture的上下文变量用__block来修饰时, 表示它是可以被block修改的。从clang编译后的结果可以看出,block是复制变量的引用地址来使用的。对于未用block修饰的变量,block是复制变量的值来使用的(复制的是block被创建时变量的值)
另外,当block被拷贝时,它会retain所capture的对象。当用__block来修饰对象时(OC对象或者OC++对象),block将不再retain该对象(仅针对于MRC, 对于ARC不管用; ARC下应该使用__weak修饰符)
__block变量被保存到block storage中,可以被变量所在的lexical scope, 以及该scope内创建的所有block共享。block storage可能在stack上,也可能在heap上。
使用block需要注意的点
- arc下不存在NSStackBlock类型的block, 而是NSMallocBlock类型。这大概是因为arc下的变量默认为strong的。arc下使用block会简单很多。这个测试很可以说明这个问题
- 避免retain cycle。 MRC下可以使用__block来避免block对其capture的object variable进行retain操作; arc下可以使用__weak来避免
本文的参考:
- http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/
- https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502-CH1-SW1
- http://stackoverflow.com/questions/7853915/how-do-i-avoid-capturing-self-in-blocks-when-implementing-an-api?rq=1