什么是Block
Block是带有自动变量(局部变量)的匿名函数。Block的内部数据结构如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
- isa指针:指向表明该block类型的类。
- flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
- reserved:保留变量,我的理解是表示block内部的变量数。
- invoke:函数指针,指向具体的block实现的函数调用地址。
- descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。
- variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
block类型
C/C++/OC编译的程序占用内存分布的结构如下:
- 栈区(stack):由系统自动分配,一般存放函数参数值、局部变量的值等。由编译器自动创建与释放。其操作方式后进先出、先进后出。
- 堆区(heap):一般由程序员申请并指明大小,最终也由程序员释放。如果程序员不释放,程序结束时可能会由OS回收。对于堆区的管理是采用链表式管理的,操作系统有一个记录空闲内存地址的链表,当接收到程序分配内存的申请时,操作系统就会遍历该链表,遍历到一个记录的内存地址大于申请内存的链表节点,并将该节点从该链表中删除,然后将该节点记录的内存地址分配给程序。
- 全局区/静态区:全局变量和静态变量存储在这个区域。程序结束后由系统释放。
- 程序代码区:主要存放函数体的二进制代码。
block按照不同的内存区域可以分为:
_NSConcreteStackBlock
,存放在栈区。只用到外部局部变量、成员属性变量,且没有强指针引用的block。
StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。_NSConcreteMallocBlock
,存放在堆区。有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制。_NSConcreteGlobalBlock
, 存放在全局/静态区。没有用到外界变量或只用到全局变量、静态变量的block,生命周期从创建到应用程序结束。
int globalNum = 3;
int main(int argc, const char * argv[]) {
int a = 3;
NSLog(@"this block is: %@",^{NSLog(@"I use nothing");});
NSLog(@"this block is: %@",[^{NSLog(@"I use ivar %d",a);} copy]);
NSLog(@"this block is: %@",^{NSLog(@"I use ivar %d",a);});
NSLog(@"this block is: %@",^{NSLog(@"%d",globalNum);});
return 0;
}
其输出结果如下:
2017-12-26 09:01:27.668046+0800 BlockDemo[98660:1165488] this block is: <__NSGlobalBlock__: 0x100001050>
2017-12-26 09:01:27.668246+0800 BlockDemo[98660:1165488] this block is: <__NSMallocBlock__: 0x100507f80>
2017-12-26 09:01:27.668293+0800 BlockDemo[98660:1165488] this block is: <__NSStackBlock__: 0x7fff5fbfefe0>
2017-12-26 09:01:27.668367+0800 BlockDemo[98660:1165488] this block is: <__NSGlobalBlock__: 0x1000010d0>
注意:在ARC的作用下,block类型通过=
进行传递时,会导致objc_retainBlock
->_Block_copy
->_Block_copy_internal
方法链,导致__NSStackBlock__
类型的block转换为__NSMallocBlock__
类型。
block的实现原理
用clang工具重写为c++代码来探究一下block的内部实现。
测试代码如下:
typedef int(^blk)(int);
int globalNum = 3;
int main(int argc, const char * argv[]) {
int a = 3;
__block int blockVar = 3;
static int staticNum = 3;
blk blk = ^(int count) {
NSLog(@"a = %d", a);
staticNum --;
globalNum --;
blockVar --;
return count * a * globalNum * staticNum;
};
a = 10;
NSLog(@"%d", blk(1));
return 0;
}
clang重写c++后的代码如下:
typedef int(*blk)(int);
int globalNum = 3;
struct __Block_byref_blockVar_0 {
void *__isa;
__Block_byref_blockVar_0 *__forwarding;
int __flags;
int __size;
int blockVar;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *staticNum;
__Block_byref_blockVar_0 *blockVar; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_staticNum, __Block_byref_blockVar_0 *_blockVar, int flags=0) : a(_a), staticNum(_staticNum), blockVar(_blockVar->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int count) {
__Block_byref_blockVar_0 *blockVar = __cself->blockVar; // bound by ref
int a = __cself->a; // bound by copy
int *staticNum = __cself->staticNum; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_03_v9qmb8t568sgwcjv8bj5ng380000gn_T_main_6b603d_mi_0, a);
(*staticNum) --;
globalNum --;
(blockVar->__forwarding->blockVar) --;
return count * a * globalNum * (*staticNum);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->blockVar, (void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
int a = 3;
__attribute__((__blocks__(byref))) __Block_byref_blockVar_0 blockVar = {(void*)0,(__Block_byref_blockVar_0 *)&blockVar, 0, sizeof(__Block_byref_blockVar_0), 3};
static int staticNum = 3;
blk blk = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &staticNum, (__Block_byref_blockVar_0 *)&blockVar, 570425344));
a = 10;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_03_v9qmb8t568sgwcjv8bj5ng380000gn_T_main_6b603d_mi_1, ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 1));
return 0;
}
我们看到block其实就是struct __main_block_impl_0
结构体,它由4个成员变量和一个构造函数组成。
其中第一个成员变量是__block_impl
结构体,指向的是__main_block_func_0
函数。
第二个成员变量是__main_block_desc_0
结构体,负责管理block的内存管理。__main_block_copy_0
和__main_block_dispose_0
就是利用OC的runtime进行内存管理。在Block中使用 __strong
或者__block
修饰符的id类型或对象类型的自动变量时,当block从栈复制到堆时,当对象需要retain的时候调用__main_block_copy
方法增加引用计数,当需要释放的时候则调用__main_block_dispose
方法释放对象。
第三个是int变量。因为block内部引用了外部的自动变量,所以在block结构体中多了一个同类型的成员变量。
第四个是捕获到的静态变量。
第五个是__block修饰符的变量。具体介绍看下一节。
__main_block_func_0
包含了block的内部代码。其中包含了一个参数__cself
,指向__main_block_impl_0
,及匿名block自身。这种写法类似与OC中的方法消息传递。
另外可以看到系统加的注释bound by copy
,自动变量通过__cself->val
方式捕获。局部变量传入的是值,静态变量传入的是地址。因此局部变量无法更改,而静态变量可以更改。同时全局变量由于作用域大,因此不需要传入就可以自由的在block内部更改。而__block
修饰符的变量是通过bound by ref
方式被捕获进来的,具体介绍看下一节。
__block 修饰符
我们直到,当想要修改block外面的局部变量时,需要用__block
修饰符。那么__block
的原理是如何的呢。
从上面的转换代码中可以看到,带有__block
修饰符的局部变量被转换为一个结构体__Block_byref_blockVar_0
。这个结构体有5个成员变量。
- __isa,isa指针
- __forwarding,指向自身类型的指针
- __flags,标记
- __size,结构体大小
- __blockVar,局部变量
struct __Block_byref_blockVar_0 {
void *__isa;
__Block_byref_blockVar_0 *__forwarding;
int __flags;
int __size;
int blockVar;
};
从代码中我们可以看到blockVar->__forwarding->blockVar
这样的方式来对__block
修饰的变量进行操作,但是为什么要搞这么复杂呢?__forwarding
指针就是针对堆里的block。把原来指向自己的__forwarding
指针指向堆上的__block
变量。然后堆上的变量的__forwarding
指向自己。
Block中的循环引用
循环引用,就是相互引用。比如A强引用B,B又强引用A。那么A和B的引用计数永远无法为0,造成内存泄漏。当然这种循环也可以是多个对象间的。
而对于block的循环引用,一般是一个对象引用block,而block内部又引用了这个对象。
打破这种循环引用一般有两种方法:1.弱引用;2.主动释放
弱引用
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"%@",weakSelf);
};
主动释放
__block id blockSelf = self;
self.blk = ^{
NSLog(@"%@",blockSelf);
blockSelf = nil;
};
self.blk();
当然,弱引用和主动释放各有缺点:
主动释放的缺点:
- 必须执行block才能避免循环引用
弱引用的缺点:
- 不能保证self对象是否已经被销毁
当然对于弱引用,可以利用weak-strong-dance来保证不会出现因为self释放引起问题。
__weak typeof(self) weakSelf = self;
self.blk = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@",strongSelf);
};
参考
1.深入研究Block捕获外部变量和__block实现原理
2.老司机出品——源码解析之从Block说开去
3.A look inside blocks: Episode 1
4.A look inside blocks: Episode 2