iOS开发之Block详解

Block定义

代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量、作为参数、作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经广泛应用于iOS开发中,常用于GCD、动画、排序及各类回调。一句话来形容Block,带有自动变量(局部变量)的匿名函数。

Block的声明与赋值只是保存了一段代码段,必须调用才能执行内部代码。

Block使用

(1) 标准声明与定义
1
2
3
4
return_type (^blockName)(var_type) = ^return_type (var_type varName) {
// ...
};
blockName(var);
(2) 当返回类型为void
1
2
3
4
void (^blockName)(var_type) = ^void (var_type varName) {
// ...
};
blockName(var);

可省略写成

1
2
3
4
void (^blockName)(var_type) = ^(var_type varName) {
// ...
};
blockName(var);

(3) 当参数类型为void
1
2
3
4
return_type (^blockName)(void) = ^return_type (void) {
// ...
};
blockName();

可省略写成

1
2
3
4
return_type (^blockName)(void) = ^return_type {
// ...
};
blockName();

(4) 当返回类型和参数类型都为void
1
2
3
4
void (^blockName)(void) = ^void (void) {
// ...
};
blockName();

可省略写成

1
2
3
4
void (^blockName)(void) = ^{
// ...
};
blockName();

(5) 匿名Block

Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:

1
2
3
4
^return_type (var_type varName)
{
//...
};

(6) typedef简化Block的声明
1
2
3
4
5
6
7
8
9
10
// 定义一种无返回值无参数列表的Block类型
typedef void(^SayHello)();

// 我们可以像OC中声明变量一样使用Block类型SayHello来声明变量
SayHello hello = ^(){
NSLog(@"hello");
};

// 调用后控制台输出"hello"
hello();

Block捕获外部变量

(1)捕获全局变量和静态全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>

int global_a = 1;
static int global_b = 2;

int main(int argc, const char * argv[]) {
@autoreleasepool {

void (^myBlock)(void) = ^{
global_a++;
global_b++;

NSLog(@"在Block中:global_a = %d, global_b = %d", global_a, global_b);
};

global_a++;
global_b++;

NSLog(@"在Block外:global_a = %d, global_b = %d", global_a, global_b);

myBlock();
}
return 0;
}

输出:

1
2
2018-04-24 20:56:10.676151+0800 Block简书代码[83038:38449696] 在Block外:global_a = 2, global_b = 3
2018-04-24 20:56:10.676303+0800 Block简书代码[83038:38449696] 在Block中:global_a = 3, global_b = 4

可以看到全局变量global_a和静态全局变量global_bBlock中进行了++操作,Block结束后,它们的值依然保留了下来。

通过clang命令将OC转为C++代码来查看一下Block底层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int global_a = 1;
static int global_b = 2;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
global_a++;
global_b++;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s6_gg35g_r90q514w_6xv0vb4fr0000gp_T_main_fd88b4_mi_0, global_a, global_b);
}
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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

global_a++;
global_b++;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s6_gg35g_r90q514w_6xv0vb4fr0000gp_T_main_fd88b4_mi_1, global_a, global_b);

((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
return 0;
}

首先全局变量global_a和静态全局变量global_b的值增加,以及它们被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,他们并没有变成Block结构体__main_block_impl_0的成员变量,因为他们的作用域大,所以可以直接更改他们的值。Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

(2)捕获静态变量和局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) { // 捕获静态变量
@autoreleasepool {
static int static_a = 1;

void (^myBlock)(void) = ^{
static_a++;
NSLog(@"static_a = %d", static_a);
};
myBlock();
}
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2018-04-24 21:11:11.337102+0800 Block简书代码[83204:38537226] static_a = 2


struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_a = __cself->static_a; // bound by copy

(*static_a)++;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s6_gg35g_r90q514w_6xv0vb4fr0000gp_T_main_74a550_mi_0, (*static_a));
}

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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int static_a = 1;

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_a)); // 传入的是静态变量static_a的内存地址

((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

}
return 0;
}

可以看出,Block捕获了静态变量static_a的内存地址,故可以在Block中对静态变量进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) { // 捕获局部变量
@autoreleasepool {
int part_a = 1;

void (^myBlock)(void) = ^{
NSLog(@"part_a = %d", part_a);
};

myBlock();
}
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2018-04-24 21:17:22.128069+0800 Block简书代码[83415:38568895] part_a = 1



struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int part_a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _part_a, int flags=0) : part_a(_part_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int part_a = __cself->part_a; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s6_gg35g_r90q514w_6xv0vb4fr0000gp_T_main_961862_mi_0, part_a);
}

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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int part_a = 1;

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, part_a)); // 传入的是局部变量part_a的值,故无法在Block中对part_a进行修改

