14 代码块和并发行

14.1 代码块

说明:代码块是由C语言实现的,是对C语言中函数的扩展。
支持的语言:Objective-CCC++Objective-C++
用途:替代函数或实现闭包
现状:代码块XcodeGCCCLang工具中是有效的,但它不属于ANSIC语言标准。关于代码块的提议已经提交给C语言标准团体。

14.1.1 代码块和函数指针

说明:代码块的语法借鉴了函数指针

  • 返回类型可以手动声明也可以由编译器通过代码块推导
  • 具有指定类型的参数列表
  • 拥有名称
  • 代码放在{}

语法:<returntype> (^blockname)(list of arguments) = ^(arguments){body;}
实现部分推导出返回值类型

1
2
3
4
// 实现部分省略了返回值类型,没有参数列表
void (^theBlock)() = ^{
printf("Hello Blocks!");
};
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 定义并实现代码块 square_block
* 计算乘方
* @param {int} number 数值
* @return {int} number 乘方结果
*/

int (^square_block)(int number) = ^(int number) {
return (number * number);
};
// 调用代码块
int result = square_block(5);
printf("Result = %d\n", result);

14.1.1.1 通过代码块名调用代码块

说明:可以像调用函数一样调用代码块
比函数强大:代码块可以访问与它相同的有效范围内声明的变量。

1
2
3
4
5
6
7
// 声明并初始化变量:声明时的作用域和代码块相同
int value = 6;
// 定义并实现代码块
int (^multiply_block)(int number) = ^(int number) {
// 访问外部同作用域声明的变量
return (value * number);
};

14.1.1.2 直接调用代码块(匿名)

说明:使用代码块时通常不需要创建一个代码块变量,而是在代码中内联代码块的内容。
使用场景:作为参数传递给方法或函数

1
2
3
4
5
6
7
8
// 数组
NSArray *array = [NSArray arrayWithObjects: @"Amir", @"Mishal", @"Irrum", @"Adam", nil];
NSLog(@"Unsorted Array %@", array);
// 传递匿名代码块
NSArray *soredArray = [array sortedArrayUsingComparator: ^(NSString *object1, NSString *object2)] {
return [object1 compare: object2];
};
NSLog(@"Sorted Array %@", sortedArray);

14.1.1.3 使用typedef关键字

说明:代码块声明定义为一种类型,更易于代码的编写。
语法:typedef 代码块定义;
注意:typedef后面的代码块定义中的代码块名不再具备代码块名的功能,而是一种类型名。

1
2
3
4
5
6
7
8
// 将代码块定义为一种类型: MKSampleMultiply2BlockRef
typedef double (^MKSampleMultiply2BlockRef)(double c, double d);
// 使用新类型创建代码块
MKSampleMultiply2BlockRef multiply2 = ^(double c, double d) {
return c * d;
};
// 调用代码块
printf("%f, %f", multiply2(4, 5), multiply2(5, 2));

14.1.1.4 代码块和变量

说明:代码块被声明后会捕捉到创建时的上下文中的变量或函数。

  • 全局变量(包括在封闭范围内声明的本地静态变量)
  • 全局函数
  • 封闭范围内的参数
  • Objective-C的实例变量
  • 代码块内部的本地变量
本地变量

说明:与代码块在同一范围内声明的变量。
捕获情况:代码块会在定义时把本地变量当作常量复制并保存它们的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义代码块类型
typedef double (^MKSampleMultiplyBlockRef)(void);
// 本地变量
double a = 10, b = 20;
// 声明并实现代码块,复制并保存状态
MKSampleMultiplyBlockRef multiply = ^(void) {
reutrn a * b;
};
// 调用代码块
NSLog(@"%f", multiply());// 200

// 修改本地变量
a = 20;
b = 50;
NSLog(@"%f", multiply());// 200
全局变量

说明:可以根据需要将变量标记为静态的(全局的)
捕获情况:同本地变量。

1
2
3
4
5
static double a = 10, b = 20;
MKSimpleMultiplyBlockRef multiply = ^(void) {
return a * b;
};
NSLog(@"%f", multiply());// 200
参数变量

