基本概念
什么是block?《Objective-C高级编程》这本书里是这样定义的:
带有自动变量(局部变量)的匿名函数。顾名思义,匿名函数就是没有名称的函数。也被称为闭包
(closure)或者Anonymous function。
我们可以理解为block就是一个没有名称的函数。定义block的方式和定义函数的方式是相似的,而block还可以作为参数使用。当block被调用其块内的代码才会被执行。
定义
根据block的定义,我们可以知道,block的主要组成是返回值和参数。其表达式如下:
^+返回值类型+参数列表+表达式
按照是否存在返回值和参数,我们可以将block的定义分为以下几种:
- 无返回值+无参数
void(^myBlock)(void) = ^void(void) {
};
// 可以简写成:
void(^myBlock)(void) = ^ {
};
- 无返回值+有参数
void(^myBlock)(int a) = ^void(int num) {
};
// 可以简写成:
void(^myBlock)(int a) = ^(int num) {
};
- 有返回值+无参数
int(^myBlock)(void) = ^int(void) {
return 10;
};
// 可以简写成:
int(^myBlock)(void) = ^int {
return 10;
};
- 有返回值+有参数
int(^myBlock)(int a, int b) = ^int(int a, int b) {
return a + b;
};
当参数和返回值为void可以忽略。
在实际开发中,我们经常使用typedef来定义block:
typedef int(^myBlock)(int a, int b);
myBlock mb = ^int(int a, int b) {
return a + b;
};
NSLog(@"==myBlock==%d==", mb(1, 2));
block与外界变量的关联
下面我们来看一个例子:
typedef int(^myBlock)(int a, int b);
int d = 10;
myBlock mb = ^int(int a, int b) {
return a + b + d;
};
d = 5;
NSLog(@"==myBlock==%d==", mb(1, 2));
我们在mb内部使用外部变量int d,随后又给d重新赋值,此时调用mb(1, 2),block内部d的取值是5还是10呢?运行程序,控制台输出13。此例子说明了block具有截获外部变量的能力,而且截获之后,变量在block的值是固定的,不会随着外部的改变而改变。
我们接着在其外部定义一个变量e:
int e = 0;
当我们在block内部对e进行赋值操作的时候,编译器会提示错误:
Variable is not assignable (missing __block type specifier)
意为变量不可以被分配使用,是因为缺少__block修饰符。那么我们为上述变量d和e的声明加上__block修饰符,我们立刻看到编辑器没有错误了。运行程序,此时控制台输出8,这说明此时block中的取值不再是10而是5了。它不但被我们分配使用了,可以随着外界的改变而改变了,甚至我们可以随意的在block内部修改d的值了,这到底是为什么呢?
从表面上看,没有被__block修饰的变量,我们在block内部使用的时候,只是截获其当时的值,所以其不会再改变,而被__block修饰的变量,我们截获该变量的地址,所以它不论怎们改变,我们都能截获到。
block分类
在iOS中,我们依据内存情况将block分为6中
-
_NSConcreteGlobalBlock:全局block,不访问外界变量(包括堆中和栈中的变量)
void (^block)(void) = ^{
NSLog(@"==block==");
};
-
_NSConcreteMallocBlock:堆block,存在于堆内存中,是带一个引用计数的对象,需要自己进行内存管理。变量本身在栈中,因为block能够自动截获变量,为了访问到变量,会将变量从堆内存中copy成栈内存中。
int a = 10;
void (^block)(void) = ^{
NSLog(@"==a==%d==", a);
};
-
_NSConcreteStackBlock:栈block,存于栈内存中,超出其作用域则马上进行销毁。作为方法或者函数的参数的时候不会被copy到堆上。
NSLog(@"%@",^{
NSLog(@"==block==");
});
_NSConcreteAutoBlock_NSConcreteFinalizingBlock_NSConcreteWeakBlockVariable
前3种在日常开发中是很常见的,后3种是系统级别的block,一般比较少用。
我们知道程序在编译的时候内存的分布有:堆区(heap)、栈区(stack)、文字常量区、程序代码区、全局区/静态区。
这张图也就解释了为什么我们使用block作为属性的时候修饰符都是用copy,_NSConcreteGlobalBlock是全局block,它只能存在于一个函数的内部,并不能作为属性;而_NSConcreteStackBlock是栈block,存于栈内存中,超出其作用域则马上进行销毁,当我们使用copy就会将其copy到堆内存中,这样会延长block的生命周期,防止出现异常;而_NSConcreteMallocBlock本身就是堆block,使用copy也不会对其有影响。
block的使用
block既然是一个匿名函数,那么它就可以作为函数使用,当然,它也可以作为作为函数的参数、或者函数的返回值调用。
上面的例子大多数都是block作为函数使用,我们也就不再赘述了。下面我们就看看其他两种情况:
首先我们先给block设置一个别名,这样看着简单明了一些。
typedef int(^SumBlock)(int a, int b);
-
block作为函数的参数
- (void)blockAsParameter:(SumBlock)mb {
NSLog(@"==blockAsParameter实现==%@==", mb);
mb(1, 2);
}
可以调用一下,然后运行程序:
[self blockAsParameter:^int(int a, int b) {
NSLog(@"==blockAsParameter调用==%d==", a + b);
return a + b;
}];
// 控制台输出
==blockAsParameter实现==<__NSGlobalBlock__: 0x106a4d090>==
==blockAsParameter调用==3==
-
block作为函数的返回值
- (SumBlock)blockAsReturns {
NSLog(@"==blockAsReturns==");
return ^int(int a, int b) {
return a + b;
};
}
SumBlock mb2 = [self blockAsReturns];
NSLog(@"==blockAsReturns==%d==", mb2(1, 2));
==blockAsReturns==
==blockAsReturns==3==
其实说的通俗点,block就是封装一段代码块,这段代码块可以像变量一样被使用。
block的循环引用问题
我们在日常使用block的时候一定要注意一个问题,那就是循环引用。因为block经常是作为变量被self持有,或者是block的持有者被self作为变量持有,然而当我们在block内部使用self的时候就会造成循环引用。我们来看一个例子:
@property (nonatomic, assign) int num1;
@property (nonatomic, assign) int num2;
@property (nonatomic, copy) SumBlock propertyBlock;
self.num1 = 1;
self.num2 = 2;
self.propertyBlock = ^int(int a, int b) {
NSLog(@"==a==%d==b==%d==", self.num1, self.num2);
return a + b;
};
编译器会直接提示:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
意为在block中强引用self可能会造成循环引用。上述例子中,num1、num2、propertyBlock作为属性被self持有,而我们又在block里使用了num1、num2,这就相当propertyBlock又持有了self,这就造成了循环引用。强持有会对引用计数进行处理,循环引用会导致对象无法被释放,就影响了对象的引用计数,会造成内存问题。那么这个问题如何解决呢?
__weak typeof(self) weakSelf = self;
self.propertyBlock = ^int(int a, int b) {
NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
return a + b;
};
这样就解决了循环引用问题。那么weakSelf是如何解决循环引用的呢?由于现在引入了weakSelf,持有的情况就变成了weakSelf持有self,self持有block,而block又持有weakSelf,但是需要注意的是weakSelf持有self是弱引用,只是一个指向,引用计数并没有发生改变,所以就打破了循环引用。
但是使用__weak需要注意一点,就是对象的释放时间。
self.propertyBlock = ^int(int a, int b) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
});
}
如果我们进入页面之后,立即退出页面,控制台输出:
==a==0==b==0==
这说明,dealloc之后持有的对象却是已经被释放了。如果我们想等到打印结果输出之后,再进行dealloc该怎么处理。
- 使用
__strong。
我们可以在block里面使用__strong再将weakSelf转化为强引用即可。此时虽然strongSelf为强引用,但是只是在block作用域内的,当block内任务执行完毕,自然也会释放,和外界并没有任何关系。
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==num==%d==", strongSelf.num);
});
- 将
self作为参数传入block。
blcok具有截获变量的能力,当参数传入block,block会copy一份使用,此时copy出来的变量和原来的就没有关系。这样也打破了循环引用。
typedef int(^MinusBlock)(int a, int b, ViewController *vController);
self.num = 10;
self.mb = ^int(int a, int b, ViewController *vController) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==num==%d==", vController.num);
});
return a - b;
};
self.mb(2, 1, self);
block的基础就介绍到这里,下一章我们再来看看block的底层分析。
参考文献:
《Objective-C高级编程 iOS和OS X多线程和内存管理》