转载自:http://mobile.51cto.com/hot-403931.htm,讲解的太犀利了,转载。
一、概述
Block是C级别的语法和运行时特性。Block比较类似C函数,但是Block比之C函数,其灵活性体现在栈内存、堆内存的引用,我们甚至可以将一个Block作为参数传给其他的函数或者Block。
二、热身
先看一个比较简单的Block例子:
- int multiplier = 7;
- int (^myBlock)(int) = ^(int num) {
- return num * multiplier;
- };
在这个例子中,myBlock是一个Block变量,它接受一个int类型的参数,返回一个int类型的值。是不是很像C函数?
来,让我们typedef一下
- typedef void (^BoolBlock)(BOOL);//一个只接受一个BOOL参数,没有返回值的block
- typedef int (^IntBlock)(void);//一个没有参数,返回int的block
- typedef BoolBlock (^HugeBlock)(IntBlock);//看看,这个HugeBlock的参数和返回值都是block
三、更详细的例子
注意:上面的typedef都还有效~
主动调用一下:
- - (void)someMethod
- {
- BoolBlock ablock = ^(BOOL bValue) {
- NSLog(@"Bool block!");
- };
- ablock();
- }
作为参数返回:
- typedef void (^BoolBlock)(BOOL);
- - (BoolBlock)foo()
- {
- BoolBlock ablock = ^(BOOL bValue) {
- NSLog(@"Bool block!");
- };
- return [[ablock copy] autorelease];//一定要copy,将其复制到堆上,更详细的原理,将在后续章节讲解
- }
类的一个成员:
- @interface OBJ1 : NSObject
- @property (nonatomic, copy)BoolBlock block;//理由同上啊,同学们
- @end
- OBJ1 *obj1 = ...
- obj1.block = ^(BOOL bValue) {
- NSLog(@"Bool block!");
- };
其他函数的参数:
- - (void)foo(BoolBlock block)
- {
- if (block) {
- block();
- }
- }
甚至其他block的参数:
- BoolBlock bBlock = ^(BOOL bV){if(Bv){/*do some thing*/}};
- HugeBlock hBlock = ^(BoolBlock bB) {bB();};
- hBolck(bBlock);
啊,全局变量!:
- static int(^maxIntBlock)(int, int) = ^(int a, int b){return a>b?a:b;};
- int main()
- {
- printf("%d\n", maxIntBlock(2,10));
- return 0;
- }
好了,你知道block大概能怎么用了。
四,特殊的标记,__block
如果要在block内修改block外声明的栈变量,那么一定要对该变量加__block标记:
- int main()
- {
- __block int i = 1024;
- BoolBlock bBlock = ^(BOOL bV) {
- if (bV) {
- i++;//如果没有__block标记,是无法通过编译的。
- }
- };
- }
好了,基础很快,更详细的内容将用来介绍深入的东西。
一、block放在哪里
我们针对不同情况来讨论block的存放位置:
1.栈和堆
以下情况中的block位于堆中:
- void foo()
- {
- __block int i = 1024;
- int j = 1;
- void (^blk)(void);
- void (^blkInHeap)(void);
- blk = ^{ printf("%d, %d\n", i, j);};//blk在栈里
- blkInHeap = Block_copy(blk);//blkInHeap在堆里
- }
- - (void)fooBar
- {
- _oi = 1;
- OBJ1* oj = self;
- void (^oblk)(void) = ^{ printf("%d\n", oj.oi);};
- void (^oblkInHeap)(void) = [oblk copy];//oblkInHeap在堆中
- }
2.全局区
以下情况中的block位于全局区:
- static int(^maxIntBlock)(int, int) = ^(int a, int b){return a>b?a:b;};
- - (void)fooBar
- {
- int(^maxIntBlockCopied)(int, int) =[maxIntBlock copy];
- }
- void foo()
- {
- int(^maxIntBlockCopied)(int, int) = Block_copy(maxIntBlock);
- }
需要注意的是,这里复制过后的block依旧位于全局区,实际上,复制操作是直接返回了原block对象。
二、block引用的变量在哪里
1.全局区
全局区的变量存储位置与block无关:
- static int gVar = 0;
- //__block static int gMVar = 1;
- void foo()
- {
- static int stackVar = 0;
- // __block static int stackMVar = 0;
- }
注意:static变量是不允许添加__block标记的
2.堆栈
此时,你可能会问,当函数foo返回后,栈上的j已经回收,那么blkInHeap怎么能继续使用它?这是因为没有__block标记的变量,会被当做实参传入block的底层实现函数中,当block中的代码被执行时,j已经不是原来的j了,所谓物是人非就是这样吧~
另外,如果使用到变量j的所有block都没有被复制至heap,那么这个变量j也不会被复制至heap。
因此,即使将j++这一句放到blk()这句之前,这段代码执行后,控制台打印结果也是:1024, 1。而不是1024, 2
三、其他特性
1.复制的行为
对block调用复制,有以下几种情况:
1.对全局区的block调用copy,会返回原指针,并且这期间不处理任何东西(至少目前的内部实现是这样);
2.对栈上的block调用copy,每次会返回新复制到堆上的block的指针,同时,所有__block变量都会被复制至堆一份(多次拷贝,只会生成一份)。
3.对已经位于heap上的block,再次调用copy,只会增加block的引用计数。
为什么我们不讨论retian的行为?原因是并没有Block_retain()这样的函数,而且objc里面的retain消息发送给block对象后,其内部实现是什么都不做。
2.objc类中的block复制
objc类实例方法中的block如果被复制至heap,那么当前实例会被增加引用计数,当这个block被释放时,此实例会被减少引用计数。
但如果这个block没有使用当前实例的任何成员,那么当前实例不会被增加引用计数。这也是很自然的道理,我既然没有用到这个instance的任何东西,那么我干嘛要retian它?
我们要注意的一点是,我看到网上有很多人说block引起了实例与block之间的循环引用(retain-cycle),并且给出解决方案:不直接使用self而先将self赋值给一个临时变量,然后再使用这个临时变量。
但是,大家注意,我们一定要为这个临时变量增加__block标记(多谢第三篇文章回帖网友的提醒)。
这一章我们以结果导向的方式来说明了各种情况下,block的内存问题,下一章,我将剖析运行时库的源码,从根源阐述block的行为。也就是过程导向的方式了。
lock到底是什么
我们使用clang的rewrite-objc命令来获取转码后的代码。
1、block的底层实现
我们来看看最简单的一个block:
这个block仅仅打印栈变量i和j的值,其被clang转码为:
首先是一个结构体__main_block_impl_0(从图二中的最后一行可以看到,block是一个指向__main_block_impl_0的指针,初始化后被类型强转为函数指针),其中包含的__block_impl是一个公共实现(学过c语言的同学都知道,__main_block_impl_0的这种写法表示其可以被类型强转为__block_impl类型):
- struct __block_impl {
- void *isa;
- int Flags;
- int Reserved;
- void *FuncPtr;
- };
isa指针说明block可以成为一个objc对象。
__main_block_impl_0的意思是main函数中的第0个block的implementation,这就是这个block的主体了。
这个结构体的构造函数的参数:
block实际执行代码所在的函数的指针,当block真正被执行时,实际上是调用了这个函数,其命名也是类似的方式。
block的描述结构体,注意这个结构体声明结束时就创建了一个唯一的desc,这个desc包含了block的大小,以及复制和析构block时需要额外调用的函数。
接下来是block所引用到的变量们
最后是一个标记值,内部实现需要用到的。(我用计算器看了一下,570425344这个值等于1<<29,即BLOCK_HAS_DESCRIPTOR这个枚举值)
所以,我们可以看到:
为什么上一篇我们说j已经不是原来的j了,因为j是作为参数传入了block的构造函数,进行了值复制。
带有__block标记的变量会被取地址来传入构造函数,为修改其值奠定了基础
接下来是block执行函数__main_block_func_0:
其唯一的参数是__main_block_impl_0的指针,我们看到printf语句的数据来源都取自__cself这个指针,比较有意思的是i的取值方式(带有__block标记的变量i被转码为一个结构体),先取__forward指针,再取i,这为将i复制到堆中奠定了基础。
再下来是预定义好的两个复制/释放辅助函数,其作用后面会讲到。
最后是block的描述信息结构体 __main_block_desc_0,其包含block的内存占用长度,已经复制/释放辅助函数的指针,其声明结束时,就创建了一个名为__main_block_desc_0_DATA的结构体,我们看它构造时传入的值,这个DATA结构体的作用就一目了然了:
长度用sizeof计算,辅助函数的指针分别为上面预定义的两个辅助函数。
注意,如果这个block没有使用到需要在block复制时进行copy/retian的变量,那么desc中不会有辅助函数
至此,一个block所有的部件我们都看齐全了,一个主体,一个真正的执行代码函数,一个描述信息(可能包含两个辅助函数)。
2、构造一个block
我们进入main函数:
图一中的第三行(block的声明),在图二中,转化为一个函数指针的声明,并且都没有被赋予初始值。
而图一中的最后一行(创建一个block),在图二中,成为了对__main_block_impl_0的构造函数的调用,传入的参数的意义上面我们已经讲过了。
所以构造一个block就是创建了__main_block_impl_0 这个c++类的实例。
3、调用一个block
调用一个block的写法很简单,与调用c语言函数的语法一样:
- blk();
其转码后的语句:
- ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
将blk这个函数指针类型强转为__block_impl类型,然后取其执行函数指针,然后将此指针类型强转为返回void*并接收一个__block_impl*的函数指针,最后调用这个函数,传入强转为__block_impl*类型的blk,
即调用了前述的函数__main_block_func_0
4、objective-c类成员函数中的block
源码如下:
- - (void)of1
- {
- OBJ1* oj = self;
- void (^oblk)(void) = ^{ printf("%d\n", oj.oi);};
- Block_copy(oblk);
- }
这里我故意将self赋值给oj这个变量,是为了验证前一章提出的一个结论:无法通过简单的间接引用self来防止retain循环,要避免循环,我们需要__block标记(多谢楼下网友的提醒)
转码如下:
- struct __OBJ1__of1_block_impl_0 {
- struct __block_impl impl;
- struct __OBJ1__of1_block_desc_0* Desc;
- OBJ1 *oj;
- __OBJ1__of1_block_impl_0(void *fp, struct __OBJ1__of1_block_desc_0 *desc, OBJ1 *_oj, int flags=0) : oj(_oj) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
- static void __OBJ1__of1_block_func_0(struct __OBJ1__of1_block_impl_0 *__cself) {
- OBJ1 *oj = __cself->oj; // bound by copy
- printf("%d\n", ((int (*)(id, SEL))(void *)objc_msgSend)((id)oj, sel_registerName("oi")));}
objc方法中的block与c中的block并无太多差别,只是一些标记值可能不同,为了标记其是objc方法中的blcok。
注意其构造函数的参数:OBJ1 *_oj
这个_oj在block复制到heap时,会被retain,而_oj与self根本就是相等的,所以,最终retain的就是self,所以如果当前实例持有了这个block,retain循环就形成了。
而一旦为其增加了__block标记:
- - (void)of1
- {
- __block OBJ1 *bSelf = self;
- ^{ printf("%d", bSelf.oi); };
- }其转码则变为:
- //增加了如下行
- struct __Block_byref_bSelf_0 {
- void *__isa;
- __Block_byref_bSelf_0 *__forwarding;
- int __flags;
- int __size;
- void (*__Block_byref_id_object_copy)(void*, void*);
- void (*__Block_byref_id_object_dispose)(void*);
- OBJ1 *bSelf;
- };
- static void __Block_byref_id_object_copy_131(void *dst, void *src) {
- _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
- }
- static void __Block_byref_id_object_dispose_131(void *src) {
- _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
- }
- //声明处变为
- __block __Block_byref_bSelf_0 bSelf = {(void*)0,(__Block_byref_bSelf_0 *)&bSelf, 33554432, sizeof(__Block_byref_bSelf_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, self};
clang为我们的bSelf结构体创建了自己的copy/dispose辅助函数,33554432(即1<<25 BLOCK_HAS_COPY_DISPOSE)这个值告诉系统,我们的bSelf结构体具有copy/dispose辅助函数。
而131这个参数(二进制1000 0011,即BLOCK_FIELD_IS_OBJECT (3) |BLOCK_BYREF_CALLER(128))
中的BLOCK_BYREF_CALLER在内部实现中告诉系统不要进行retain或者copy,
也就是说,在 __block bSelf 被复制至heap上时,系统会发现有辅助函数,而辅助函数调用后,并不retain或者copy 其结构体内的bSelf。
这样就避免了循环retain。
小结:
当我们创建一个block,并调用之,编译器为我们做的事情如下:
1.创建block所有的部件代码:一个主体,一个真正的执行代码函数,一个描述信息(可能包含两个辅助函数)。
2.将我们的创建代码转码为block_impl的构造语句。
3.将我们的执行语句转码为对block的执行函数的调用。
下一篇我们将剖析runtime.c的源码,并理解block的堆栈转换。