-
什么是Block
OC作为C语言的超集,将面向过程的C语言扩展成了一门动态的面向对象语言,其中Block就是OC对C语言中的函数指针、结构体进行扩展而成的新的特色语法,block本质是一个代码块,你也可以把block理解成能够作为OC对象进行传递的匿名函数,并且是可以直接定义在其他函数内部并共享该函数内所有变量的匿名函数。
-
如何使用Block
既然block是OC的对象,那么我将通过用OC的NSString对象进行类比的方式帮助你更好的了解它。现有如下代码:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *myString = @"这是一个字符串对象";
[self stringTest:myString];
}
- (void)stringTest:(NSString *)pString{
NSLog(@"%@",pString);
}
上述代码很简单,把string对象传递给stringTest方法,在该方法中对该对象进行了打印操作。现在,我们依样画葫芦,用同样的形式传递一个block对象,类似的,block的形参和实参的声明如下:
- (void)viewDidLoad {
[super viewDidLoad];
void (^myBlock)(int, int) = ^void (int a, int b) {
NSLog(@"计算结果=%d",a+b);
};
[self sumFunction:myBlock];
}
- (void)sumFunction:(void(^)(int a,int b))block{
block(2,3);
}
第一次接触block的新手看到以上代码或许会感到费劲,我们一步一步来解析这个语法稍显"别扭"的block。
void (^myBlock)(int, int) = ^void (int a, int b) {
NSLog(@"计算结果=%d",a+b);
};
这是Block的完整定义,等号左边从左往右看,该block的返回类型为void、变量名叫myBlock、^符号用于申明myBlock是一个block类型的变量、block入参为两个int型变量,等号右边则为myBlock的具体内部实现,用{}将实现代码包裹起来。看到这里你是否觉得block和函数越发类似,有返回值类型,有入参。如果你熟悉C语音,你会发现等号左边的申明方式和C语言中的函数指针非常相似,仅把*变成了^而已,当然myBlock变量实际上是一个结构体,而非单独一个指针,等号右边实际上则是一个没有函数名的匿名函数,将一个匿名函数的实现赋值给myBlock结构体中的一个指针,就构成了这样一个完整的block型变量。而myBlock变量根据其作用域不同决定了其可以在对象内部,甚至对象间进行传递。在实际开发中,我们常常会将myString申明为一个属性,以供本类中其他方法读写,现在,我们同样将myBlock申明为一个属性,通过申明属性的方式,可以让代码看起来更加清晰明了,申明方式如下:
//用typedef将MyBlock自定成一个类型名
typedef void(^MyBlock)(int a,int b);
#import "ViewController.h"
@interface ViewController ()
//block创建在栈区,使用copy修饰
@property(nonatomic,copy)MyBlock myBlock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^void (int a, int b) {
NSLog(@"计算结果=%d",a+b);
};
[self sumFunction:self.myBlock];
}
- (void)sumFunction:(MyBlock)pBlock{
pBlock(2,3);
}
@end
通过这种方式申明block,比第一种方式更清晰明了。需要注意的是,使用typedef重命名时,(^MyBlock)中的MyBlock被抽象了一种自定义类型名而不再是变量名,self.myBlock中的myBlock才是被作为变量进行传递。在此笔者希望读者都能使用typedef的方式申明Block,这样不论是形参又或实参的申明,都能使用你自己取的MyBlock类型名来直接创建对象。这种方式更接近于我们平时的代码习惯,现在仔细观察以上代码,我们不难发现viewDidLoad方法和sumFunction方法之间,进行了一次简单的"通信",我们先在viewDidLoad方法中创建了self.myBlock变量,即在viewDidLoad方法中内联了一个匿名函数,我们知道self.myBlock的内部实现,但我们暂时还不想要执行这个self.myBlock对象内的实现代码,直到代码执行到sumFunction方法,在sumFunction方法内才又反向调用了这个Block。嗯,看上去很不错,但实际好像并没有什么用处。事实上,在对象内部方法之间使用block通信的确有些多此一举,实际开发中也很少用到。因为你完全可以把self.myBlock的实现重新定义成新的方法,进行两次正向调用。其实block的真正用武之地确实并不在此,接下来,请阅读如下较复杂的常见场景:
在UIController的viewDidLoad方法中,我们初始化页面的同时还需要异步的从接口获取页面数据从而完成对view的渲染,假设你的UIController已经十分臃肿,你不希望UIController再负责网络请求的逻辑,于是你写了一个URLRequestManager类来专门负责网络请求业务。当你需要发起URL请求时,只需要实例化这个manager,由他发起请求即可,Controller并不关心manager的内部实现代码,也不关心何时完成请求,只需要在请求成功或者失败时的结果告诉控制器即可,控制器会在拿到数据后将数据赋值给view,完成界面的最终显示。
对于上述需求,我们就可以通过block来达到目的。代码如下:
#import "ViewController.h"
#import "URLRequestManager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//创建页面
[self initSubViews];
//数据请求
[self requestData];
}
- (void)initSubViews{
//view创建代码实现
}
- (void)requestData{
//发起请求
[URLRequestManager requestWithUrl:@“url地址”
parameters:nil
backHandler:^(BOOL isSucessful, NSError *error, NSData *data) {
if (isSucessful) {
//拿到数据
NSLog(@"请求成功");
NSLog(@"%@",data);
//可以在这里进行界面赋值
}else{
//弹出错误提示
NSLog(@"%@",error);
}
}];
}
@end
URLRequestManager提供了一个网络请求方法,URLRequestManager申明和实现如下:
#import <Foundation/Foundation.h>
typedef void(^RequestBackHandler)(BOOL isSucessful, NSError *error,NSData *data);
@interface URLRequestManager : NSObject
/**
发起网络请求
@param url 请求地址
@param parameters 请求体
@param handler 回调Block
*/
+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(requestBackHandler)handler;
@end
#import "URLRequestManager.h"
@implementation URLRequestManager
+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(RequestBackHandler)handler{
//延时两秒调用block,模拟网络请求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//执行block回调
handler(YES,nil,nil);
});
}
@end
上述代码中,我们就利用block完成了一次对象间的通信,代码简单的还原了异步网络请求的需求,控制器的requestData方法中调用了manager的类方法发起网络请求,并且传入了请求所需的参数以及定义好的block对象,实际使用中,我们不关心类方法的内部实现,只要在其完成请求后再执行调用我们早已经定义好的block对象即可。如果你熟悉代理模式,会发现其实这两者之间相似但又有细微差别,两者主要都用于对象的回调,但block更注重结果的传输,代码更清晰简练,delegate更偏向过程信息的传输,代码更规范严谨。
-
Block的内部构造
前面提到,block对象实际上是一个结构体而非简单的函数指针,现在我们就具体来探索一下Block的神秘本质。block的数据结构定义如下
4.Block内部结构
对应的结构体定义如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
从上面代码看出,一个block实例实际上由以下6部分构成:
isa指针:指向该block类型的类的指针,每个Objective-C对象,都有一个isa
指针,指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身。
flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
reserved:保留变量,我的理解是表示block内部的变量数。
invoke:函数指针,指向block的实现代码地址。
descriptor:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针,copy函数为当block执行copy操作或者当block从栈上拷贝到堆上时调用,dispose函数则是block在堆上释放时调用。
variables:block内部捕获的对象,如void (^blk)(void) = ^{print(fmt,val)};此时,variables中则为fmt和val这两个变量
由于篇幅有限,笔者不再对block的各个部分做具体介绍。
-
Block的类型
根据block的本身的存储位置,block有三种类型,分别如下:
NSGlobalBlock: 类似函数,位于text段;
NSStackBlock : 位于栈内存,仅在函数作用域内有效;
NSMallocBlock: 位于堆内存。
block的类型并非我们创建block时手动指定的,而是编译器根据block捕获的外部变量的不同而自动确定的。以下三个例子分别对应三种类型的block。
{
float (^myBlock)(float, float) = ^(float a, float b){
NSLog(@"heheda");
};
NSLog(@"block is %@", myBlock);
//block is <__NSGlobalBlock__: 0x47d0>
}
{
NSString *str = @"heheda";
NSLog(@"block is %@", ^{
NSLog(@"%@", str);
});
//block is <__NSStackBlock__: 0xbfffdac0>
}
{
NSString *str = @"heheda";
void (^TestBlock)(void) = ^{
NSLog(@"%@", str);
};
NSLog(@"block is %@", TestBlock);
//block is <__NSStackBlock__: 0x75425a0> MRC
//block is <__NSMallocBlock__: 0x75425a0> ARC
}
分析以上三个打印结果,我们得出以下结论:
- 如果block没有捕获任何外部变量,该block所需要的全部信息都能在编译期确定。该block是全局存在的,相当于函数。
- 如果block捕获了自动变量,block存在于栈区,copy操作可以使其存储于堆区。
- 在ARC下,赋值的同时编译器会帮我们进行copy操作,无需手动。
-
Block注意事项
1、若要修改捕获到的自动变量,用__block修饰该变量。
示例:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
int x = 3;
MyBlock myBlock = ^{
x+=1;
};
myBlock();
NSLog(@"%d",x);
}
运行以上代码编译器会报错,并告诉你要将变量x添加__block修饰符。其实这个错误原因很容易理解,学习C语言的时候我们知道,向某个函数传入变量的值,实际上只是将该变量的值赋值给该函数内的形参,函数内部并不能修改这个变量本身,若要修改该变量,应传入其地址。block同样可以通过这种方式达到目的,不同之处在于x是直接被“捕获”而不是作为参数传入,代码如下:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
int x = 3;
int *p = &x;
MyBlock myBlock = ^{
*p +=1;
};
myBlock();
NSLog(@"%d",x);
}
然而这样的代码明显不是我们想要的,因此OC为我们提供了__block修饰符,所以上述代码可以用以下代码代替:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
__block int x = 3;
MyBlock myBlock = ^{
x +=1;
};
myBlock();
NSLog(@"%d",x);
}
将x用__block修饰后,指针p指向x的操作便交由block内部去实现,此外,x在存储方式也发上了变化,由原本的栈区改为了堆区。而对于全局变量和静态变量,我们则可以直接在block内部修改其值。
block内部还可以访问类的实例变量和self变量,且block会按照属性的修饰语义进行引用。这就引出了我们需要特别注意的问题,即循环引用。
2、如果块所捕获的对象直接或间接地保留了块本身,那么就要当心循环引用问题。
示例:
#import "ViewController.h"
typedef void(^MyBlock)(void);
@interface ViewController ()
@property(nonatomic,strong)NSString *myStr;
@property(nonatomic,copy)MyBlock myBlock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^{
self.myStr = @"123";
};
}
@end
运行上述代码,系统会有如下警告
Capturing 'self' strongly in this block is likely to lead to a retain cycle
如果你熟悉OC内存管理机制,你应该知道这是self和myBlock两个对象相互引用,从而导致了内存无法正确释放。为了避免循环引用,可以将代码如下修改(MRC下将__weak替换为__block):
__weak __typeof__(self) weakSelf = self;
self.myBlock = ^{
weakSelf.myStr = @"123";
};
上述情况系统很容易能够检测出,故我们可以排查修改,但有时候会遇到情况较为复杂的情况,编译器未必能发现,而循环引用导致的crash难以追踪,一旦出现非常头疼。所以在使用block的时候,希望读者们能够多多注意这方面的问题。
网友评论