概述
上图就是一个block简单使用,它包括了block的声明、赋值实现、调用 三个部分,其中,实现部分可以看作是一种匿名函数;跟函数一样,block也是需要调用才能执行内部代码的;赋值的行为又让block看起来跟数据类型类似
代码块Block是在iOS4开始引入的,是对C语言的扩展,用来实现匿名函数的特性
Block是一种特殊的数据类型,可以像基本数据类型一样定义成变量、作为参数、返回值来使用
Block还可以保存一段代码,在需要的时候调用
在iOS开发中,Block被系统应在很多地方,例如:GCD、UIView动画、排序等,我们开发者也可以应用在各类回调、传值、传消息等
Block的声明、赋值实现、调用
Block的声明样式。
返回类型 (^Block名称)(参数列表)
void(^myBlock)(NSString *, NSString *)
Block的返回类型分为有返回类型和无返回类型(void),参数列表也可有也可以没有,具体看需求
// 无返回类型无参数列表
void(^block)();
// 无返回类型有参数列表
void(^block)(int);
// 有返回类型无参数列表
int(^block)();
// 有返回列表有参数列表
int(^block)(int);
Block的简单使用:声明、赋值、调用
Block变量的赋值格式为:
Block变量 = ^(参数列表){函数体}; // 这里的参数列表一定要和声明时的参数列表一致
// 声明
void(^block)();
// 赋值
block = ^(){
NSLog(@"Hello World");
};
// 调用
block();
也可以在声明时完成赋值
// 声明、赋值
void(^block)() = ^(){
NSLog(@"Hello World");
};
// 调用
block();
定义Block类型
前面提到过,Block是一种特殊的数据类型,我们可以使用 typedef 来定义 Block 类型,这样我们就可以使用该类型来声明很多相同的Block变量了。
typedef 返回类型(^Block名称)(参数列表);
示例:
// 声明一个Block类型
typedef void(^Block)();
// 使用定义两个block变量
Block myBlock,myNewBlock;
// 赋值实现
myBlock = ^(){
NSLog(@"Hello World");
};
myNewBlock = ^(){
NSLog(@"Hello World, I am lolita0164.");
};
// 调用
myBlock();
myNewBlock();
ARC模式下简单应用
- 作为对象属性实现消息传递
前面说到,Block保存一段代码,在需要的时候调用。我们可以将使用Block的三个步骤拆开,实现消息传递、传值功能。
// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 声明Block变量
@property (nonatomic, copy) Block myBlock;
-(void)sayHello;
@end
@implementation Person
-(void)sayHello{
// Block调用
self.myBlock(@"Hello, I am lolita0164");
}
@end
在定义声明、调用之后,还缺少实现的部分,这一步通常由外部实现。
Person *p = [Person new];
// Block赋值实现
p.myBlock = ^(NSString *string) {
NSLog(@"%@",string);
};
[p sayHello];
这样,我们就可以在Block的赋值实现部分里拿到 p类里的数据了。
- 作为函数参数实现数据回调
我们将之前的例子稍加改动
// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 将Block作为参数
-(void)sayHelloUseBlock:(Block)myBlock;
@end
@implementation Person
-(void)sayHelloUseBlock:(Block)myBlock{
// Block调用
myBlock(@"Hello, I am lolita0164");
}
@end
在外部进行实现。
Person *p = [Person new];
// Block实现
[p sayHelloUseBlock:^(NSString *string) {
NSLog(@"%@",string);
}];
作为参数和作为属性传递消息,在应用场景稍稍有些不同。
作为参数时,通常和当前的方法有着紧密的联系,函数体内部需要与调用的外部进行交互。例如在请求方法中,经常会使用到block进行回调,而这个block和当前的方法关系紧密,通常是该方法的结果回调。又或者是方法执行期间需要外部提供一定的信息,从而通过block获取外部提供的数据。
作为属性时,通常是和当前类相关,作为类与类之间的交互代表。
- 作为返回值实现链式语法
将block作为返回值的经典例子就是约束库 masonry
,这个库在做完每次约束设置之后通过 block 将实例再次回调,就形成了链式语法。
[view makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(view1.mas_right).offset(10);
make.top.equalTo(view1).offset(0);
make.right.equalTo(-10);
make.size.equalTo(viewWidth);
}];
下面通过创建颜色类来演示 block 作为参数的使用。
// block 作为返回值
+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
// block 的声明和实现
UIColor* (^rgbBlock)(CGFloat, CGFloat, CGFloat) = ^id(CGFloat r, CGFloat g, CGFloat b) {
return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
};
return rgbBlock;
}
解析
返回值是一个有返回值参数有三个的block:UIColor * (^)(CGFloat, CGFloat, CGFloat)
。我们在该方法的内部进行了block的声明和具体实现,并且将其作为返回值返回了出去,那么外部在调用该方法之后接收到的是一个block,可以使用该值。
那么外部使用情况如下。
// 接收 block 类型
UIColor* (^colorBlock)(CGFloat, CGFloat, CGFloat) = [UIColor rgb];
// 使用 block 获取到颜色
UIColor* color = colorBlock(10,33,65);
self.view.backgroundColor = color;
在丢弃不需要的部分后,代码如下。
+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
return ^id(CGFloat r, CGFloat g, CGFloat b) {
return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
};
}
// 使用block作为参数的方法
self.view.backgroundColor = UIColor.rgb(10, 33, 65);
Block 和 变量
Block访问局部变量问题
block 内部可以访问局部变量。
int global = 100;
void (^Block)() = ^(){
NSLog(@"global = %i", global);
};
Block(); // 输出 "global = 100"
但是 block 会把变量 复制 为自己私有的const变量,也就是说block会捕获栈上的变量(或指针),将其复制为自己私有的const变量,当变量被修改时,不会影响到block自己私有的const变量。
int global = 100;
void (^Block)() = ^(){
NSLog(@"global = %i", global);
};
global = 101;
Block(); // 输出 "global = 100"
在Block中不可以直接修改局部变量。
int global = 100;
void (^Block)() = ^(){
global ++; // 这句报错
NSLog(@"global = %i", global);
};
Block();
但是可以通过 __block
修饰符修改局部变量。
__block int global = 100;
void (^Block)() = ^(){
NSLog(@"global = %i", global);
};
global = 101;
Block(); //输出 "global = 101"
__block int global = 100;
void (^Block)() = ^(){
global ++; // 这句正确
NSLog(@"global = %i", global);
};
Block(); //输出 "global = 101"
原因:在局部变量前使用 __block修饰 ,在Block定义时便是将局部变量的指针传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。
Block访问全局变量、静态变量问题
可以访问和修改。
全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block变量所指向的结构体,因此在调用Block之前对全局变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。
在Block定义时便是将静态变量的指针传给Block变量所指向的结构体,因此在调用Block之前对静态变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。
ARC下的内存管理
在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,我们只需要避免循环引用即可。
// 当Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
NSLog(@"------");
};
myBlock();
如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,因此比并不会造成内存泄漏问题。
Person *p = [[Person alloc] init];
void(^myBlock)() = ^{
NSLog(@"------%@", p);
};
myBlock();
// Person对象在这里可以正常被释放
// 注:这里的Block只是单方面的强引用,所以不会产生循环引用,也不会内存泄漏
如果对象内部引用一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,导致内存泄漏。
self.block = ^{
NSLog(@"------%@", self);
};
解决办法:使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样就避免了Block对对象进行强引用。
__weak typeof(self) weakSelf = self;
weakSelf.block = ^{
NSLog(@"------%@", weakSelf);
};
提示:如果只是Block单方面地对外部变量进行强引用,并不会造成内存泄漏。
补充
1、声明block属性的时候为什么用copy呢?
在说明为什么要用copy前,先思考下block是存储在栈区还是堆区呢?其实block有3种类型:
- 全局块(_NSConcreteGlobalBlock)
- 栈块(_NSConcreteStackBlock)
- 堆块(_NSConcreteMallocBlock)
全局块存储在静态区(也叫全局区),相当于OC中的单例;栈块存储在栈区,超出作用域则马上被销毁。堆块存储在堆区中,是一个带引用计数的对象,需要自行管理其内存。
关于内存分配,请看这篇:C语言内存分配。
怎么判断一个block所在的存储位置呢?
- block不访问外界变量(包括栈中和堆中的变量)
block既不在栈中也不在堆中,此时就为全局块,ARC和MRC下都是如此。
- block访问外面变量
MRC环境下:默认存储在栈区
ARC环境下:默认存储在堆中,实际上是先放在栈区,在ARC情况下自动又拷贝到堆区,自动释放
因此,使用 copy 修饰符的作用就是将block从栈区拷贝到堆区
为什么要这么做呢?官方给出的答案是:
复制到堆区的主要目的就是 保存 block 的状态,延长其声明周期。因为block如果在栈上的话,其所属的变量作用域结束,该block就被释放掉了,block中的 __block 变量也同时被释放掉了,为了解决超出作用域就被释放的问题,我们就需要把block复制到堆中。
总结
OC 中的 block 是对 C 语言的匿名函数的一种特性是实现。block 具有函数特性,同时也可以作为变量使用。block 可以作为属性、参数、返回值使用。想要在 Block 内部修改外部变量时,需要使用 __Block
将变量指针传递给 block。在使用 Block 时需要特别注意内存泄漏的问题。