说明:代码块中的参数变量与函数中的参数变量具有相同的作用。

1
2
3
4
5
6
7
8
// 定义代码块类型
typedef double (^MKSampleMultiply2BlockRef)(double c, double d);
// 声明并实现代码块
MKSimpleMultiply2BlockRef multiply2 = ^(double c, double d) {
return c * d;
};
// 调用代码块
NSLog(@"%f, %f", multiply2(4, 5), multiply2(5, 2));
_block变量

关键字:_block
说明:本地变量会被代码块当作常量获取到,如果想要修改它们的值,必须通过_black将它们声明为可修改的。
限制:由两种情况不能使用_block修饰

  • 长度可变数组
  • 包含长度可变数组的结构体
1
2
3
4
5
// 用_block 修饰,使变量 c 在代码块中的副本可修改
_block double c = 3;
MKSampleMultiplyBlockRef multiply = ^(double a, double b) {
c = a * b;
};
代码块内部的本地变量

说明:代码块来说,和本地变量一样使用。

1
2
3
4
5
6
7
8
// 定义并实现代码块
void (^MKSampleBlockRef)(void) = ^(void){
double a = 4;
double c = 2;
NSLog(@"%f", a * c);
};
// 调用代码块
MKSimpleBlockRef();

14.1.2 Objective-C代码块内存管理

说明:代码块是对象,所以可以向它发送任何与内存管理由关的信息。

  • 如果引用了一个Objective-C对象,必须要保留
  • 如果类的方法中的代码块通过引用访问了一个实例变量,要保留一次self(执行所在方法的对象)
  • 如果通过数值访问了一个实例变量,变量需要保留

ProcessString.h:方法中包含代码块的类

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface ProcessStrings : NSObject
@property (strong) NSString *theString;

- (void)testMyString;
@end

ProcessString.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "ProcessStrings.h"

@implementation ProcessStrings
@synthesize theString = _theString;

- (void)testMyString
{
// 代码块1
NSString *string1 = ^{
// 规则2:直接通过引用(实例变量名)访问了实例变量,若没有ARC则应该保留self
return [_theString stringByAppendingString:_theString];
};

NSString *localObject = _theString;

// 代码块2
NSString *string2 = ^{
// 规则3: 通过中间变量间接访问了实例变量,如果没有ARC则要保留localObject
return [localObject stringByAppendingString:localObject];
};
}
@end

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>

#import "ProcessStrings.h"

int main(int argc, const char * argv[])
{
@autoreleasepool
{
ProcessStrings *myStringProcessor = [[ProcessStrings alloc] init];
myStringProcessor.theString = @"Hello Objective Blocks!";

// 调用对象中包含代码块的方法
[myStringProcessor testMyString];
}
return 0;
}

扩展:在C语言中,必须使用Block_copy()Block_release()函数来适当地管理内存。

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>

typedef void (^MKSampleVoidBlockRef)(void);

int main(int argc, const char * argv[])
{
@autoreleasepool
{
MKSampleVoidBlockRef block1 = ^{
NSLog(@"Block1");
};
block1();

MKSampleVoidBlockRef block2 = ^{
NSLog(@"Block2");
};
block2();
Block_release(block2);

block2 = Block_copy(block1);
block2();
}
return 0;
}

14.2 并发性

说明:能够在同一时间执行多个任务的程序称为并发的(concurrent)程序。苹果公司提供了多种可以利用多核特性的API

相关技术选择 说明 备注
POSIX线程 利用并发行最基础的方法是使用POSIX线程来处理程序的不同部分使其能够独立运行。POSIX线程拥有支持CObjective-C的API。 因为线程是级别较低的API,必须手动管理,挑战很大
GDC(Grand Central Dipatch) 运行在系统级别,减少了不少线程管理的麻烦 可以平衡应用程序所有内同的家在,从而提高计算机或设备的运行效率

14.2.1 同步

关键字:@synchronized
说明:用来设置临界区,确保多个线程不会在同一时间进入临界区
相关:@property指令的atomic特性会让编译器通过插入@synchronize(mutex, atomic)生成强制彼此互斥的gettersetter方法(降低了代码性能),而nonatomic特性(默认)则不会。

