voidtestBlock(){int a = 1;void (^block)(void) = ^{// NSLog(@"Hello:: %ld", a);};block();}intmain(int argc, char * argv[]) {@autoreleasepool {testBlock();}}
为了研究它的内部结构,可以将此代码保存为一个.c文件,例如test.c,通过运行clang -rewrite-objc test.c命令, 可以在同目录下生成一个test.cpp文件。 (在ios项目中的main.m文件,可以通过命令行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m编译为一个c++文件,不过这个文件会更大一些,可以滚动到文件的最下方找到main方法之前的那些代码即可。)
生成的代码如下:

重点关注main函数上边的定义,从上面110行,可以了解到block中编译后会的结构体中可以看到:
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
impl.isa: 它的内部实际有一个isa指针, 并且是一个stackBlock(栈类型的block),后面会介绍到block的各个类型
impl.FuncPtr: 它是一个函数指针,指向的是block要执行的代码封装成了一个函数,然后用这个指针指向这个函数。一句话理解: FuncPtr指向的就是block体中的内容;
impl.Desc: 是block占用的内存大小;
impl.Flags: block的负载信息(引用计数,以及类型信息);

这个结构体中,包含2个成员变量:
reserved: 这里是0,它表示Block版本升级所需的预留空间;
Block_size: Block的大小,也就是图中的(sizeof(struct __blockTest_block_impl_0))
当block中有定义使用外部变量时,也会在block体新增一个同名的变量,
例如block体中有使用外部的a, 那么block的struct结构中也会多一个a的变量;这个是block自动捕获的。
全局block __NSGlobalBlock__
如果block中没有访问普通局部变量,那么这个block就是__NSGlobalBlock__,全局block类型的内存存储在数据区的全局静态区(在上节内存分布中已经介绍过各个区的类型,可自行查看)。
注意: 全局变量和全局静态变量,在任何地方都可以被访问,因此几遍在block中有调用到该类型的变量,那么也不会被捕获, 因为该变量可以直接被访问。
栈block __NSStackBlock__
如果block中访问了普通的局部变量,那它就是一个__NSStackBlock__,它在内存中存储在栈区, 栈区的block是由系统管理释放的,所以在调用栈block时要注意,要确保它没有被释放。
对栈block进行copy操作,会从这个block从栈copy到堆上。
在MRC下,默认创建的block是栈block,而ARC下默认创建的block是堆block,它会帮我们自动从栈block copy到堆block, 如果想在ARC下创建栈block,可以在block的定义上增加一个__weak的修饰符即可。
__NSStackBlock__继承链:__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject
堆block __NSMallocBlock__
当block发生了copy动作,也就是从栈copy到了堆, 那么这个block就是__NSMallocBlock__类型, 它存储在堆区。
__NSMallocBlock__继承链:__NSMallocBlock__ : __NSMallocBlock
: NSBlock : NSObject
在ARC环境下, 编译器会根据情况,自动将栈上的block复制到堆上, 常见有4种情况:
block作为方法的返回值时
typedef void (^TestBlock)(void);- (TestBlock)createTestBlock{NSInteger a = 1;return ^{NSLog(@"******%ld",a);};}block常见的情况(默认会赋值给强指针):
- (void)testBlock{NSInteger a = 1;void (^testMyBlock)(void)=^{NSLog(@"******%ld",a);};testMyBlock();}当block作为参数传递给coacoaAPI时
[UIView animateWithDuration:1.0f animations:^{}];当block作为GCD的api的参数时
dispatch_async(dispatch_get_main_queue(), ^{});
在ARC下我们通常在声明一个block的属性时, 建议使用copy关键字,这里如果使用copy或者strong关键字,其实都会将栈区block复制到堆上,但是MRC下,需要写出copy关键字。
@property (nonatomic, strong) void (^testBlock)(void);@property (nonatomic, copy) void (^testBlock)(void);
上面已经提到了3种类型的block的存储区间,我们知道block变量默认值copy, 一旦被block捕获后,外部变量修改值,不会对block内部变量造成任何影响, 如果想要block外部和内部同时都可以修改时,需要__block变量来实现。
例如: 下面这段代码,我们知道block中是不可以直接对a进行修改的,
- (void)test{int a = 1;void (^testblock)(void) = ^{a = 2;};}
因为a是局部变量,它的作用域和生命周期仅仅在test方法里边, 而Block题中会捕获这个变量a,在testblock中会声明一个同名的a变量,且值=1,我们知道Block会将大括号的代码封装为一个函数并由impl.FuncPtr指向,如果在Block中访问变量a就相当于是,第二个函数访问test函数中的局部变量,这种操作肯定是不允许的,所以程序就会报错。
那么如果想在内部修改a变量的值, 上面也提到了比如将a定义为
static int a = 1;这种局部变量虽然在test方法里面, 在Block中实际是一次引用传递,所以Block中就可以捕获a的地址,并且修改。 但是这种方式并不被推荐, 由于静态局部变量在程序运行中是不会被释放的, 所以还是应该要尽可能的少用, 第二种方式就是通过__block的方式。
- (void)test1{__block int a = 1;void (^testblock)(void) = ^{age = 2;};testblock();NSLog(@"%d",a);}
上面已经介绍过,它通过clang可以查看到它真正编译后的结构体是:
struct __main_testblock_impl_0 {struct __block_impl impl;struct __main_testblock_desc_0* Desc;__Block_byref_a_0 *a;};struct __Block_byref_a_0 {void *__isa; // isa指针__Block_byref_a_0 *__forwarding; // 如果这block是在堆上那么这个指针就是指向它自己,如果这个block是在栈上,那这个指针是指向它拷贝到堆上后的那个blockint __flags;int __size; // 结构体大小int a; // 真正捕获到的a};
可以知道a用__block修饰后, 在Block结构体中会变成__Block_byref_a_0 *a结构体;这个结构体中有一个成员变量a, 这个才是真正捕获到的外部变量a, 实际外部变量的a的地址指向的也是这里的地址,所以不管是外部还是block里边修改a,他们都是通过同一个地址进行的修改,
当变量a被__block修饰后,它就不再是test方法内部的局部变量了,而是被包装为了一个对象(这里描述的对象:__Block_byref_a_0 *a结构体,因为它内部有一个成员变量isa指针), a就存储在这个之中。
在内部实现的核心上, 究竟是如何实现的变量可以同时被访问呢?
那么在内存方面当Block从栈复制到堆,__block的也会跟随变化:

在栈Block时,__block的存储也在栈上,__block变量被栈上的Block持有。
在堆Block时(copy动作),这时会调用Block内部的copy函数,copy函数内部就会调用底层的_Block_object_assign函数, 此时__block变量也会copy到堆上,同样,新的堆区__block变量也会被堆区Block所持有。
当堆区的Block被释放时, 会调用Block内部的dispose,它内部会调用_Block_object_dispose函数,堆上的__block变量被释放。

栈区上比如有多个block同时放问__block变量时, 这个__block变量是被多个Block所持有的。
例如图上的Block1在copy到堆区时,__block变量也会被复制到堆区,并被堆区的Block1所持有, 栈区的Block2仍然持有栈区的__block变量。原栈上的__block变量里的__forwarding变量指向copy到堆上的__block变量。
如果Block2从栈区copy到堆区, 同样堆上的__block变量也会堆区Block1和Block2所同时持有,__block变量的引用计数会+1。
当堆区Block都释放后, __block变量的引用计数=0,这时通过调用
_Block_object_dispose函数,堆上的__block变量被释放。
上面介绍有一个很关键的点,就是__forwarding的变化, 这个是技术上实现堆区,栈区都能共同访问同一个变量的关键,流程图如下:

结论: __forwarding设计的目的就是保证在栈上和在堆上都能正确的访问到变量。
整理一下知识点:
__block不管是修饰基本数据类型还是修饰引用对象数据类型,底层都是将它包装成了一个对象(__Block_byref_a_0 *a结构体的对象), 我们比如叫这个对象起名为__blockObjc, 在Block结构体中有这个对象__blockObjc ,在block内部的内存管理上:
栈Block, block内部并不会对__blockObjc产生强引用。
堆Block, block内部会对__blockObjc产生强引用,并且在堆block释放时,通过内部的
dispose函数,dispose函数内部又会调用_Block_object_dispose函数来释放__blockObjc
后续
本篇重点介绍了Block的本质,包括对block的结构体的拆解还有内存管理都有了详细说明, 所以我们就知道了,为什么在一个类中定义的Block,如果是copy关键字修饰, 那么self会强持有这个Block。而如果Block中一旦有使用self或self的成员变量, 那么self也就会被Block所捕获, 并生成内部的一个强引用的self的变量, 我们知道Block本质上还是oc对象,这样也就形成了对象与对象之间强持有的关系。 所以这也就为什么一定要在block之前先进行__weak关键字修饰的根本原因。
好了,今天就先介绍这么多, 后续再结合GCD的方式,对block易出错的点进行一下介绍。
夜雨聆风