片头
一句话搞懂block:可以理解为,block是对代码段的打包,然后在适当的时机执行。
正文
1. 语法
变量声明:返回值类型 (^block对象指针变量名)([形参列表])
类型声明:typedef 返回值类型 (^block类型名)([形参列表])
定义:^([形参列表]){block体}
调用:block对象指针变量([入参列表])
语法例子1:
int (^func)(int, int) = ^(int a, int b) {return a+b};
int result = fun(1,2);// 3
解释:
int (^func)(int, int)
声明了一个返回值类型为int,有两个int型参数的block对象指针func,
^(int a, int b){return a+b;}
定义了一个block体为return a+b的block对象,
int result = func(1,2);
调用block,入参为1和2,block返回了3。
语法例子2:
typedef int (^Func)(int, int);
Func func = ^(int a, int b) {return a-b;};
int result = fun(1,2);// -1
解释:
typedef int (^Func)(int, int);
声明了block的类型,
Func func
声明了Func类型的block指针func,
= ^(int a, int b) {return a-b;};
定义了block对象并赋值给func,
func(1,2);
调用block。
(有C基础的可以继续往下看)
block语法和C的函数指针语法非常相似,比如C函数指针的声明:
// C函数指针声明
// 返回值类型为int,有两个int型参数的函数指针
int (*ptr)(int, int);
typedef (*Ptr)(int, int);
那么现在把 * 换成 ^ ,相信会很好理解了
// block声明
int (^func)(int, int);
typedef (^Func)(int, int);
**实际上block体代码段会被编译器用C重写成静态函数,block的调用的结果是C函数的调用,所以在语法设计上两者会比较相似。
作为形参:( 返回值类型 (^)([block的形参列表]) )形参名 or (block类型)形参名
语法例子3:
// typedef void (^Callback)(NSData *);
// -(void)asyncHttpAndCallback:(Callback)block
-(void)asyncHttpAndCallback:(void (^)(NSData *))block
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 异步加载
NSData *data = ..;
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程回调
block(data);
});
});
}
调用:
// Callback block = ..
void (^block)(NSData *) = ^(NSData *data) {
NSLog(@"下载完成");
NSLog(@"%@",data);
UIImage *image = [UIImage imageWithData:data];
..
};
[self asyncHttpAndCallback:block];
2. 特性
捕获:
block能够跟据上下文捕获外界的变量。通过编译器在编译期,将外界与block体中同名的变量"编译"进block体,从而实现捕获。为了便于说明,用伪代码演示:
特性例子1:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
int i = 1;
void (^block)(void) = ^{
int j = i+1;
NSLog(@"%d", j);
};
block();
return 0;
}
经过编译器的处理后,在block体构造局部变量_block_i,完成"捕获"变量i:
int i = 1;
void (^block)(void) = ^{
int _block_i = i;// 构造局部变量
int j = _block_i+1;
NSLog(@"%d", j);
};
举一反三,为什么如果在block体中进行i++操作(不加__block修饰),编译器会报错:
特性例子2:
int i = 1;
void (^block)(void) = ^{
i++;// 这里报错
};
int i = 1;
void (^block)(void) = ^{
int _block_i = i;
_block_i++;
};
因为i++操作编译后,实际上是对局部变量_block_i++操作,所以编译器很智能地给出了不能在block体内修改变量i的错误提示。
加了__block修饰局部变量之后,就可以在block体内对变量值进行修改了,编译器会做如下处理:
特性例子2:
__block int i = 1;
void (^block)(void) = ^{
i++;// 通过编译
};
int i = 1;
void (^block)(void) = ^{
int *_block_i = &i;// 指向i的指针
(*_block_i)++;
};
可以发现_block_i是指向变量i的指针,所以在block体修改_block_i值,对变量i同样生效。需要一提的是,对全局变量(全局变量,合局静态变量)并不需要用__block修饰。因为对于全局变量“捕获”进来的也是引用(在表现上与天生加上了__block一样)。
上面例子都是用伪代码的表示的。更准确来说,对于每个block,编译后会自动生成一个表示block的结构体,block体被编译成一个静态C函数,“捕获”进来的变量是结构体的成员变量,调用block相当于调用静态C函数来对结构体的成员变量进行操作。比如,下面将Objective-C源码用Clang编译后得到的代码片段,可以看到编译生成了其它很多代码:
(代码片段引用自深入研究Block捕获外部变量和__block实现原理,为读者方便理解,我加上了较多的注释)
#import <Foundation/Foundation.h>
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_i ++;
static_global_j ++;
static_k ++;
NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
};
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
myBlock();
return 0;
}
// 将被捕获的变量
int global_i = 1;
static int static_global_j = 2;
struct __main_block_impl_0 {// block结构体(**编译生成)
struct __block_impl impl;// 实现结构体
struct __main_block_desc_0* Desc;// 描述结构体,用来描述block
// 捕获的变量作为成员变量
int *static_k;
int val;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
// 初始化实现结构体impl
impl.isa = &_NSConcreteStackBlock;// 只要看到is_a指针,就可以知道block在oc中被视为对象
impl.Flags = flags;// 用于内存管理的flags
impl.FuncPtr = fp;// 指向下面的C实现函数
// 描述结构体
Desc = desc;// 保存block的大小、引用计数信息,引用block的拷贝构造、析构函数
}
};
// block体编译出来的静态函数(**编译生成)
// 也就是说,在block体写的代码会转化为以下的C代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_k = __cself->static_k;// 指向静态变量的指针
int val = __cself->val;// 普通局部变量
global_i ++;
static_global_j ++;
(*static_k) ++;// 由于是通过指针(地址)取值,所以进行++后被捕获前的static_k值也会改变
NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
};
// block的描述结构体(**编译生成)
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
//
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
// 构造block结构体
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
// 打印调用block前的变量
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
// 访问block的实现结构体,调用C静态函数
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
引用:
ARC下,被捕获的强指针,其指向的对象会被引用(引用计数增加)。比如 id __strong obj、NSData *data等。obj、data在block被释放前都不会被释放,一个不注意就会容易产生内存泄漏。
使用场景
书写block能使代码可阅读性更强,只有在一个地方(函数)里用到回调的代码,不必特地去写另外一个函数去处理回调结果。
先举个简单的例子
使用场景例子1:
// 获取年龄最大的person对象,其中person有个age属性。
- (Person *)getOldest:(NSArray *)people
{
// 定义block,用于比较大小时调用
Person * (^comparer)(Person *, Person *) = ^(Person *a, Person b) {
return a.age>b.age ? a : b;// 返回年龄比较大的对象
}
//
Person *oldest = nil;
for (int i = 0; i<[people count]; i++) {
Person *cur = people[i];
Person *older = comparer(cur,oldest);// 调用block
oldest = older;
}
//
return oldest;
}
进阶使用:根据不同规则,获取相应的对象
使用场景例子2:
- (void)get:(NSArray *)people rule:(Person * (^)(Person *, Person *))cmper
{
Person *re= nil;
for (int i = 0; i<[people count]; i++) {
Person *cur = people[i];
re = cmper(cur,re);// 回调block
}
}
// 获取id最小的对象
[self get:people rule:^(Person *a, Person *b) {
return a.id<b.id ? a : b;
}];
// 获取年龄最大的对象
[self get:people rule:^(Person *a, Person *b) {
return a.age>b.age ? a : b;
}];
这种写法跟-enumerateObjectsUsingBlock:类似,如何不使用block的话,这时需要写两个规则的比较函数,并传入@selector(method)进行回调。根据个人的编码风格,可以选择不同的写法。
block最最为主要的应用场景,就是作为回调来使用。
比如常见的网络数据请求完成后回调,可以回看语法例子3。
还有一种就是block作为属性,打包到某对象当中,在适当时机让对象访问block并调用。
例子有网上流行的视图控制器之间 block传值:
// 在普通控制器a中,有一个UIImageView用来显示用户头像
// 准备present的登录控制器,有一个block属性
LoginContr *b = [LoginContr new];
b.block = ^(UIImage *img){// 登录成功回调
[this.pic setImage:img];// 设置头像
NSLog(@"已登录");
};
[this presentViewController:b ..];
..
// 在b中,登录完成之后
- (void)doLogin
{
// blablabla..
// 终于登录成功了
UIImage *img = [self getUserAvatar];
self.block(img);// 调用block, a.pic的image被设置并打印log
}
面试常问
Q1:block是什么、如何使用、好处、实现原理?
手动翻一翻上面的吐血知识整理。
Q2:以下的代码有什么问题(如何解决block的循环引用/内存泄漏)?
// .h
typedef void (^Block)(void);
@interface Test
@property (nonatomic, strong) Block block;
@property (nonatomic, assign) int j;
@end
// .m
@implementation
{
int _i = 1;
}
- (instancetype)init
{
if(self = [super init]) {
[self doTest];
}
return self;
}
- (void)doTest
{
self.block = ^{
self.j = 1;
_i = 2;
};
self.block();
NSLog(@"%d", self.j);
NSLog(@"%d", _i);
}
- (void)dealloc
{
NSLog(@"dealloc");// dealloc会打印吗?
}
- 答:- dealloc不会被回调也不会打印。因为Test对象永不被释放,发生了内存泄漏。
- 解释:首先self.block作为strong属性,在self不被释放之前,block是不会被释放的。同时之前说过block会引用强指针指向的对象,因此block捕获并引用了self,导致block不被释放,self也不会被释放,形成了引用循环。于是内存泄漏了。
- 解决思路1:block内不使用强指针。
__weak typeof(self)weakPtr = self;
self.block = ^{
weakPtr.j = 1;
_i = 2;
};
是不是这样就解决了呢?经过测试发现,依然会有内存泄漏的情况发生。这个问题是比较隐蔽的,发生在self.block=^{_i = 2;}
这句话,其等价于:
self.block = ^{
self->_i = 2;// _i相当于self->_i,self被引用了
};
比较合适的做法有2,一是将_i声明为属性,通过与j一样地去访问;二是通过KVC来设置成员变量的值:
__weak typeof(self)weakPtr = self;
self.block = ^{
weakPtr.j = 1;
[weakPtr setValue:@2 forKey:@"_i"];
};
到这里dealloc就完美打印了。
- 解决思路2:使用完block后,不再引用block(block置为nil)
self.block = ^{
self.j = 1;
_i = 2;
};
self.block();
self.block = nil;
将self.block置为nil,self不再引用block,于是block引用计数降为0,block被释放。所以self失去了block的引用,当dealloc时能正常释放内存。相比思路1,这种方案适合block只需要被临时使用一次的情况。
Q3:使用block回调和代理回调(有的会问委托模式、协议)有什么区别?
两者主要的区别有三点,
- 一是在语法使用上:block语法简洁,相对使用代理模式,不要求写额外的协议。而且block自动捕获变量,于是在调用的时候,使用代理方法需要比使用block传入更多的参数。总而言之,使用block突出两个字:“便利”。
- 二是易用性上:使用block留意防止产生循环引用,引起内存泄漏。在这点上使用代理模式,即仅是调用“方法”更加不容易出错。
- 三是执行方式上:使用block,其捕获的局部变量要拷贝到堆内存中,生命周期结束时释放。而使用代理模式,代理对象回调方法,数据以参数方式传入,操作发生栈上。不考虑构造block和构造对象的耗费,仅仅是挎贝几个变量值和指针地址,性能相差也不会很大。
以上就是block的定义与使用和相关的注意点啦。
网友评论