一、概述
上图就是一个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 myBlock,myNewBlock;
// 赋值
myBlock = ^(){
NSLog(@"Hello World");
};
myNewBlock = ^(){
NSLog(@"Hello World, I am lolita0164.");
};
// 调用
myBlock();
myNewBlock();
三、简单应用,ARC模式下
1、作为对象属性实现数据传递
前面说到,Block保存一段代码,在需要的时候调用。我们可以将使用Block的三个步骤拆开,实现消息传递、传值功能
首先我们定义一个Person类
Person.h 文件
#import
// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 声明Block变量
@property (nonatomic,copy) Block myBlock;
-(void)sayHello;
@end
Person.m 文件
#import "Person.h"
@implementation Person
-(void)sayHello{
// Block调用
self.myBlock(@"Hello, I am lolita0164");
}
@end
在主函数中
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person new];
// Block赋值实现
p.myBlock = ^(NSString *string) {
NSLog(@"%@",string);
};
[p sayHello];
}
return 0;
}
结果
这样,在主程序中,我们就可以在Block的赋值实现部分里拿到p类里的数据了
2、作为函数参数实现数据回调
我们将之前的例子稍加改动
Person.h 文件
#import
// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 将Block作为参数
-(void)sayHelloUseBlock:(Block)myBlock;
@end
Person.m 文件
#import "Person.h"
@implementation Person
-(void)sayHelloUseBlock:(Block)myBlock{
// Block调用
myBlock(@"Hello, I am lolita0164");
}
@end
在主程序中
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person new];
// Block实现
[p sayHelloUseBlock:^(NSString *string) {
NSLog(@"%@",string);
}];
}
return 0;
}
结果和属性传递一样的
注:在使用自定义的Block中,要特别注意循环引用而导致内存泄漏的问题;该问题的产生原因以及解决方案会在后面介绍
3、作为返回值实现链式语法
使用过著名的约束框架masonry
的人,想必都属性它的语法。masonry
是一种典型的基于Block作为返回值的约束框架,通过Block在设置完部分约束之后再将类本身回调出来继续设置其他约束。下面我们通过一个简单的示例来演示Block作为返回值使用。
/// 十六进制颜色
+(UIColor* (^)(NSInteger))colorWithHex{
return ^UIColor (NSInteger rgbValue) {
return [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0];
};
}
上述方法中的 UIColor* (^)(NSInteger)
是一个标准的有返回值UIColor
,参数为NSInterger
的Block,它被作为来返回值,在该方法体中,最外层的return
返回了Block的实现部分,那么此时Block的声明、赋值实现、调用三要素中还差一个调用部分,这部分是由使用者在外部完成的:
UIColor.colorWithHex(0xdddddd);
当你调用colorWithHex
时,实际上拿到了返回值Block,这时候你可以直接加上()
给其传递需要的参数,完成调用部分,有几个参数就需要传递几个参数并且需要一一对应。
和masonry
不同,我们这里并没有做完一些操作(如约束)后再次回调自身而是根据需求回调了最终的结果。
四、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内部的值,同时内部的值也是可以修改的
五、Block在ARC下的内存管理
- 在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,我们只需要避免循环引用即可
// 当Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
NSLog(@"------");
};
myBlock();
- 在Block的内存存储在堆中,如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,因此比并不会造成内存泄漏问题
Person *p = [[Person alloc] init];
void(^myBlock)() = ^{
NSLog(@"------%@", p);
};
myBlock();
// Person对象在这里可以正常被释放
// 注:这里的Block只是单方面的强引用,所以不会产生循环引用,也不会内存泄漏
- 如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,导致内存泄漏
Person.m 文件
@interface Person : NSObject
@property (nonatomic, copy) void(^myBlock)(); // person类强引用Block
@end
@implementation Person
- (void)dealloc{
NSLog(@"Person dealloc");
}
@end
Person类在Block内强引用了自己,这时候产生了循环引用,不能被释放
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.block = ^{
NSLog(@"------%@", p);
};
p.block();
}
return 0;
}
上述问题解决办法
解决循环引用的办法就是使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样就避免了BLock对对象进行强引用
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
__weak typeof(p) weakP = p;
p.block = ^{
NSLog(@"------%@", weakP);
};
p.block();
}
return 0;
}
这时候p就可以正常释放了
补充:
在Block内部定义的变量,会在作用域结束时自动释放,Block对其并没有强引用关系,且在ARC中只需要避免循环引用即可,如果只是Block单方面地对外部变量进行强引用,并不会造成内存泄漏
六、扩展,仿写系统数组block排序
在iOS开发过程中,我们可能会遇到给对象数组排序的问题,系统也提供了以下方法来解决问题
- (NSArray *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr;
其中:NSComparator是一个Block类型
typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);
接下来就使用Block仿写这种排序方式
首先我们来新建NSArray的分类,为此添加一个带有Block参数的排序方法,并仿照系统定义一个枚举,作为Block的返回值,也是我们进行排序的依据
类声明部分
#import
typedef NS_ENUM(NSInteger, NSComparisonResult_LOLITA) {
NSOrderedAscending_LOLITA = -1L, // 表示左边对象小于右边的对象 ,不交换
NSOrderedSame_LOLITA, // 两者相等,不交换
NSOrderedDescending_LOLITA, // 左边对象大于右边的对象, 交换
};
@interface NSArray (sortArray)
-(NSArray *)sortedArrayUsingComparator_LOLITA:(NSComparisonResult_LOLITA(^)(id obj1, id obj2))Comparison;// Block的声明部分
@end
类实现部分
#import "NSArray+sortArray.h"
@implementation NSArray (sortArray)
-(NSArray *)sortedArrayUsingComparator_LOLITA:(NSComparisonResult_LOLITA (^)(id, id))Comparison{
NSMutableArray *tmpArray = [NSMutableArray arrayWithArray:self];
for (int i=0; i
注:系统的sortedArrayUsingComparator这个方法本身就是按递增的方式排序,他的枚举型(NSOrderedAscending 不交换,NSOrderedSame 不交换,NSOrderedDescending 交换),所以除了NSOrderedDescending做了交换处理之外,另外两个根本没做处理
这样我们就可以使用我们自定义的排序方法了
给Person类添加一个年龄属性,我们根据这个年龄来排序
#import
@interface Person : NSObject
@property (assign ,nonatomic) int age;
@end
在主程序中
#import
#import "Person.h"
#import "NSArray+sortArray.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p1,*p2,*p3,*p4;
p1 = [Person new];
p2 = [Person new];
p3 = [Person new];
p4 = [Person new];
p1.age = 10;
p2.age = 20;
p3.age = 5;
p4.age = 12;
NSArray *array = @[p1,p2,p3,p4];
array = [array sortedArrayUsingComparator_LOLITA:^NSComparisonResult_LOLITA(id obj1, id obj2) { // block的实现部分
Person *p1 = obj1;
Person *p2 = obj2;
if (p1.age > p2.age) {
return NSOrderedDescending_LOLITA;
}
return NSOrderedSame_LOLITA; // 或者 NSOrderedAscending_LOLITA 都是一样的
}];
for (Person *p in array) {
NSLog(@"年龄:%i\n",p.age);
}
}
return 0;
}
运行结果
如果你想将序,只需将Block内部实现改为
if (p1.age < p2.age) { // 这里更改
return NSOrderedDescending_LOLITA;
}
return NSOrderedSame_LOLITA; // 或者 NSOrderedAscending_LOLITA 都是一样的
在上述例子中,Block的使用三步骤依旧被我们拆开,我们在排序方法里进行了Block的声明和调用操作,在使用排序方法时,才赋值实现该Block
抛砖引玉,你也可以类似的去仿写一下系统的其他使用到Block的方法,例如GCD、UIView动画块等,快去试试吧
七、补充
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复制到堆中
八、参考文章
- 一篇文章看懂iOS代码块Block
- iOS进阶(一)block与property