((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

}
return 0;
}

我们可以发现,系统自动给我们加上的注释,bound by copy,局部变量part_a虽然被捕获进来了,但是是用 __cself-> part_a来访问的。Block仅仅捕获了part_a的值,并没有捕获part_a的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个局部变量part_a的值,依旧没法去改变Block外面自动变量part_a的值。

OC可能是基于这一点,在编译的层面就防止开发者可能犯的错误,因为局部变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。
Block中不能修改局部变量的值.png

(3)捕获静态变量和局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) { // 捕获局部变量
@autoreleasepool {
__block int part_a = 1;

void (^myBlock)(void) = ^{
part_a++;
NSLog(@"part_a = %d", part_a);
};
myBlock();
}
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2018-04-24 21:35:20.456984+0800 Block简书代码[83648:38653724] part_a = 2



struct __Block_byref_part_a_0 {
void *__isa;
__Block_byref_part_a_0 *__forwarding;
int __flags;
int __size;
int part_a;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_part_a_0 *part_a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_part_a_0 *_part_a, int flags=0) : part_a(_part_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_part_a_0 *part_a = __cself->part_a; // bound by ref

(part_a->__forwarding->part_a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s6_gg35g_r90q514w_6xv0vb4fr0000gp_T_main_61637c_mi_0, (part_a->__forwarding->part_a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->part_a, (void*)src->part_a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->part_a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_part_a_0 part_a = {(void*)0,(__Block_byref_part_a_0 *)&part_a, 0, sizeof(__Block_byref_part_a_0), 1};

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_part_a_0 *)&part_a, 570425344)); // 传入的是part_a的内存地址,故可以在Block中修改它的值

((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

}
return 0;
}

Block循环引用

一般来说我们总会在设置Block之后,在合适的时间回调Block,而不希望回调Block的时候Block已经被释放了,所以我们需要对Block进行copycopy到堆中,以便后用。
Block可能会导致循环引用问题,因为Block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果Block中如果引用了他的宿主对象,那很有可能引起循环引用,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
TestCycleRetain

- (void) dealloc {
NSLog(@"no cycle retain");
}

- (id) init {
self = [super init];
if (self) {
#if TestCycleRetainCase1
//会循环引用
self.myblock = ^{
[self doSomething];
};

#elif TestCycleRetainCase2
//会循环引用
__block TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};

#elif TestCycleRetainCase3
//不会循环引用
__weak TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};

#elif TestCycleRetainCase4
//不会循环引用
__unsafe_unretained TestCycleRetain * weakSelf = self;
self.myblock = ^{
[weakSelf doSomething];
};

#endif NSLog(@"myblock is %@", self.myblock);
}
return self;

}

- (void) doSomething {
NSLog(@"do Something");
}



main

int main(int argc, char * argv[]) {
@autoreleasepool {
TestCycleRetain * obj = [[TestCycleRetain alloc] init];
obj = nil;
return 0;
}
}

  • MRC情况下,用__block可以消除循环引用。
  • ARC情况下,必须用弱引用才可以解决循环引用问题,iOS 5之后可以直接使用__weak

在上述使用Block中,虽说使用__weak,但是此处会有一个隐患,你不知道self什么时候会被释放,为了保证在Block内不会被释放,我们添加__strong。更多的时候需要配合strongSelf使用,如下:

1
2
3
4
5
__weak __typeof(self) weakSelf = self; 
self.testBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf test];
});

Block实现链式编程

我们下面就需要使用这些基础知识来实现链式编程和函数式编程。

场景:

实现加法计算,比如我需要计算1+2+5+14。通常做法如下:
定义加法函数:

1
2
3
-(NSInteger)addWithParam1:(NSInteger)param1 param2:(NSInteger)param2 {
return param1 + param2;
}

然后调用:

1
2
3
4
NSInteger result = [self addWithParam1:1 param2:2];
result = [self addWithParam1:result param2:5];
result = [self addWithParam1:result param2:14];
NSLog(@"%zd",result);

有多少个数字需要相加,我们就需要调用多少次这个方法,相当麻烦。
我们想实现如下效果的调用,类似于masonry,也就是所谓的链式编程,看起来就十分优雅。

