iOS中的block类似比如js中的闭包,叫做“带自动变量的匿名函数”,而实际上它更多地体现的是函数式编程的概念,在开启了clang 的 “-rewrite-objc”选项之后,可以发现block其实是作为 C++对象来实现的
Block编译后的C++代码
上述示例代码第二行 声明并定义了block 即blk
第3行以blk自身(作为__block_impl对象)作为参数调用实际的执行函数 __main_block_func_0
对作用域中自动变量的处理
处理方法:如果block中有取用自动变量的话,会在__main_block_impl_0中添加相应的成员变量以存储这些变量值
而对于需要修改自动变量的场景,需要为自动变量添加__block说明符,比如 __block int val = 10,所有block变量对应的C++对象为__Block_byref_val_0:
__forwarding会赋值为对象自身的地址&val(注意,如果此结构体实例还在栈上则为栈上的地址,若在堆中,则为_forwarding会被赋值为此结构体实例在堆中的地址,所以无论_block变量配置在栈上还是在堆上,都能够正确地访问到到该变量(通过_forwarding->val))
block所定义的作用域中如果 val 赋值10则__Block_byref_val_0.val 会被赋值为10,其他自动变量亦会以此种形式保存在__Block_byref_val_0中
Block及__block变量 超过变量作用域 可存在的原因是什么呢?
原因归根到底是: Blocks提供了将Block和__block变量从栈上复制到堆上的方法来应对
ARC有效时,大多数情况编译器会自动生成将Block从栈上复制到堆上的代码,复制到堆上的Block将_NSConcreteMallocBlock类对象写进Block变量的isa, 哪些情况需要手动将block从栈上复制到堆上呢,比如栈上生成的block并没有引用其生成作用域中的变量,没有调用copy等情况,需要手动调用block的copy方法以让编译器将block从栈上复制到堆上。
若在block中使用了的_Block变量会在Block从栈中复制到堆上的同时配置到堆上,相应地,当Block被 释放时,相应的_block变量所在的结构体实例也被释放(just the reference count being decreased, you know, maybe someone else is using it)
下面我们再来看一个未使用Block变量的例子,let's rock the cocoa
在作用域结束时, array 本来应该被废弃掉的,局部作用域中其强引用会失效,但输出结果却是: array count = 1 array count = 2 array count = 3
这段代码编译器转换过后的C++代码与之前的例子不太一样:
OC高级编程里面说,一般而言,C语言结构体不能含有添加了__strong修饰符的变量,因为编译器无法判断何时进行初始化及废弃操作,不方便内存管理。但凡事都有例外,在Block用结构体中,OC运行库能够准确把握Block从栈上复制到堆上及从堆上被废弃的时机,所以在Block用结构体中可以附有__strong或__weak修饰符。
但自动变量array在其所在作用域结束之后是会被释放的,使Block对象在从栈上复制到堆上时会调用 __main_block_copy_0将array复制到成员变量array中,而其复制的起点则为__main_block_impl_0(.., id __strong _array...) :array(_array){..}; 即在初始化Block对象的时候__main_block_impl_0就已经将自动变量array的引用传递到了Block对象中。 __main_block_copy_0使用_Block_object_assign将对象赋值给Block用结构体的成员变量array中并持有该变量,其相当于retain实例方法。__main_block_dispose_0函数使用_Block_object_dispose函数释放成员变量array中的对象,相当于调用release实例方法。其实在使用__block变量的时候已经使用了这两种方法,只不过__block对象对应的类型为BLOCK_FIELD_IS_BYREF(另一种类型为对象:BLOCK_FIELD_IS_OBJECT)。
插播下声明,栈上的block复制到堆上的时机: 1 调用Block的copy实例方法 2 Block作为函数返回值返回时 3 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时 4 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时
所以,在上述源代码中不调用 copy方法时,会发生什么情况呢?答案是执行后,程序会强制结束,因为只有调用 _Block_copy函数才能持有截获的附有__strong修饰符的对象类型的自动变量值,不调用_Block_copy的情况下,自动变量会随作用域结束而被废弃。
附有 __strong修饰符的id或对象类型自动变量(同样适用于__strong修饰符修饰的__block id或对象变量)在Block函数从栈上复制到堆上时,会使用_Block_object_assign将自动变量对象赋值给Block用结构体中的成员变量,并持有此对象,并在Block销毁时由_Block_object_dispose释放
上例已经讲述了在Block中使用附有 __strong修饰符的自动变量的情况(同样适用于__block说明符与__strong修饰符同时使用的情况),对于附有__weak修饰符的自动变量的情况(同样适用于__block说明符与__weak修饰符同时使用的情况),由于自动变量在作用域结束时,__weak变量会被赋值nil,所以会有一些奇特的现象。__block与__unsafe_unretained的组合与后者表现相似(__unsafe_unretained 只是对象的指针,但不作引用计数,有野指针的危险),而__block与__autoreleasing的组合会引发编译错误。
Block循环引用的问题可能发生在将self直接或间接(Block函数体中引用了成员变量)地引用传进Block函数体中的情况。一般的处理循环引用的方法是使用__weak变量(或者在可以确保的情况下使用__unsafe_unretained变量),也可以通过__block变量 在Block创建之初持有 self,但运行过程中将__block变量置nil来避免循环引用。
ARC无效时的,手动调用copy/release
ARC无效时,一般需要手动将Block从栈复制到堆:
void (^blk_on_heap)(void) = [blk_on_stack copy];
[blk_on_heap release];
[blk_on_heap retain]; //持有
而对栈上的block持有是不起作用的 [blk_on_stack retain];
c语言中的copy/release为 Block_copy和Block_release: void (^blk_on_heap)(void) = Block_copy(blk_on_stack); Block_release(blk_on_heap);
ARC无效时,__block说明符用来避免循环引用:因为当Block从栈复制到堆上时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain,反之则会被retain
Block无法捕获从外部传入其作用域中的引用
上述示例代码中标注的4行代码的执行结果是怎样呢( cppstruct.result == 2, num == 3),现在揭晓答案
#1 res != 2 #2 n !=3 #3 d != 3
#4 ss == "this is a string"
可以基本得出的结论是Block在method:num:作用域释放时,其会持有cppstruct,num,cd的引用(即其地址),而会持有s 的值的拷贝。
res,n 及 d 在赋值后的值有时候并不等于其所赋引用中持有的值,推断的话应该是各引用(cppstuct,num,cd)已经非法,而可否赋值成功跟对已释放的引用的非法访问有关