14.2.1.1 选择性能

说明:NSObject提供了一些可以使代码在后台以较低性能运行的方法(方法名带有performSelector前缀)

performSelectorInBackground实例方法

说明:通过创建一个线程,在后端运行一个指定的方法。
限制:指定运行的方法(第一个参数)要遵从以下限制

  • 方法中需要@autoreleasepool
  • 方法不能有返回值,参数最多一个且必须为id类型
  1. - (void) myMethod;
  2. - (void) myMethod:(id)myObject;

原型:NSObject

1
2
3
4
5
/**
* @param {SEL} 希望在后台运行的方法
* @param {id} object 可以传递一个对象
*/

(void) performSelectorInBackground:(nonnull SEL) withObject:(nullable id)>

SelectorTester.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface SelectorTester : NSObject

- (void) runSelectors;

@end

SelectorTester.m

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
#import "SelectorTester.h"

@implementation SelectorTester

/**
* 封装对性能选择器的调用
*/

- (void) runSelectors {
// 在后端运行不带参数的方法
[self performSelectorInBackground:@selector(myBackgroundMethod1) withObject:nil];
// 在后端运行带一个参数的方法
[self performSelectorInBackground:@selector(myBackgroundMethod2:) withObject:@"Hello Selector"];
NSLog(@"Done performing selectors");
}

/**
* @pravite
* 会被选择器调用的方法:不带参数
*/

- (void) myBackgroundMethod1 {
@autoreleasepool {
NSLog(@"myBackgroundMethod1");
}
}

/**
* @pravite
* 会被选择器调用的方法:只有一个参数
* @param {id} object 一个对象
*/

- (void) myBackgroundMethod2: (id)object {
@autoreleasepool {
NSLog(@"myBackgroundMethod2 %@", object);
}
}

@end

14.2.1.2 调度队列

说明:GDC可以使用调度队列(dispatch queue),共有3种。

队列类型 说明 并行/串行 备注
连续队列 根据指派的顺序执行任务 串行,先入先出(FIFO,栈) 可以创建多个连续队列,彼此并行
并发队列 并发执行一个或多个任务 并行,根据指派到队列的顺序开始执行 无法创建,只能从系统提供的并发队列中选择(一共3个)
主队列 应用程序的有效的主队列 主线程只有一个,无所谓串/并行 执行的应用程序的主线程任务

调度队列数据类型:dispatch_queue_t

连续队列

说明:只要任务是异步提交的,队列会确保任务根据预定顺序执行,不会发生死锁。
适用:一连串的任务需要按照一定的顺序执行的场景

dispatch_queue_create全局方法

说明:创建连续队列
原型:

1
2
3
4
5
6
/**
* @param {const char *} label 队列的名称
* @param {dispatch_queue_attr_t} attr 队列的特性(可以为NULL)
* @return {dispatch_queue_t} 顺序队列
*/

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
1
2
3
4
// 声明连续队列
dispatch_queue_t my_serial_queue;
// 创建连续队列
my_serial_queue = dispatch_queue_create("com.apress.MySerialQueue1", NULL);
并发队列

说明:并发调度队列适用于那些可以并行运行的任务

  • 开始执行时间遵从FIFO
  • 任务可以在前一个任务结束前就开始执行
  • 一次所运行的任务数量是无法预测的(根据其它运行的任务的状况)

技巧:如果需要确保每次运行的任务的数量都是一样的,可以通过线程API来手动管理线程。

dispatch_get_global_queue全局方法

说明:获取系统的并发队列。
原型:/usr/include/dispatch/queue.h

1
2
3
4
5
6
/**
* @param {long} identifier 优先级选项
* @param {unsigned long} flags 标记(可以为0)
* @return {dispatch_queue_t} 顺序队列
*/

dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
1
2
3
4
// 声明并发队列
dispatch_queue_t my_global_queue;
// 获取并发队列(默认优先级)
my_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主队列
dispatch_get_current_queue全局方法

