基本概念
什么是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
多线程和内存管理》