先说明一下,因为ARC下系统会对block做一些拷贝和释放操作,对深入于理解block无益,所以本篇文章所提到的栗子编译环境均为MRC。
首先引用《Objective-C高级编程》Blocks章节中的第一句话:Blocks是对C语言的扩充功能。而且OC是建立在C语言基础上之上,添加了面向对象机制的一门编程语言。
所以不要再说block的实现原理是C++的函数指针了,正确答案是:block的实现原理是C语言的函数指针。
函数指针即函数在内存中的地址,通过这个地址可以达到调用函数的目的。
Block是NSObject的子类,拥有NSObject的所有属性,所以block对象也有自己的生命周期,生存期间也会被持有和释放。
NSGlobalBlock 静态区(全局区)block,这是一种特殊的bloclk,因为不引用外部变量而存在。另外,作为静态区的对象,它的释放是有操作系统控制的,这一点我们最后再聊。
NSStackBlock 栈区block,位于内存的栈区,一般作为函数的参数出现。
NSMallocBlock 堆区block,位于内存的堆区,一般作为对象的property出现。
如果一个blcok引用了外部变量是栈block,则其不引用外部变量就成为了静态blcok。
如果一个block引用了外部变量是堆block,则其不引用外部变量就成为了静态block。
说到栈操作,大家都明白,出栈和入栈,函数只有入栈后才能执行,出栈后就释放了。
栈block一般在函数内部定义,并在函数内部调用;或者在函数外部定义,作为函数的一个参数在函数内部调用。函数出栈时和其他变量或参数一起释放。
栈区block形式有2,如下:
- (void)xxx
{
__block int i = 0;
void (^block1)() = ^{
//此处若不引用外部变量i,则block1是静态block,若引用,则为栈block
i++;
NSLog(@"i = %d",i);
};
block1();
}
- (void)saveFile:(NSDictionary *)dic complete:(void(^)(BOOL success))complete
{
//dic写入本地近作demo使用,实际开发要判断路径是否存在,以及dic中是否存在null值等
NSString * path = [NSHomeDirectory() stringByAppendingFormat:@"/Caches/dicInfo"];
BOOL suc = [dic writeToFile:path atomically:YES];
if (complete) {
//若complete中不引用外部变量suc,则complete是静态block,此处complete为栈block
complete (suc);
}
}
堆区是内存的常驻区域,也叫永久存储区,block一般在函数中定义,最多是个栈block,什么时候才能打怪升级成为堆block呢?
在MRC时代你需要使用Block_copy()方法,才可以将blcok复制到堆中。
然而复制到堆中有何用处呢?
首先,作为一个对象,把它复制到堆中,想要使用它肯定要有一个指针指向它,而指向它的指针是作为property或静态变量出现的(如果不被引用也就没有了常驻于堆区的意义),而实际开发中多使用property引用。
在MRC中,如果一个类的block属性是使用copy修饰的,则不需要手动调用Block_copy将其复制到堆中。如果是用strong修饰的,则必须使用Block_copy()将其复制到堆中,并在释放时调用Block_release()方法将其释放,否则会因野指针导致程序崩溃。
@interface TestV : UIView
@property (nonatomic, strong) void(^block1)();
@property (nonatomic, copy) void(^block2)();
@end
- (void)testFunc
{
__block int i = 0;
void (^block1)() = ^{
//此处若不引用外部变量i,则block1是静态block,若引用,则为堆block
i++;
NSLog(@"block1 -- i = %d",i);
};
void (^block2)() = ^{
i --;
NSLog(@"block2 -- i = %d",i);
};
TestV * view = [[TestV alloc] initWithFrame:CGRectMake(100, 100, kScreenWidth - 200, 40)];
[self.view addSubview:view];
view.block1 = Block_copy(block1);
view.block2 = block2;
NSLog(@"block1 -- %@",block1);
NSLog(@"block2 -- %@",block2);
}
因此在MRC中,基本上会用copy关键字修饰block,既优雅又省代码,还避免了因为未调用Block_release而造成内存泄露,这大概是很多人在ARC项目中依旧使用copy修饰block的历史原因。
其实在ARC下使用strong或copy修饰block没啥区别,亲测有效,可能会遇到一些开发人员,他们看到ARC工程中出现strong修饰的block就会提出质疑,那只能说明他们没有亲自写两行代码测试过是否真的有区别。
既然存在于静态区,则只有当进程被杀死,进程所占用的内存空间被释放后,静态区的对象才会被释放。静态block并不依附于某个对象而存在,也并不为某个类对象单独享有,而是所有的该类对象都共同拥有这个block,都有这个block的使用权,不管有多少个类对象,在程序运行期间这个block始终只有一份,直到进程被杀死,内存被释放。
以下代码片段将有助于你的理解:
//在一个成员方法中定义一个静态block,然后打印出self的地址和block的地址
- (void)blockAction
{
void(^block1)() = ^{
//未引用外部变量,则为静态block
NSLog(@"block1");
};
NSLog(@" -- self : %p",self);
NSLog(@" -- %@ --",block1);
}
//初始化了两个对象,同时调用各自的成员方法
[_v1 blockAction];
[_v2 blockAction];
//打印结果如下:两个对象地址不同,两个block地址相同
xxx BlockDemo[22106:1969807] -- self : 0x7f81da513f20
xxx BlockDemo[22106:1969807] -- <__NSGlobalBlock__: 0x1057ca1a0> --
xxx BlockDemo[22106:1969807] -- self : 0x7f81da512770
xxx BlockDemo[22106:1969807] -- <__NSGlobalBlock__: 0x1057ca1a0> --
下一篇博文《block之循环引用》,我们将会讲block与循环引用。
以上观点均为个人研究、理解,如有不妥之处,欢迎指正,谢谢!