说明:获取当前运行的队列代码块,如果在代码块的对象之外调用了这个函数,则它会返回主队列
注意:该方法在从OS X 10.9ios 6开始被废弃,因为GCD队列本身是不可重入的,同步阻塞会导致死锁。
用途:仍然可以作为调试手段在代码中使用。

1
2
// 获取主线程或当前队列
dispatch_queue_t theQueue = dispatch_get_current_queue();

14.2.2 队列也要内存管理

说明:调度队列是引用计数对象,可以使用dispatch_retain()dispatch_release()来修改队列的保留计数器的值。

14.2.2.1 队列的上下文

说明:可以向调度对象(包括调度队列)指派全局数据上下文,可以在上下文中指派任意类型的数据,比如Objective-C对象或指针。
内存管理:必须在需要队列上下文的时候分配内存并在队列销毁之前进行清理。

dispatch_set_context全局方法

说明:为指定队列设置全局上下文
原型:/usr/include/dispatch/object.h

1
2
3
4
5
/**
* @param {dispatch_object_t} object 调度队列
* @param {void *} context 上下文
*/

void dispatch_set_context(dispatch_object_t object, void *context);
dispatch_get_context全局方法

说明:获得调度队列全局数据上下文
原型:/usr/include/dispatch/object.h

1
2
3
4
5
/**
* @param {dispatch_object_t} object 调度队列
* @return {void *} 全局数据上下文
*/

void * dispatch_get_context(dispatch_object_t object);
1
2
3
4
5
6
7
8
9
10
// 创建可变字典(作为全局数据上下文)
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5];
// 为全局上下文添加队列需要的数据
[myContext setObject:@"My Context" forKey:@"title"];
[myContext setObject:[NSNumber numberWithInt:0] forKey:@"value"];
// 为连续队列 my_serial_queue 设置全局数据上下文
dispatch_set_context(my_serial_queue, (__bridge_retained void *)myContext);

// 提取全局数据上下文(桥接转换__bridge,告诉ARC不想自己来管理上下文的内存)
myContext = (__bridge NSMutableDictionary *)dispatch_get_context(my_serial_queue);

14.2.2.2 全局数据上下文内存管理

说明:编写一个终结器(finalizer)函数,在dealloc中调用。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 终结器函数
* @param {void *} context 全局数据上下文对象
*/

void myFinalizerFunction(void *context) {
NSLog(@"myFinalizerFunction");
// 桥接转换:将全局数据上下文对象桥接转换为具体类型
// 其中,__bridge_transfer 将拥有权限转移到了本函数中,意味着该对象的内存管理由全局释放池换成了我们的函数
NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary*)context;
// 清空作为全局数据上下文的对象
[theData removeAllObjects];
}

14.2.2.3 向调度队列添加任务

说明:有两种方式可以向队列中添加任务,每种方式针对代码块函数各有一个调度函数(共4个)

  • 同步:队列会一直等待前面任务结束
  • 异步:添加任务后,不必等待任务,函数会立刻返回(推荐,因为不会阻塞其他代码的运行)

|**|同步|异步|
|代码块|dispatch_sync|dispatch_async|
|函数|dispatch_sync_f|dispatch_async_f|

dispatch_sync全局函数

说明:向队列中同步添加代码块
原型:/use/include/dispatch/queue.h

1
2
3
4
5
/**
* @param {dispatch_queue_t} queue 调度队列
* @param {dispatch_block_t} block 代码块
*/

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_async全局函数

说明:向队列中异步添加代码块
原型:/use/include/dispatch/queue.h

1
2
3
4
5
/**
* @param {dispatch_queue_t} queue 调度队列
* @param {dispatch_block_t} block 代码块
*/

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
1
2
3
4
5
6
7
8
9
10
11
// 异步添加代码块:内联方式
dispatch_async(my_serial_queue, ^{
NSLog(@"Serial Task 1");
});

// 异步添加代码块:非内联
typedef void (^dispatch_block_t)(void);
dispatch_block_t myBlock = ^{
NSLog(@"My Prefined block");
};
dispatch_async(my_serial_queue, myBlock);
dispatch_sync_f全局函数

说明:向队列中同步添加代码块
原型:/use/include/dispatch/queue.h