1
2
3
int reslut = [NSObject makeCalculate:^(CalculateManager *mgr) {
mgr.add(5).add(6).add(7).add(10);
}];

下面我们就来看看具体的实现过程吧。

1、先定义一个NSObject的分类如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import 

#import "CalculateManager.h"

@interface NSObject (Calculate)

+ (int)makeCalculate:(void(^)(CalculateManager *))block;
@end

==============================================================================

#import "NSObject+Calculate.h"
#import "CalculateManager.h"

@implementation NSObject (Calculate)
+ (int)makeCalculate:(void (^)(CalculateManager *))block
{
// 创建计算管理者
CalculateManager *mgr = [[CalculateManager alloc] init];

// 执行计算
block(mgr);

return mgr.result;
}
@end
2、继续定义一个类实现计算过程,比如add:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import 

@interface CalculateManager : NSObject

@property (nonatomic, assign) int result;

- (CalculateManager *(^)(int))add;
@end


=======================================================


#import "CalculateManager.h"

@implementation CalculateManager

- (CalculateManager * (^)(int))add
{
return ^(int value){
_result += value;
return self;
};
}

@end
3、然后调用:
1
2
3
4
int reslut = [NSObject makeCalculate:^(CalculateManager *mgr) {
mgr.add(5).add(6).add(7).add(10);
}];
NSLog(@"%zd",reslut);

要实现链式调用的一个关键点:就是每次调用add方法必须返回自身,然后才可以继续调用,如此一致循环下去,实现这一切都是Block的功劳。

4、实现过程分析:

上面的步骤3,调用nsobject的分类方法makeCalculate:^(CalculateManager *mgr)block,该方法的参数是一个Block,我们在这里传递一个定义好的Block到该函数。Block的实现是mgr.add(5).add(6).add(7).add(10)
回到步骤1,是分类方法makeCalculate:^(CalculateManager *mgr)block的具体实现,该方法内部初始化一个CalculateManager实例对象mgr,然后作为Block的参数传入block,也就是步骤3的Block内部的mgr参数,然后调用该Block,也就是上一步实现的这句代码mgr.add(5).add(6).add(7).add(10),然后返回执行完毕后的结果,也就是mgr.result
回到步骤2,是链式调用代码mgr.add(5).add(6).add(7).add(10)的关键,可以看到add方法返回的是一个Block,该Block的实现是累加传递进来的值然后赋值给属性result保存下来,然后返回值是self,也就是CalculateManager实例对象。这样又可以实现点语法继续调用add方法。

####Block实现函数式编程
不了解什么是函数编程的童鞋可以看看这篇文章,作为一个入门了解:

函数式编程初探

函数编程有两个好处:

  1. 去掉了中间变量
  2. 把运算过程写成一系列的函数嵌套调用,逻辑更加清楚

还是上面的例子,不过这次我们想如下写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CalculateManager *mgr = [[CalculateManager alloc] init];
[[[[mgr calculate:^(int result){
// 存放所有的计算代码
result += 5;
result *= 5;
return result;
}]printResult:^(int result) {
NSLog(@"第一次计算结果为:%d",result);
}]calculate:^int(int result) {
result -= 2;
result /= 3;
return result;
}]printResult:^(int result) {
NSLog(@"第二次计算结果为:%d",result);
}];

可以看到计算函数calculate和输出函数printResult可以一直循环嵌套调用,所有的运算过程全部聚在一起,看起来逻辑更加清楚。

下面来看看如何实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import 
@interface CalculateManager : NSObject
@property (nonatomic, assign) int result;
- (instancetype)calculate:(int(^)(int))calculateBlock;
-(instancetype)printResult:(void(^)(int))printBlock;
@end

===========================================================

#import "CalculateManager.h"

@implementation CalculateManager
- (instancetype)calculate:(int (^)(int))calculateBlock
{
_result = calculateBlock(_result);
return self;
}

-(instancetype)printResult:(void(^)(int))printBlock{
printBlock(_result);
return self;
}
@end

上面两个函数的关键点在于每次都必须返回self,这样才可以继续嵌套调用其他函数。函数的内部实现是做一些内部处理,然后传入参数来调用block


参考链接:

深入研究Block捕获外部变量和__block实现原理
一篇文章看懂iOS代码块Block
iOS中Block的用法,示例,应用场景,与底层原理解析(这可能是最详细的Block解析)
Block使用场景
网络回调:Block和Delegate的对比
使用FBRetainCycleDetector检测引用循环

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 Acan's blog All Rights Reserved.

访客数 : | 访问量 :