理解“块”这一个概念
块可以实现闭包。这项语言特性是作为“扩展”而加入GCC编译器中的,从技术上讲,这是个C语言层面的特性,因此,只要有支持此特性的编译器,以及能执行块的运行期组件,就可以在C、C++,OC,OC++代码中使用
块的基本知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享一个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码,例如,下面就是一个简单的块:
^{
//Block implementation here
}
块其实就是一个值,而且有其相关类型。与int、float或者OC对象一样,也可以吧块赋值给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:
void (^someBlock)() = ^{
//Block implementation here
}
下面就是有两个参数并且有返回值的:
int (^addBlock)(int a,int b) = ^(int a,int b){
//Block implementation here
return a + b;
};
我们可以像使用C语言的函数一样使用:
int add = addBlock(4,5);
块的强大之处是:在声明它的范围内,所有的变量都可以为其所捕获。也就是说,那个范围的全部变量,在块里依然可用,比如,下面这段代码所定义的快,就使用了块外的变量:
int admition = 6;
int (^addBlock)(int a,int b) = ^(int a,int b){
//Block implementation here
return a + b + addition;
};
int add = addBlock(4,5);
默认情况下,为块捕获的变量,是不可以在块里面修改的,在本例中,假如块内的代码改动了addition变量的值,那么编译器就会报错,不过声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。Block不允许修改外部变量的值Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
我们可以打印下内存地址来进行验证:
__block int a = 0;
NSLog(@"定义前:%p", &a); //栈区
void (^foo)(void) = ^{
a = 1;
NSLog(@"block内部:%p", &a); //堆区
};
NSLog(@"定义后:%p", &a); //堆区
foo();
打印结果
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8
“定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。
那么如何证明“block内部”打印的是堆地址?
把三个16进制的内存地址转成10进制就是:
定义后前:6171559672
block内部:5732708296
定义后后:5732708296
中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。
这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。
理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:
以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。
NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
NSLog(@"\n 定以前:------------------------------------\n\
a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a); //a在栈区
void (^foo)(void) = ^{
a.string = @"Jerry";
NSLog(@"\n block内部:------------------------------------\n\
a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a); //a在栈区
a = [NSMutableString stringWithString:@"William"];
};
foo();
NSLog(@"\n 定以后:------------------------------------\n\
a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);
打印结果
定以前:------------------------------------
a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58
block内部:------------------------------------
a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fdd88c180e0
定以后:------------------------------------
a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58
我们还能经常看到“内联块” 的用法:
NSArray *array = [NSArray array];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
这种常见的编码习惯也是可以看出来块为何如此有用。在OC中引入块这个特性之前,想要编出同样功能的代码,就必须传入函数指针或者是选择子的名称,这样就会再写几行代码了,而且还会令方法变得有些松散,与之相反,若声明内联形式的块,就能把业务逻辑都放在一起了。
如果块所捕获的变量就是对象类型,那么就睡自动保存它,系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的问题。块本身可视为对象。实际上,在其他OC对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获是所执行的保留操作。
如果将块定义在OC类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加__block。不过,如果通过读取或者写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量与self所指代的实例关联在一起。
@property (nonatomic,copy) NSString *value;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
void (^someBlock)() = ^{
_value = @"someBlock";
};
}
@end
在这种情况下,self变量就指向此block。由于在块内没有明确的使用self变量,所以很容易忘记self变量其实以为块所捕获了。直接访问实例变量和通过self来访问是等效的。然而一定要记住:self也是一个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时保留了块,那么就会出现保留环,在这样的情况下,我们一般会采用weak-strong dance方法(在ARC的情况下)来解决这个问题。
块的内部结构
每个OC对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大小之分。块本身也就是对象,在存放块对象内存区域中,首个变量是指向Class对象的指针,该指针叫做isa,其他结构如图。
在内存布局中,最重要的就是invoke变量,这是一个函数指针,指向块的实现代码。函数原型至少要接受一个void*型 的参数,此参数代表块。descriptor变量是指向结构体指针,每个块里都包含这个结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,后者将之释放块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间,请注意,拷贝并不是对象的本身,而是指向这些对象的指针变量。
栈块和堆块
定义块的时候,其所站的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。例如,下面这段代码就是有危险的:
void (^block)();
if (<#condition#>) {
block = ^{
NSLog(@"BlockA");
};
}else{
block = ^{
NSLog(@"BlockB");
};
}
定义在if和else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if和else语句范围内有效。这样写出来的代码可以编辑,但是运行起来有时正确有时不正确,若编译器没有覆写待执行的块,程序正常运行,若覆写,程序崩溃。为解决这个问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆 了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到了堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC下会自动释放,而在手动管理应用计数则需要自己来调用release方法。当应用计数降为0后,“分配到堆上的块”就会被系统回收。在“栈上的块”无须释放,占内存本身就会自动回收。
void (^block)();
if (<#condition#>) {
block = [^{
NSLog(@"BlockA");
} copy];
}else{
block = [^{
NSLog(@"BlockB");
} copy];
}
这样就能够变得安全了,如果手动管理引用计数,那么在用完块之后还需要将其释放。
站在巨人的肩膀上,同时有个人的想法……