1
2
3
4
5
6
/**
* @param {dispatch_queue_t} queue 调度队列
* @param {void *} context 需要传递的任意上下文
* @param {dispatch_function_t} work 函数
*/

void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
dispatch_async_f全局函数

说明:向队列中异步添加代码块
原型:/use/include/dispatch/queue.h

1
2
3
4
5
6
/**
* @param {dispatch_queue_t} queue 调度队列
* @param {void *} context 需要传递的任意上下文
* @param {dispatch_function_t} work 函数
*/

void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
1
2
3
4
5
6
7
8
9
// 定义好要添加到队列的函数
void myDispatchFunction (void *argument) {
NSLog(@"Serial Task %@", (__bridge NSNumber *)argument);
// 获得当前队列的全局数据上下文
NSMutableDictionary *context = (__bridge NSMutableDictionary *)dispatch_get_context(dispatch_get_current_queue());
// 在字典中索引
NSNumber *value = [context objectForKey:@"value"];
NSLog(@"value = %@", value);
}
1
2
// 同步向队列中添加函数
dispatch_async_f(my_serial_queue, (__bridge void *)[NSNumber numberWithInt:3], (dispatch_function_t)myDispatchFunction);

14.2.2.3 调度队列的暂停和重启

dispatch_suspend全局方法

说明:暂停队列
原型:/use/include/dispatch/object.h

1
2
3
4
/**
* @param {dispatch_object_t} object 要暂停的队列
*/

void dispatch_suspend(dispatch_object_t object);
dispatch_resume全局方法

说明:重启队列
原型:/use/include/dispatch/object.h

1
2
3
4
/**
* @param {dispatch_object_t} object 要暂停的队列
*/

void dispatch_resume(dispatch_object_t object);
1
2
3
4
// 暂停队列
dispatch_suspend(my_serial_queue);
// 重启队列
dispatch_resume(my_serial_queue);

14.2.3 操作队列

说明:有一些称为操作(operation)API,可以让队列使用起来更加简单。

  1. 创建一个操作对象
  2. 将其指派给操作队列
  3. 操作被队列执行

操作的创建方式

说明:一共有3种方式

方式 说明
调用操作(NSInvocationOperation 前提是已经拥有一个可以完成工作的类,并且想在队列上执行
代码块操作(NSBlockOperation 类似包含了要执行代码块的dispatch_async函数
自定义的操作 通过继承NSOperation定义自己的操作
方式一:创建调用操作
1
2
3
4
5
6
7
8
9
10
11
12
@implementation MyCustomClass

- (NSOperation *)operationWithData: (id)data {
return [[NSInvocationOperation alloc] initWithTarget: self selectorL @selector(myWorkerMethod:)object:data];
}

// 做具体工作的函数
- (void)myWorkerMethod: (id)data {
NSLog(@"My Worker Method %@", data);
}

@end
方式二:创建代码块操作

说明:创建时作为参数的代码块的类型和在调度队列中使用的相同。

  • 一旦创建了第一个代码块操作,便可以通过addExecutionBlock方法继续添加更多的代码块
  • 根据队列的类型,代码块会分别以连续或并行的方式运行
1
2
3
4
5
6
7
8
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock: ^ {
// 具体工作内容
}];

// 通过代码块操作,继续添加代码块
[blockOperation addExecutionBlock:^{
// 更多要做工作
}];

向队列中添加操作

说明:可以使用NSOperationQueue来取代之前使用的dispatch_queue函数,特点如下

  • 并发执行操作
  • 具有相关性,也就是说,如果某个操作是基于其它操作的,则也会先被执行

技巧:如果要确保添加的操作是连续执行(串行)的,可以设置最大并发操作数为1,这样会按照先入先出的规范执行

1
2
3
4
5
6
7
8
9
10
// 获取队列
NSOperationQueue *currentQueue = [NSOperation currentQueue];

// 添加操作
[theQueue addOperation:blockOperation];

// 也可以添加代码块替代操作对象
[theQueue addOperationWithBlock:^{
NSLog(@"my work");
}];

14.3 小结