目标越接近,困难越增加。但愿每一个人都像星星一样安详而从容地不断沿着既定的目标走完自己的路程。
一. Block的基本概念
1. 什么是Block
苹果在Mac OS X10.6 和iOS 4之后引入了block语法。这一举动对于许多OC使用者的编码风格改变很大。对于block苹果文档上有这样的描述,block对象是一个C语言结构体,可以并入到C和Objective-C代码中。block对象本质上是一个匿名函数,以及与该函数一起使用的数据,其他语言中有时称为闭包或lambda。 block特别适用于回调,或者是在你为了让代码看起来具有更清晰的逻辑进行代码的组合时使用。
上面的解释,告诉我们: 首先,block是一个OC中的对象;并且,这个对象是一个C语言的结构体,它可以使用在C语言和OC中;同时,block本质上是一个匿名函数和其包含的数据集中。这应该就是block的定义了。
2. 为什么要使用Block
在iOS中,在以下情况下通常使用block(摘自于苹果官方文档):
- 代替代理和委托方法;
- 作为回调函数的替代;
- 一次性编写多次处理的任务;
- 方便对集合中的所有项执行任务;
- 与GCD队列一起执行异步任务;
这里告诉我们在以上情况下,我们都能够使用block。我感受最深刻的是使用block进行回调。很多情况下,我们可能只需要对某个事件进行一个单一的回调,也许仅仅就一次,如果我使用代理的话,我需要创建类、编写协议,仅仅对于一个小地方的回调成本很高,那么block登场就恰到好处了。 除此之外,block的特性可以让代理集中在某处,我们只需要在一个地方就可以完成回调之前和回调时的代码,相比,使用回调函数和代理都没有这个优势!另外,我们可以想到,OC中封装好了一些集合的方法,比如,数组的排序,仔细会发现,这里就使用block进行回到操作的。
3. Block声明和定义
我们先看一张苹果官方文档上的图:
3.1)声明一个Block变量
returnType (^blockName)(parameter1, parameter2, ...);
- returnType:
block
返回值类型,如果没有返回值使用void
; - 必须包含blockName并且以
^
开头,^
符号是block
的标志; - 参数列表可以和声明函数一样,只写出形参类型不需写出形参名称;
3.2)定义一个Block基础语法
^ retutnType (parameter1, parameter2, ...){
//block code
};
block
的标志就是^
,所有的block
必须以^
符号开头;returnType
表示返回值类型,可以省略不写,一般都省略不写,编译器会根据上下文自动补充返回值类型;- 参数列表包括参数类型和形参名称;
3.3)定义Block的四种形式
// 1.无返回值、无参数
void (^notReturnNotParameter)(void) = ^{
};
// 2.无返回值、有参数
void (^notReturnIsParameter)(int) = ^(int parameter){
};
// 3.有返回值、无参数
NSString* (^isReturnNotParameter)(void) = ^{
return @"isReturnNotParameter";
};
// 4.有返回值、有参数
NSString* (^isReturnIsParameter)(NSString *) = ^(NSString *name){
return name;
};
3.4)Block类型
returnType (^blockName)(parameter1, parameter2, ...);
上面语法格式为声明了一个block变量blockName
,那么blockName
变量的的类型应该写为:returnType (^)(parameter1, parameter2, ...)
。
我们常使用typedef
定义块变量类型,其语法为:
typdef returnType (^blockTypeName)(parameter1, parameter2,..);
这里的blockTypeName
为块变量类型名,它区别于声明时候的块变量名。定义了块变量类型就能够重复定义块变量。
3.5)Block调用
调用一个block,与C语言调用函数一致;
blockName();
二. Block捕获变量
block捕获变量总结:
-
auto变量,自动变量,平时我们定义
int age = 3
,前面有个auto,auto int age = 3
,系统帮我们默认的加上了一个auto。自动变量auto
修饰的变量,是有捕捉到block内部,并且是属于值传递。 -
静态变量
static
修饰的变量,例如static int age = 3
;static 修饰的age,是会捕获进block内部,并且捕获的是age的地址(指针传递),所以外面age改变的时候,block内部的age也会跟着改变,因为外部和block内部的age指向的是同一地址。 -
全局变量,没有捕获到block的内部,因为全局变量,作用域是所有函数,生命周期是程序结束,所以哪里都能改,哪里都能输出它的值,所以没必要捕获进block的内部,并且无论block上面时候回调,全局变量的值都能正常输出。
- (void)blockTest {
// Block捕获变量
int age = 28;
void (^printAgeBlock)(void) = ^{
NSLog(@"age1 = %ld", age); // age1 = 28
};
NSLog(@"age2 = %ld", age); // age2 = 28
age = 30;
NSLog(@"age3 = %ld", age); // age3 = 30
printAgeBlock();
}
从输出结果可以看出,执行block
之前进行的变量值修改并没有影响到block
块内的代码,这是由于在定义block
块的时候编译器已经在编译期将外部变量值赋给了block
内部变量(称为“值捕获”),在这时候进行了一次值拷贝,而不是在运行时赋值,因此外部变量的修改不会影响到内部block
的输出。
如果捕获是一个指针类型的变量则外部的修改会影响到内部,就和函数传递形参是一样的道理,这个时候block
内部或持有这个对象,并增加引用计数,在block
结束释放后,也会释放持有的对象,减少引用计数,这里需要注意循环引用的问题,在后文中会讲解。
上述block
块内注释了一段给age
重新赋值的代码,因为在block
内部不允许修改捕获的变量。
- (void)blockTest {
NSMutableString *nameMutString = [NSMutableString stringWithString:@"nutName"];
void (^printMutNameBlock)(void) = ^{
// nutName1 = nutName good!
NSLog(@"nutName1 = %@", nameMutString);
};
// nutName2 = nutName
NSLog(@"nutName2 = %@", nameMutString);
[nameMutString appendString:@" good!"];
// nutName3 = nutName good!
NSLog(@"nutName3 = %@", nameMutString);
printMutNameBlock();
}
三. __block的使用
如果希望block
捕获的变量在外部修改后也可以影响block
内部,或是想在block
内部修改捕获的变量,可以使用__block
关键字定义变量。
- (void)blockTest {
// __block
__block NSUInteger height = 65;
void (^printHeightBlock)(void) = ^{
// height1 = 70
NSLog(@"height1 = %ld", height);
height = 75;
};
// height2 = 65
NSLog(@"height2 = %ld", height);
height = 70;
// height3 = 70;
NSLog(@"height3 = %ld", height);
// height1 = 70
printHeightBlock();
// height4 = 75
NSLog(@"height4 = %ld", height);
}
上述代码使用__block
定义变量height
,这样定义以后编译器会在block
定义的时候捕获变量的引用而不是拷贝一个值。这样,外部变量的修改就会影响到block
内部。
__block的使用原理:
- 在block的内部,访问外部的变量时,block内部会对外部的变量进行一次拷贝,在block内部操作的是拷贝之后的副本,不会影响外部的变量,这个变量在堆区;
- 在block内部,修改外部变量,是不被允许的;
- 如果非要在block内部修改外部的变量,需要使用__block修饰外部变量;
- 一旦外部的int变量(在栈区)被__block标记了,如果block内部又修改了这个变量,那么这个变量的地址会永久的被修改在堆区 ;
- 如果外部变量是NSMutableString这样本身就在堆区的,在block内部修改就不会报错;
- 为什么在block的内部不能修改外部的变量? 因为block一般是需要传递给另外一个类里面,block内部的一些变量不能存储在栈区,需要存在堆区,不然数据就容易丢失,这就是使用__block修饰的原因,这样传输数据的时候,数据就不会丢失;
四. Block原理性知识
1. Block循环引用
(1.1)常见循环引用情况及解决办法
使用block最常见的问题就是循环引用问题,循环引用也可能发生在delegate
或NSTimer
中,具体可以自行查阅。
前文讲过如果block
内部访问了外部变量会进行值捕获,block
同样是一个对象,也有引用计数,如果一个对象持有了一个block
而block
内部也捕获了这个对象,那么就会产生循环引用。
@interface HBBlockViewController ()
{
void (^_cycleReferenceBlock)(void);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 循环引用
_cycleReferenceBlock = ^{
NSLog(@"%@",self); // 引发循环引用
};
}
遇到这种代码编译器只会告诉你存在警告,很多时候我们都是忽略警告的,这最后会导致内存泄露,两者都无法释放。跟普通变量存在__block
关键字一样的,系统提供给我们__weak
的关键字用来修饰对象变量,声明这是一个弱引用的对象,从而解决了循环引用的问题;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
__weak typeof(*&self)weakSelf = self;
_cycleReferenceBlock = ^{
NSLog(@"%@",weakSelf); //弱指针引用,不会造成循环引用
};
}
(1.2)嵌套block循环引用情况及解决办法
比如block中有使用block,在调用self的时候又有延迟操作,延迟操作的waekSelf可能已经提前销毁,输出的为null。
__weak typeof(*&self)weakSelf = self;
// 嵌套block
_cycleReferenceBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf); // null
});
};
_cycleReferenceBlock();
优化方式,在内部在用__strong typeof修饰weakSelf:
__weak typeof(self)weakSelf = self;
_cycleReferenceBlock = ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf);
});
};
_cycleReferenceBlock();
对于block这种有趣的特性,在唐巧的谈Objective-C block的实现有详细介绍block的底层实现代码,有兴趣的可以去研究研究。
五. Block内存管理
后续补充!!!!
六. Block应用实例详解
1. Block作为参数传递
定义一个block
块然后作为C函数参数传递:
//使用typedef定义一个无返回值、有一个NSInteger类型的形参的block类型,该block名字为 HBNumberOperationBlock
typedef void (^HBNumberOperationBlock)(NSInteger);
//numberOperator函数,参数为一个numberArray数组和一个HBNumberOperationBlock块类型
void numberOperator(NSArray *numberArray, HBNumberOperationBlock operationBlock) {
NSUInteger count = [numberArray count];
for (NSUInteger i = 0; i < count; i++) {
operationBlock([numberArray[i] integerValue]);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//定义一个数组
NSArray *numberArray = @[@1, @2, @3];
//直接调用numberOperator函数,实现一个匿名的block传入成为“内联块”(inline block),在swift中成为“尾随闭包”(Trailing closure)
// pow(number, 2) = 1
// pow(number, 2) = 4
// pow(number, 2) = 9
numberOperator(numberArray, ^(NSInteger number) {
NSLog(@"pow(number, 2) = %ld", number * number);
});
//定义一个HBNumberOperationBlock
HBNumberOperationBlock operationBlock = ^(NSInteger number) {
NSLog(@"add(number, number) = %ld", number + number);
};
//将上述定义的CJMNumberOperationBlock参数传入
// add(number, number) = 2
// add(number, number) = 4
// add(number, number) = 6
numberOperator(numberArray, operationBlock);
}
return 0;
}
定义一个block
块然后作为OC函数参数传递:
// NSArray+Category.h 文件
// 使用typedef定义一个无返回值、有一个NSInteger类型的形参的block类型,该block名字为 HBNumberOperationBlock
typedef void (^HBNumberOperationBlock)(NSInteger);
@interface NSArray (Category)
// numberOperator函数,参数为一个HBNumberOperationBlock块类型。遍历数组,并按照operationBlock处理数据
- (void)numberOperatorBlock:(HBNumberOperationBlock)operationBlock;
@end
// NSArray+Category.m 文件
@implementation NSArray (Category)
// 遍历数组,并按照operationBlock处理数据
- (void)numberOperatorBlock:(HBNumberOperationBlock)operationBlock {
NSInteger count = [self count];
for (int i=0; i<count; i++) {
operationBlock([[self objectAtIndex:i] integerValue]);
};
}
@end
// 调用
// 007 - Block作为参数传递
- (void)blockParameterTest {
NSArray *number = @[@1, @2, @3];
[number numberOperatorBlock:^(NSInteger number) {
NSInteger pow = number * number;
NSLog(@"pow(number, 2) = %ld", pow);
// pow(number, 2) = 1
// pow(number, 2) = 4
// pow(number, 2) = 9
}];
HBNumberOperationBlock operationBlock = ^(NSInteger number) {
NSInteger add = number + number;
NSLog(@"add(number, number) = %ld", add);
// add(number, number) = 2
// add(number, number) = 4
// add(number, number) = 6
};
[number numberOperatorBlock:operationBlock];
}
2. 仿swift高阶函数
其实上面的Block作为参数传递实例中就已经体现了用block仿swift高阶函数的特性,接下来我们详细研究下其应用。
用过swift的开发者都知道swift的函数调用很好的体现了链式编程的思想,即将多个操作通过.
连接起来,使得可读性更强。这种编程方式的条件之一是每次函数调用必须有返回值。虽然在使用Objective-C开发的过程中,方法的调用是通过[target action]
的方式完成的,但是block本身的调用方式也是通过blockName(parameters)
的方式执行的,与这种链式函数有异曲同工之妙。
在swift中提供了包括map
、filter
、reduce
等十分简洁优秀的高阶函数供我们对数组数据进行操作,同样情况下,遍历一个数组并求和在使用oc(不使用kvc)和swift的环境下的代码是这样的:
#pragma mark - OC code
NSArray numbers = @[@10, @20, @30];
NSInteger totalNumber = 0;
for (NSNumber number in numbers) {
totalNumber += number.integerValue;
}
#pragma mark - swift code
let numbers = [10, 15, 99, 66, 25];
let totalNumber = numbers.reduce(0, { $0+$1 })
无论是代码量还是简洁性,此时的oc都比不上swift。那么接下来就要通过神奇的block来为oc添加这些高阶函数的实现。为此我们需要新建一个NSArray
的分类扩展,命名为NSArray+HBExtension
// NSArray+HBExtension.h文件
// 数组元素转换
typedef id(^HBItemMap)(id item);
typedef NSArray *(^HBArrayMap)(HBItemMap itemMap);
// 数组元素筛选
typedef BOOL(^HBItemFilter)(id item);
typedef NSArray *(^HBArrayFilter)(HBItemFilter itemFilter);
NS_ASSUME_NONNULL_BEGIN
/**
* 扩展数组高级方法仿swift调用
*/
@interface NSArray (HBExtension)
@property(nonatomic, copy, readonly)HBArrayMap map;
@property (nonatomic, copy, readonly) HBArrayFilter filter;
@end
前面说了为了实现链式编程,函数调用的前提是具有返回对象。因此我使用了typedef
声明了几个不同类型的block。虽然本质上HBArrayMap
和HBArrayFilter
两个block是一样的,但是为了区分它们的功能,还是建议这么做。其实现文件如下:
// NSArray+HBExtension.m文件
#import "NSArray+HBExtension.h"
@implementation NSArray (HBExtension)
- (HBArrayMap)map {
HBArrayMap map = ^id(HBItemMap itemMap) {
NSMutableArray *items = @[].mutableCopy;
for (id item in self) {
[items addObject:itemMap(item)];
}
return items;
};
return map;
}
- (HBArrayFilter)filter {
HBArrayFilter filter = ^(HBItemFilter itemFilter) {
NSMutableArray * items = @[].mutableCopy;
for (id item in self) {
if (itemFilter(item)) { [items addObject: item]; }
}
return items;
};
return filter;
}
- (void)setMap:(HBArrayMap _Nonnull)map {}
- (void)setFilter:(HBArrayFilter)filter {}
@end
我们通过重写setter方法保证block不会被外部修改实现,并且在getter中遍历数组的元素并调用传入的执行代码来实现map
和filter
等功能。对于这两个功能的实现也很简单,下面举出两个调用高阶函数的例子:
#pragma mark - 筛选数组中大于20的数值并转换成字符串
NSArray<NSNumber *> * numbers = @[@10, @15, @60, @22, @25, @28.1, @18.6, @11.2, @66.2];
NSArray * result = numbers.filter(^BOOL(NSNumber * item) {
return item.doubleValue > 20
}).map(^id(NSNumber * item) {
return [NSString stringWithFormat: @"string %g", item.doubleValue];
});
#pragma mark - 将数组中的字典转换成对应的数据模型
NSArray<NSDictionary *> * jsons = @[@{ ... }, @{ ... }, @{ ... }];
NSArray<LXDModel *> * models = jsons.map(^id(id item) {
return [[LXDModel alloc] initWithJSON: item];
})
由于语法上的限制,虽然这样的调用跟swift原生的调用对比起来还是复杂了,但通过block让oc实现了函数链式调用的代码看起来也清爽了很多
3. 作为回调函数的替代
在block出现之前,开发者实现回调基本都是通过代理的方式进行的。比如负责网络请求的原生类NSURLConnection
类,通过多个协议方法实现请求中的事件处理。而在最新的环境下,使用的NSURLSession
已经采用block的方式处理任务请求了。各种第三方网络请求框架也都在使用block进行回调处理。这种转变很大一部分原因在于block使用简单,逻辑清晰,灵活等原因。接下来我会完成一次网络请求,然后通过block进行回调处理。
// HBDownloadManager.h文件
@interface HBDownloadManager : NSObject
typedef void(^HBDownloadHandler)(NSData *receiveData, NSError *error);
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(HBDownloadHandler)handler;
@end
// HBDownloadManager.m文件
@implementation HBDownloadManager
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(HBDownloadHandler)handler {
// 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]];
NSData *data = [NSJSONSerialization dataWithJSONObject:parameters options:NSJSONWritingPrettyPrinted error:nil];
[request setHTTPBody:data];
NSURLSession *session = [NSURLSession sharedSession];
// 执行请求任务
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(data, error);
});
}
}];
[task resume];
}
@end
上面通过封装NSURLSession
的请求,传入一个处理请求结果的block对象,就会自动将请求任务放到工作线程中执行实现,我们在网络请求逻辑的代码中调用如下:
[[HBDownloadManager alloc] downloadWithURL:@"" parameters:nil handler ^(NSData * receiveData, NSError * error) {
if (error) { NSLog(@"下载失败:%@", error) }
else {
//处理下载数据
}
}
4. Block传值
传值分为两种,顺传是给需要传值的对象直接定义属性就能传值。逆传可以用代理、block方式实现。具体代码后期有时间再写!!!
七. 最后
block捕获变量、代码传递、代码内联等特性赋予了它多于代理机制的功能和灵活性,尽管它也存在循环引用、不易调试追溯等缺陷,但无可置疑它的优点深受码农们的喜爱。如何更加灵活的使用block需要我们对它不断的使用、探究了解才能完成。