前言
一直在使用block,但却不知道block是什么。本篇文章用以学习并记录。
目录
- Block的声明
- Block的内部实现
- Block循环引用的理解
- block的类型,为什么要用copy修饰
- 其他问题
Block的声明
声明一个block
返回类型 (^名称)(形参列表) = ^(形参列表) {
内容
}
int (^addBlock)(int, int) = ^(int a, int b) {
return (a + b);
};
执行一个block
addBlock(1, 2);
Block的内部实现
1.简单block内部实现
我们先来写一个简单的block
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
void(^blockA)(void) = ^(void) {
NSLog(@"1");
};
blockA();
return 0;
}
通过clang的命令可以将oc代码转换为c++代码
在命令行中进入到main.m文件目录,并输入:
(ps:该命令有相关参数,可在转换时引入框架等功能,当命令执行报错时可以尝试下,参数百度能搜得到。)
clang -rewrite-objc main.m
同级目录下会得到一个main.cpp文件,打开它之后可以看到非常非常多的内容,上面的一堆是Foundation框架转换后的内容。我们直接拖到最下面,可以找到我们的main函数。
为方便理解,我加了注释
1.1 我们先来看main函数中的内容
我们可以看到,原本main函数中写的block定义和执行的地方被转换成了结构体和函数指针的调用。blockA被转换成了2个结构体__main_block_impl_0、__main_block_desc_0,其匿名调用转换成了静态函数__main_block_func_0。
由此我们可以得知block本质上是结构体,而block的匿名调用本质上是静态函数。
1.2 再来看blockA转换得到的结构体__main_block_impl_0
// __main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl; // __block_impl是系统的block结构体,包含block的基础信息
struct __main_block_desc_0* Desc; // block详细信息
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// __block_impl,包含了block的基础信息。其定义在main.cpp中搜索一下就可以找到
struct __block_impl {
void *isa; // 和所有oc对象一样有一个isa指针,指向block的类型
int Flags; // 标识
int Reserved; // 保留值
void *FuncPtr; // 函数指针,指向block匿名调用对应的静态函数
};
从结构体的定义我们可以知道:
1. __main_block_impl_0对应blockA的本体。2. 其中__block_impl类型的变量impl包含了每个block结构体都有的基本属性。这有点类似于面向对象的思想,类似于__main_block_impl_0继承了__block_impl。
3. __block_impl中的isa指针,表示block实际上是一个OC对象。类比NSObject的isa指针,相同的是它们都指向对象的前8字节。不同的是NSObject及派生类对象的isa指针指向Class的元类,而block的isa指针指向“block的类型”。
4. __block_impl中的函数指针FuncPtr,指向block的匿名调用对应的静态函数。
5. 对于block的isa指针指向“block的类型”的解释。block的类型有三种:_NSContreteGlobalBlock、_NSContreteStackBlock、_NSContreteMallocBlock,这三种类型都是由OC中的类__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__转换而来的,通过一个小测试我们可以看到这三种类型。
关于block的类型的区别后面会具体去解释。
6. block匿名调用对应的静态函数,其函数指针保存在FuncPtr中
1.3 那么回过来再看main函数中的内容
main函数中的两句代码为了方便理解我做了拆分。
block的定义和实现其实就是:通过构造函数创建了一个__main_block_impl_0类型的结构体变量,并将其首地址赋值给函数指针blockA。
block的执行其实就是:通过blockA这个指针去执行FuncPtr指向的静态函数
扩展:
在探究过程中发现上图中的两步强转类型有些不太对劲。
// 构造__main_block_impl_0变量
__main_block_impl_0 block_impl_0 = __main_block_impl_0(fp, desc);
// 强转类型
void(*blockA)(void) = (void (*)())&block_impl_0;
// 强转类型
__block_impl *block_impl = (__block_impl *)blockA;
其实代码合并起来相当于:
__block_impl *block_impl = &block_impl_0;
问题是:__main_block_impl_0和__block_impl的类型都不同,为什么赋值后使用却不会出错。
不是应该这样写吗?
__block_impl *block_impl = block_impl_0.impl;
后来突然想到了原因,这是因为内存分配顺序的原因。因为结构体__main_block_impl_0中impl的定义是写在最前面的,所以该结构体变量在分配内存时会先从impl开始。而上述的block_impl指针指向block_impl_0的首地址,也就相当于刚好指向其中的impl。
比如:__main_block_impl_0的大小是100位,__block_impl的大小是50位,那么使用block_impl指针时操作的是__main_block_impl_0里的前50位内存地址,刚好是__main_block_impl_0里面的impl。
如果将struct __block_impl impl定义写在struct __main_block_desc_0* Desc后面则会出错。
猜测这样写的原因可能是为了代码简洁。= =!
2.block中使用外部参数情况的内部实现
首先来看一个简单的例子
转换成c++代码后
2.1 我们可以看到普通变量a_int和a_number在block中的传值过程:
1. 结构体__main_block_impl_0的构造函数接收变量a_int和a_number的值,且传参形式为值传递。
2. 结构体__main_block_impl_0中新增了与之对应的成员变量a_int和a_number用来保存这两个值。
3. 静态函数中使用的a_int和a_number是重新定义的局部变量,其值为结构体中保存的值。
4. 在block中使用的变量其实是静态函数中的局部变量
2.2 通过上述的传值过程,我们应该能够明白两个问题:
1.为什么block中使用的普通外部参数,外部修改其值不会影响到block内部?
这是因为在构造结构体__main_block_impl_0时,a_int和a_number是值传递。在外部修改a_int的值,或是修改a_number指针的指向,是不会影响到结构体中保存的值的,所以当然也无法改变在静态函数中使用时的值。2.为什么在block内部无法修改外部变量的值?
我们可以得知,由于是值传递,block内部a_int、a_number和外部的a_int、a_number并不是同一个变量,所以在block内部是并不能获取到外部的变量的,当然也不能在block内部修改他们的值。(ps:其实即使是真的在block内部修改a_int也应该可以,只不过修改的是静态函数内部的局部变量a_int,至于为什么编译器设定这样写会报错?我想可能是为了便于理解,防止数据紊乱吧。)
过程如下图所示:
另外下图可以证实,block内部的变量和外部的变量并不是同一个。
2.3 但是有几种情况下可以在block中修改外部变量的值
1.全局变量
2.静态变量
3.变量使用__block修饰
看看对应的实现代码
转换成C++代码后:
从图中可以看出,用__block修饰的变量block_int、block_number被包装成了结构体,这个结构体其实是OC对象。
从图中可以看出,blockA对应的结构体中新增了3个指针变量static_int、block_int、block_number。
从图中可以看出,blockA匿名调用对应的静态函数,里面使用的block_int和block_number也都是从blockA中获取的包装变量。OC代码中在block内部修改值相当于修改该封装变量中保存的值。
图中的__main_block_copy_0函数和__main_block_disopse_0是用来处理内存,类似于retain和release。__main_block_desc_0为blockA的扩展信息结构体。
最后我们在main函数中可以看到,定义一个__block修饰的变量,其实是定义了一个包装它的结构体变量。在结构体__main_block_impl_0的构造函数传参中,作为参数传递的就是这个结构体,且传的是地址。另外静态变量static_int传的是地址,全局变量由于全局都能获取到的,所以不用传参。
通过上述的代码我们可以知道为什么这三种变量可以在block内部修改其值:
1. 全局变量由于全局都可以获取到并修改它的值,所以可以在内部进行修改。并且在block中使用不会有任何特殊处理。
2. 使用静态变量与普通变量相同的是都会在blockA结构体中有定义,不同的地方是传递的是地址而不是值,所以它可以在内部进行修改。
3. 使用__block修饰的变量会被包装成一个对象,且传参时传递的是该包装对象的引用。因此这个变量不管是在block内部还是在block外部都是以包装对象的形式存在,并且修改该变量的值其实修改的是包装对象内部持有的该变量的值(ps:我们写代码只能操作该值,操作不了包装对象)。因此该变量在block内外都是同一个对象,所以可以修改。
Block循环引用的理解
通过上述的探究过程我们可以了解到block的结构和实现以及使用各种类型变量的情况,不过一般来说我们也不太常会用到,但是了解这些可以让我们明白为什么block会导致循环引用。
我们先来了解一下什么是循环引用:
由于iOS系统下使用引用计数的方式来管理内存,所以一个对象是否需要被释放是由引用计数是否为0决定的,但是如果出现了A类的实例强引用B类的实例,B类的实例又强引用了A类的实例(这里我简写为:A->B->A),或者是类似A->B->C->A这种情况,这样就会出现闭环,这几个实例的引用计数都将无法变为0,因此应用运行期间也将无法被释放。这就是循环引用,它会导致内存泄漏的问题(因为有一块内存一直无法释放,并且除了他们之间相互引用的指针之外没有其他指针指向他们,所以也没办法再操作他们了)。
如下图,闭环为ak47->gamer->ak47
解决循环引用的方法就不详细说了,只需要将闭环中某一个成员属性用weak修饰即可打破循环。
那么block为什么会导致循环引用呢?
首先我们知道block是一个对象,通过上面对block内部结构的研究我们还知道,一个不加修饰的对象在block内使用时,block会定义相应的成员变量,并使用外部变量对其赋值,这就相当于block强引用持有这些对象。所以在使用block时,只要出现A->block->B->...->A这种闭环的形式,就有可能因循环引用的问题导致内存泄漏。
如下图:闭环为self -> gun -> block -> self
但是如下图就不会出现循环引用,因为没有产生闭环。
持有关系为gun -> block -> self
如何解决block的循环引用问题?
使用__weak打破闭环即可。
持有关系为self -> gun -> block -X> self,因为block持有弱引用指针weakself不会改变其引用计数,因此打断了block强引用持有self的关系。
那么为什么我们经常会看到与weakself成对出现的strongself呢?strongself有什么作用?
因为如果是异步执行block,或者在block中有异步执行的代码,那么有可能会出现在block执行到其中某一句代码时weakself会突然变成nil。
如下图,我将代码写到了一个控制器类中,当执行到[weakself handleFire]这句代码时我将控制器pop,控制器将会被销毁,在3秒后执行到[weakself peopleDead]这句代码时可以看到weakself变成了nil。
这就会产生一个问题,明明开了枪人却没死。类比一下,比如当你做某个本地存储的功能,如果因为操作过快引起了了上述的状况,导致数据处理好了,状态也更新了,但是数据没有落库,这种问题发生也还是蛮可怕的。
所以说strongself其实就是确保了block执行时self一直是为nil或一直有值,而不会出现前一半代码self有值,后一半代码self为nil的情况。
block的类型,为什么要用copy修饰
在说block的类型之前我们先来了解一下内存的划分:
1. 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
2. 堆区(heap):一般通过代码分配释放,若未释放,则程序结束时由系统回收。操作方式类似于链表。
3. 全局区(静态区static):存储全局变量和静态变量,初始化的全局变量和静态变量在一块区域(.data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss)。程序结束后由系统释放。
4. 文字常量区:常量字符串就是放在这里的(.rodata)。程序结束后由系统释放。
5. 程序代码区:存放函数体的二进制代码(.text)。
现在来看block,在上面的探究过程中我们知道了block其实是对象,其类型有三种:
1. __NSGlobalBlock__:定义在.data区,block内部未使用任何外部变量。
我们知道在block内部使用的外部变量会在block对应的结构体中有所定义,而全局类型的block内部没有使用外部变量,无论如何执行都不依赖于执行状态,因此定义在全局区。例如^(void) { }
2. __NSStackBlock__:定义在栈区,使用了外部变量的block默认是创建在栈上的。
3. __NSMallocBlock__:定义在堆区,当block执行copy方法时会自动从栈拷贝到堆上。
那么为什么block类型的成员变量需要用copy修饰呢?
1.我们先来看一下block在MRC下的使用:
由于block默认创建在栈上(此默认的说法先不考虑全局block的情况),其生命周期即为创建时所在方法的作用域,当方法执行完之后block就会被自动释放,指向它的指针也都会变成野指针。如果想在超出block定义时的生命周期范围之外使用,那么需要执行block的copy方法将其复制到堆上。
如下图,在MRC环境下没有对block拷贝就直接返回,在block离开getABlock方法的作用域之后被释放,main函数中的block指针变为野指针,所以发生了崩溃。
如下图,当执行了copy方法之后,block被拷贝到堆上,生命周期延长,程序正常执行。
另外再看一个例子:
Gun类的成员变量fireBlock使用了retain修饰,在load方法中将block赋值给fireBlock时并未执行copy方法,所以我们可以看到在赋值了之后block仍在栈上,load方法作用域结束block被释放,所以main函数中使用fireBlock就会报野指针。
使用copy修饰,就不会出错了。
结论:在MRC下对block类型的成员属性修饰最好用copy,而不要用retain,由于使用retain修饰只会改变引用计数而不会执行copy方法将block复制到堆上。此外block是一个对象就更不可能用assign修饰了。
(其实明白了block在内存中的存储位置和规则,如果真的要较真的话在MRC下用retain也是可以的,不过需要在block创建后调一下copy方法,如果是成员属性需要重写其set方法,并在set方法中调用copy,以达到将block复制到堆上的目的,对此我只能说何必呢)。
2.我们再来看一下block在ARC下的使用:
很有意思的是在ARC环境下,只要将block赋值就会自动拷贝到堆上。那么ARC环境下什么情况block会被copy到堆上呢?
1.执行copy方法。
2.作为方法返回值。
3.将Block赋值给非weak修饰的变量
4.作为UsingBlock或者GCD的方法入参时。(暂无法验证)
例子如下:
结论:一般来说我们使用block都是会有赋值操作的,由于有上述条件的存在,所以基本上不会遇到在栈上的block的情况。所以在ARC环境下,block类型的成员属性使用strong或copy修饰其实都可以,但是为了延续MRC的习惯,另外避免真的出现一些奇怪问题的情况,通常还是使用copy修饰。
总结:在MRC环境下需要用copy修饰,因为如果不对block执行copy操作,它在所在方法执行完成后会被释放。但ARC环境下由于有内部机制所以可以免去麻烦,但延续习惯也用copy修饰。
其他问题
1.全局/静态block持有的对象无法dealloc
类似于单例对象,全局或静态的block持有的对象不会释放,所以在使用全局block或者使用单例持有的block时,要注意在合适的时候将block置为nil。
// VC1.m
static dispatch_block_t staticBlock;
- (void)gotoNextVC {
UIViewController *vc2 = [UIViewController new]
staticBlock = ^{
vc.view.backgroundColor = [UIColor greenColor];
NSLog(@"test vc: %@", vc);
};
[self.navigationController pushViewController:vc animated:YES];
}
// 若staticBlock不置为nil,vc2对象不会释放
// XXManager.m
- (void)doSomethingWithCompletion:(dispatch_block_t)completion {
self.completion = completion;
[self startDoSomethingWithFinishAction:@selector(finish)];
}
- (void)finish {
if (self.completion) {
self.completion();
// 这里应该置nil
self.completion = nil;
}
}
// VC.m
- (void)doFirstThing {
[[XXManager sharedInstance] doSomethingWithCompletion:^{
[self doSecondThing];
}];
}
// 若self.completion不置为nil,VC对象不会释放
2.使用block时出现野指针问题(EXC_BAD_ACCESS)
nil对象调用方法时不会造成crash,而执行一个置空的block会引起野指针崩溃。在大多数场景下,执行block都应该加上非空判断。
if (block) {
block();
}
3.block的持有和释放
假如 vc1 push 跳转到 vc2,vc2 有一个 block 持有 vc1,当 vc2 pop 时,vc2 和 block 都会销毁,不会存在循环引用。
因为 Nav 持有 vc1 和 vc2,但是 vc1 并没有持有 vc2,所以不会存在循环引用。
假如 vc1 modal 跳转到 vc2,vc2 有一个 block 持有 vc1,当 vc2 dismiss 时,vc2 和 block 都会销毁,不会存在循环引用。
参考文章
深究Block的实现
iOS Block源码分析系列(三)————隐藏的三种Block本体以及为什么要使用copy修饰符
深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。