block 本质是一个OC对象,也存在 isa 指针。或者说Block 是封装了函数调用和函数调用环境的OC对象。
编写一段最简单的OC代码顶一个block,代码如:
我们打开编译生成的main.cpp代码,会发现上述代码被转化为如下:
包含isa指针说明,block本质上也是一个OC对象,FuncPtr 指向block所封装的代码块地址,等执行block时会通过FuncPtr寻找将要执行的代码块,并且调用。
如果我们将main函数改为:
我们可以看到,较之前,__main_block_impl_0
结构体新增一个 int 类型变量abc,用于存储所引用的外部变量的值。因为是值存储,所以在block生成之后,无论外部变量做何更改,abc依然是之前所定义的值。
因为我们所定义的外部变量 abc 之前没有任何修饰符,也就是默认的auto变量,此时block是值捕获。如果将外部变量声明为 static 类型再观察底层实现。
转化为c++底层实现为:
使用static 修饰的变量在block内部为指针传递,block直接捕获外部变量的内存地址,此时若外部变量在block声明之后修改,block内部也会同步进行修改。
如果使用全局变量,block不会捕获。因为声明全局变量的类型会在程序的整个声明周期都不会被释放,所以在使用block时,直接会去访问全局变量的值。所以捕获就没有意义了,感兴趣的可以自行查看底层实现。
当我们声明一个block 并且打印他的继承链我们可以看到:
从而也进一步证明了block 本质上为 OC对象。并且,在不引用外部变量的情况下,block为 NSGlobalBlock
类型。
我们定义三个不同的block,分别打印他们的实际类型:
他们在内存中位置分别:
那他们是如何区分的呢?可以使用如下表格来说明:
NSStackBlock 执行 copy 后会将栈区的block 复制到堆区,便于程序员管理,那其他类型的block执行 copy 会有什么变化呢?如下表所示:
4. ARC 下某些情况下系统会对 Block 自动执行一次 copy 操作,将 Block 从栈区转移到堆区
如果此代码在MRC 环境下,会崩溃。Block访问的变量已被释放。 如果在ARC环境下,在参数的返回值为block时,系统会对block自动执行一次 copy 操作,使其变为 NSMallocBlock 类型。
2.当Block 被强指针引用时会自动执行copy操作
所以在 MAC 环境下的block属性必须使用 copy 修饰,而ARC环境下的block属性即可使用 strong 修饰,也可以使用 copy 修饰,两者都会对block自动执行copy操作,故无任何区别。
会发现当 函数体内 大括号执行完毕后 XWPerson 即被释放,此时的block 是 栈类型的Block 即 __NSStackBlock__
. 存储在栈区的block即便引用了对象,也会跟随大括号一并释放。
为什么堆 block 会对外部对象强引用呢?
方法。最终调用的 _Block_object_assign
方法会对block引入的对象 person 进行引用计数操作,当所引入的对象使用 strong 修饰则使其引用计数加1,若使用weak修饰则引用计数不变。
方法,将block内部引用的对象成员引用计数减1,如果此时外部对象使用strong 修饰,引用计数在copy加1后 此时再减1.依然会强引用外部对象,不会释放,如果使用weak修饰,此时因为自身以及被释放,所以不会再持有所引用外部对象,然而此时所引用外部对象是否会被释放取决于它的引用计数是否为 0。
6. block 内部修改外部变量的值。
我们知道,如果block 内部捕获的外部变量为 auto 类型,在block 内部生成的是该变量的值类型变量,无法通过block内部的值修改外部变量。
如果想在block内部修改外部变量的值有几种方法?
使用 static 修饰的变量block内部会直接获取到变量的内存地址,可以直接修改。
若使用 static 变量修饰,该变量的生命周期就会无限延长,这不符合我们的设计思路,故我们可以使用 __block
来修饰外部变量,从而达到在block内部修改外部成员变量的目的。
那 __block
是如何实现此需求的呢?
此时在main函数内声明 __block
类型的变量会以此方式初始化:
其中 __forwarding
保存的原变量 a 的内存地址,size
为当前变量的内存大小,10 保存未原变量的值。
如此,我们在block 内部修改 原变量时:
直接取原变量的地址进行更改,从而实现在block内部更改外部变量。
对于block内部捕获的对象类型的auto变量和__block修饰的变量。如果block在栈区,不会对他们进行内存管理,即不会强引用外部变量
如果block被复制到堆区,则会调用内部 copy 函数对外部 __block 修饰的变量和对象类型的auto变量进行内存管理。
当block从内存中移除时,同样也会调用dispose函数对所引用的外部变量进行释放。
使用 block 很容易形成循环引用,如果一个类中定义的block内部引用了该类的外部属性,包括 类本身的 self, 均会导致 self 强引用 block,block 也强引用 self。导致self不会被释放。如下代码就会造成循环引用:
产生循环引用的本质原因是,在block内部实现里,会将self 捕获到block内部,并且strong 强引用。如下代码所示:
9. 避免产生循环引用
1. (ARC 环境下) __weak : 弱引用对象,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。
2. (ARC / MRC 环境下) __unsafe_unretained : 弱引用对象,指向的对象销毁时,不会自动将指针置为nil。再次引用该对象时可能会产生访问僵尸对象的错误,产生崩溃,故不建议使用!
3. (ARC / MRC 环境下) __block : 使用__block 修饰对象. 在ARC环境下-前提是一定要调用此block,并且要在block内部将所引用的外部变量手动置nil。因为 MRC 环境下,引用__block 修饰的对象不会使其引用计数加1,所以不需要手动置nil,也不是必需要使用block。