多线程开发的性能

一、简介

  • performSelectorInBackground:withObject: 这是让代码在主线程之外运行的最简单方法,当任务很简单且需求明确时这个方法当效果很好。使用这个方法,系统不会承担有关任务任何额外当管理工作,所以它最适用与非循环当任务。
  • NSOperationQueue:这个方法稍微有点复杂,它提供了一些额外的控制功能,比如串行运行、并发运行或者按照任务间的依赖关系运行。队列操作最适用于重复任务和定义明确的异步任务,比如网络调用和解析。
  • GCD队列:这是让代码在主线程外运行所使用的最底层的方法,其灵活性也是最好的。示例程序将会演示使用GCD队列以串行和并发的方式运行任务。GCD的使用范围很广,从实现后台程序和主队列间的通信到为列表中的每一个元素快速执行代码段都可以使用,还可以处理大型、重复的异步任务。

二、队列介绍

有关并发处理的各种术语会让人觉得很困惑。线程就是其中一个常用术语;在iOS应用的上下文中,一个线程就是一个标准的POSIX线程。从技术上讲,一个线程就是在进程(一个应用可以看作一个进程)中的一组可以独立处理的指令,一个进程可以包含多个线程,它们共享内存和资源。因为线程的功能是独立的,所以可以通过将线程分开工作来获得更快的运行速度。当多个线程需要访问同样的资源或数据时,有可能出现问题。所以iOS应用都具有一个主线程用于处理运行环路和更新UI界面。应用要保持对用户交互都响应,主线程的任务必须是可以在六十分只一秒内完成的任务。

队列是苹果公司在Grand Central Dispatch中提出的用于描述一种上下文的术语。队列是由GCD管理的一组需要执行的任务。根据所处当前系统的情况,GCD会动态确定队列中用来执行的线程个数。主队列是一个由GCD管理的特殊队列,它和主线程相关联。所以当你在主队列运行一个任务是,GCD也会在主线程上执行该任务。

人们会经常认为线程和队列两个概念是可以互换的,一定要记住队列就是一组被管理的线程,而“主”的概念只是对于处理主运行环路和UI的线程而言的。

三、在主线程上运行

运行示例程序,表视图中的5个初始元素都是可见的,不过它们不能滑动,当额外当元素被添加进来时UI完全没有响应。当额外当元素添加时,UI是冻结当,通过在运行应用时查看输出日志和调试控制台就可以看出。冻结当UI显然是糟糕的用户体验,并且遗憾的是,在应用中很容易找到类似的情况。

#import "ICFMainThreadLongRunningTaskViewController.h"

@interface ICFMainThreadLongRunningTaskViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
@end

@implementation ICFMainThreadLongRunningTaskViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        
        NSLog(@"Main Added %@-%d",iterationNumber,i);
    }
    
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems =
    [[NSMutableArray alloc] initWithCapacity:45];
    
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    for (int i=1; i<=5; i++)
    {
        NSNumber *iteration = [NSNumber numberWithInt:i];
        [self performLongRunningTaskForIteration:iteration];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFMainThreadCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}


#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

在这种情况下,调用 sleepForTimeInterval: 方法会很快且经常地阻塞主线程,即使for循环完成后,在所有performLongRunningTaskForIteration:方法没有执行完成之前,主线程仍然没有足够的时间更新UI。

四、在后台运行

从以下代码中可以看到,使用后台执行的方法对 selector 进行设置。NSObject 定义的方法 performSelectorInBackground:withObject:,需要传递一个 Objective-C 对象作为 withObject: 的参数。这个方法将会生成一个新的线程,根据传入的参数在新线程上执行该方法,并立即返回调用线程。这个新的线程由开发者负责管理,所以频繁调用这个方法完全有可能创建许多新的线程而使系统压力增大。如果测试发现有错误出现,可以使用操作队列或调度队列在任务执行过程中进行更准确的控制,以便更好地管理系统资源。

#import "ICFPerformBackgroundViewController.h"

@interface ICFPerformBackgroundViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFPerformBackgroundViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        
        NSLog(@"Background Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:NO];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];    
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:5];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        NSNumber *iteration = [NSNumber numberWithInt:i];
        [self performSelectorInBackground:taskSelector
                               withObject:iteration];
    }
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFBackgroundCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

方法 performLongRunningTaskForIteration: 执行任务的方法同 Main Thread 方法中的一样,不过这里不再是直接向 displayItems 中添加 newArray,而是使用 NSObject 的方法 performSelectorOnMainThread:withObject:waitUntilDone: 调用 updateTableData: 方法。使用这个方法的原因有两个:
首先,包含在表视图中的UIKit对象只有在主线程上才能更新UI界面;
其次,displayItems属性被声明为 nonatomic 类型,这意味着生成的 getter 和 setter 方法并不是线程安全的。要 “修复”这个问题,可以将displayItems 声明为 atomic 类型,这样就需要在数组更新前将其锁定,这会增加性能上的开销。如果属性在主线程上更新,就不需要锁定了。

updateTableData: 方法用于将新创建的元素添加到 displayItems 数组中并通知表视图重载和更新UI界面。
一个有意思的副作用是这些后添加的行的顺序是不确定的,即应用每次启动时都有可能不同。

2017-11-30 13:28:28.712214+0800 LongRunningTasks[895:181158] Background Added 1-1
2017-11-30 13:28:28.713574+0800 LongRunningTasks[895:181159] Background Added 2-1
2017-11-30 13:28:28.714598+0800 LongRunningTasks[895:181161] Background Added 4-1
2017-11-30 13:28:28.722606+0800 LongRunningTasks[895:181160] Background Added 3-1
2017-11-30 13:28:28.723873+0800 LongRunningTasks[895:181162] Background Added 5-1
2017-11-30 13:28:28.881256+0800 LongRunningTasks[895:181158] Background Added 1-2
2017-11-30 13:28:28.882564+0800 LongRunningTasks[895:181159] Background Added 2-2
2017-11-30 13:28:28.883513+0800 LongRunningTasks[895:181161] Background Added 4-2
2017-11-30 13:28:28.897932+0800 LongRunningTasks[895:181160] Background Added 3-2
2017-11-30 13:28:28.899231+0800 LongRunningTasks[895:181162] Background Added 5-2
2017-11-30 13:28:29.054651+0800 LongRunningTasks[895:181158] Background Added 1-3
2017-11-30 13:28:29.055960+0800 LongRunningTasks[895:181159] Background Added 2-3
2017-11-30 13:28:29.057052+0800 LongRunningTasks[895:181161] Background Added 4-3
2017-11-30 13:28:29.064670+0800 LongRunningTasks[895:181160] Background Added 3-3
2017-11-30 13:28:29.065975+0800 LongRunningTasks[895:181162] Background Added 5-3
2017-11-30 13:28:29.228852+0800 LongRunningTasks[895:181158] Background Added 1-4
2017-11-30 13:28:29.231612+0800 LongRunningTasks[895:181159] Background Added 2-4
2017-11-30 13:28:29.232979+0800 LongRunningTasks[895:181161] Background Added 4-4
2017-11-30 13:28:29.233937+0800 LongRunningTasks[895:181160] Background Added 3-4
2017-11-30 13:28:29.234966+0800 LongRunningTasks[895:181162] Background Added 5-4
2017-11-30 13:28:29.398651+0800 LongRunningTasks[895:181158] Background Added 1-5
2017-11-30 13:28:29.400140+0800 LongRunningTasks[895:181159] Background Added 2-5
2017-11-30 13:28:29.400921+0800 LongRunningTasks[895:181161] Background Added 4-5
2017-11-30 13:28:29.402092+0800 LongRunningTasks[895:181160] Background Added 3-5
2017-11-30 13:28:29.410778+0800 LongRunningTasks[895:181162] Background Added 5-5
2017-11-30 13:28:29.565031+0800 LongRunningTasks[895:181158] Background Added 1-6
2017-11-30 13:28:29.565506+0800 LongRunningTasks[895:181161] Background Added 4-6
2017-11-30 13:28:29.566321+0800 LongRunningTasks[895:181160] Background Added 3-6
2017-11-30 13:28:29.565091+0800 LongRunningTasks[895:181159] Background Added 2-6
2017-11-30 13:28:29.581668+0800 LongRunningTasks[895:181162] Background Added 5-6
2017-11-30 13:28:29.677827+0800 LongRunningTasks[895:181158] Background Added 1-7
2017-11-30 13:28:29.678298+0800 LongRunningTasks[895:181160] Background Added 3-7
2017-11-30 13:28:29.731737+0800 LongRunningTasks[895:181161] Background Added 4-7
2017-11-30 13:28:29.731737+0800 LongRunningTasks[895:181159] Background Added 2-7
2017-11-30 13:28:29.748420+0800 LongRunningTasks[895:181162] Background Added 5-7
2017-11-30 13:28:29.845071+0800 LongRunningTasks[895:181160] Background Added 3-8
2017-11-30 13:28:29.846579+0800 LongRunningTasks[895:181161] Background Added 4-8
2017-11-30 13:28:29.852906+0800 LongRunningTasks[895:181158] Background Added 1-8
2017-11-30 13:28:29.902459+0800 LongRunningTasks[895:181159] Background Added 2-8
2017-11-30 13:28:29.924039+0800 LongRunningTasks[895:181162] Background Added 5-8
2017-11-30 13:28:30.015191+0800 LongRunningTasks[895:181160] Background Added 3-9
2017-11-30 13:28:30.016704+0800 LongRunningTasks[895:181161] Background Added 4-9
2017-11-30 13:28:30.029297+0800 LongRunningTasks[895:181158] Background Added 1-9
2017-11-30 13:28:30.078941+0800 LongRunningTasks[895:181159] Background Added 2-9
2017-11-30 13:28:30.098572+0800 LongRunningTasks[895:181162] Background Added 5-9
2017-11-30 13:28:30.181942+0800 LongRunningTasks[895:181160] Background Added 3-10
2017-11-30 13:28:30.220771+0800 LongRunningTasks[895:181161] Background Added 4-10
2017-11-30 13:28:30.224771+0800 LongRunningTasks[895:181158] Background Added 1-10
2017-11-30 13:28:30.248644+0800 LongRunningTasks[895:181159] Background Added 2-10
2017-11-30 13:28:30.265323+0800 LongRunningTasks[895:181162] Background Added 5-10

这是一个事实,所以使用这种技术对于任务何时完成或者说任务执行的顺序都是无法确定的,因为任务都是在不同线程上执行的。如果操作的顺序不重要,就适用该技术;如果顺序重要,操作队列或调度队列就需要以串行的方式执行任务(后面我们会对这两种队列进行描述,分别在“串行操作”和“串行调度队列”两个小节中)。

五、在操作队列中运行

操作队列(NSOperationQueue)可以对一组任务进行管理或操作(NSOperation)。一个操作队列可以指定多个操作并发运行、可以挂起任务和重启、可以取消所有暂停的操作。操作可以是简单的方法调用、代码快或自定义的操作类。操作可以具有使其能够串行运行的依赖关系。操作和操作队列实际上由 Grand Central Dispathc 进行管理,并在调度队列中实现。
以下将逐一介绍并发操作、带有依赖关系的串行操作和支持取消的自定义操作。

5.1 并发操作

运行以下代码,表视图中显示了5个初始元素,在长线任务执行时它们仍然可以滑动。任务完成后,额外的行就可见了。

#import "ICFOperationQueueConcurrentViewController.h"

@interface ICFOperationQueueConcurrentViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueConcurrentViewController

- (IBAction)cancelButtonTouched:(id)sender
{
    [self.processingQueue cancelAllOperations];
}

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Concurrent Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:NO];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
    [self.tableView setTableHeaderView:self.statusView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        [operation setCompletionBlock:^{
            NSLog(@"Operation #%d completed.",i);
        }];
        
        [self.processingQueue addOperation:operation];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationConcurrentCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}


#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

阅读以上代码,了解并发操作是如何设置的。在添加操作前,需要在viewDidLoad:方法中设置操作队列:

self.processingQueue = [[NSOperationQueue alloc] init];

这个方法设置初始数据的方式同 Main Thread 方法一样。在初始数据设置完且视图可见之后,长线任务作为一个 NSInvocationOperation 实例被添加到操作队列中:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        [operation setCompletionBlock:^{
            NSLog(@"Operation #%d completed.",i);
        }];
        
        [self.processingQueue addOperation:operation];
    }
}

每个操作都被分配了一个 completion block, 操作处理结束时会运行这个块代码。performLongRunningTaskForIteration:方法所执行都任务完全同 Perform Background 方式一样,实际上,并发操作上用到都这个方法并没有变化。updateTableData:方法也没有变化。结果也和Perform Background 方式类似,元素都添加也没有确定都顺序。

2017-11-30 13:55:40.130648+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-1
2017-11-30 13:55:40.130731+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-1
2017-11-30 13:55:40.130984+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-1
2017-11-30 13:55:40.130990+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-1
2017-11-30 13:55:40.131134+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-1
2017-11-30 13:55:40.236213+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-2
2017-11-30 13:55:40.236503+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-2
2017-11-30 13:55:40.236647+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-2
2017-11-30 13:55:40.236783+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-2
2017-11-30 13:55:40.236920+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-2
2017-11-30 13:55:40.340567+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-3
2017-11-30 13:55:40.340859+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-3
2017-11-30 13:55:40.341004+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-3
2017-11-30 13:55:40.341141+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-3
2017-11-30 13:55:40.341278+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-3
2017-11-30 13:55:40.444323+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-4
2017-11-30 13:55:40.444609+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-4
2017-11-30 13:55:40.444753+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-4
2017-11-30 13:55:40.444890+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-4
2017-11-30 13:55:40.445027+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-4
2017-11-30 13:55:40.544916+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-5
2017-11-30 13:55:40.545177+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-5
2017-11-30 13:55:40.550485+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-5
2017-11-30 13:55:40.550890+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-5
2017-11-30 13:55:40.551086+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-5
2017-11-30 13:55:40.650279+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-6
2017-11-30 13:55:40.650279+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-6
2017-11-30 13:55:40.655973+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-6
2017-11-30 13:55:40.655979+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-6
2017-11-30 13:55:40.656027+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-6
2017-11-30 13:55:40.755762+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-7
2017-11-30 13:55:40.755762+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-7
2017-11-30 13:55:40.759462+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-7
2017-11-30 13:55:40.761635+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-7
2017-11-30 13:55:40.761635+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-7
2017-11-30 13:55:40.859583+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-8
2017-11-30 13:55:40.861219+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-8
2017-11-30 13:55:40.861464+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-8
2017-11-30 13:55:40.864972+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-8
2017-11-30 13:55:40.867129+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-8
2017-11-30 13:55:40.965054+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-9
2017-11-30 13:55:40.965415+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-9
2017-11-30 13:55:40.965562+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-9
2017-11-30 13:55:40.969690+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-9
2017-11-30 13:55:40.972404+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-9
2017-11-30 13:55:41.070392+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-10
2017-11-30 13:55:41.099595+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-10
2017-11-30 13:55:41.100562+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-10
2017-11-30 13:55:41.100942+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-10
2017-11-30 13:55:41.101133+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-10
2017-11-30 13:55:41.101590+0800 LongRunningTasks[895:186723] Operation #3 completed.
2017-11-30 13:55:41.101729+0800 LongRunningTasks[895:186723] Operation #4 completed.
2017-11-30 13:55:41.101839+0800 LongRunningTasks[895:186723] Operation #1 completed.
2017-11-30 13:55:41.101981+0800 LongRunningTasks[895:191148] Operation #2 completed.
2017-11-30 13:55:41.102147+0800 LongRunningTasks[895:191152] Operation #5 completed.

这里主要都区别是NSOperationQueue现在负责管理线程,并且只有当队列中的线程到达默认最大并发操作时才进行处理。当应用中有很多不同当竞争任务同时出现时这一点很重要,并且需要对其进行管理以避免系统超负荷。

注意:
一个操作队列的默认最大并发量是根据现实中的系统动态确定的,会基于当前系统的负载而变化。也可以为操作队列指定最大并发数量,这种情况下只有当操作并发达到指定当数量时队列才会处理。

5.2 串行操作

运行以下代码,表视图中现实了5个初始元素,在长线任务执行时它们仍然可以滑动。任务完成后,额外当行就可见了。

#import "ICFOperationQueueSerialViewController.h"

@interface ICFOperationQueueSerialViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueSerialViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Serial Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:YES];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];
    
    NSInvocationOperation *prevOperation = nil;
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        //新创建的操作
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        if (prevOperation) {
            //新创建的操作会将一个依赖关系添加到之前的操作,直到前面的操作完成时才运行新的操作
            [operation addDependency:prevOperation];
        }
        //新操作被添加到操作对象数组
        [operationsToAdd addObject:operation];
        
        prevOperation = operation;
    }
    //遍历操作对象数组,将操作添加到队列中
    for (NSInvocationOperation *operation in operationsToAdd) {
        [self.processingQueue addOperation:operation];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationSerialCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

初始数据和操作队列当设置同 并发操作 方式一样。要让操作进程按照正确当顺序串行执行,需要对其设置依赖关系。要完成这个任务,viewDidAppear:方法会添加一个数组,用于保存新创建当操作,并使用NSInvocationOperation(prevOperation)跟踪之前创建当操作。

NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];

NSInvocationOperation *prevOperation = nil;

当创建操作时,该方法会跟踪之前创建当操作。新创建当操作会将一个依赖关系添加到之前的操作,直到前面的操作完成时才运行新的操作。新操作被添加到操作对象数组,然后被添加到队列中。

当所有当操作都创建完成并将它们添加到数组后,就会将它们添加到队列中。由于操作一旦被加到操作队列中就会立即开始执行,因此应该一次添加所有操作,这样队列就可以明确操作间到依赖关系了。

    //遍历操作对象数组,将操作添加到队列中
    for (NSInvocationOperation *operation in operationsToAdd) {
        [self.processingQueue addOperation:operation];
    }

操作队列会对添加到操作和依赖关系进行分析,并确定执行它们的最优顺序。观察调试控制台可以看到,操作按照正确到串行顺序执行。

2017-11-30 14:18:50.109129+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-1
2017-11-30 14:18:50.211273+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-2
2017-11-30 14:18:50.314411+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-3
2017-11-30 14:18:50.418526+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-4
2017-11-30 14:18:50.523856+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-5
2017-11-30 14:18:50.629020+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-6
2017-11-30 14:18:50.734499+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-7
2017-11-30 14:18:50.840192+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-8
2017-11-30 14:18:50.944854+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-9
2017-11-30 14:18:51.047135+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-10
2017-11-30 14:18:51.194856+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-1
2017-11-30 14:18:51.300335+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-2
2017-11-30 14:18:51.405780+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-3
2017-11-30 14:18:51.507019+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-4
2017-11-30 14:18:51.612334+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-5
2017-11-30 14:18:51.717758+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-6
2017-11-30 14:18:51.823200+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-7
2017-11-30 14:18:51.928654+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-8
2017-11-30 14:18:52.032277+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-9
2017-11-30 14:18:52.133318+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-10
2017-11-30 14:18:52.244936+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-1
2017-11-30 14:18:52.350393+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-2
2017-11-30 14:18:52.455856+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-3
2017-11-30 14:18:52.558032+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-4
2017-11-30 14:18:52.662302+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-5
2017-11-30 14:18:52.767775+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-6
2017-11-30 14:18:52.872079+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-7
2017-11-30 14:18:52.977675+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-8
2017-11-30 14:18:53.082886+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-9
2017-11-30 14:18:53.183973+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-10
2017-11-30 14:18:53.295841+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-1
2017-11-30 14:18:53.401299+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-2
2017-11-30 14:18:53.504616+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-3
2017-11-30 14:18:53.608967+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-4
2017-11-30 14:18:53.714370+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-5
2017-11-30 14:18:53.819861+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-6
2017-11-30 14:18:53.921612+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-7
2017-11-30 14:18:54.027221+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-8
2017-11-30 14:18:54.129037+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-9
2017-11-30 14:18:54.234498+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-10
2017-11-30 14:18:54.346365+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-1
2017-11-30 14:18:54.448219+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-2
2017-11-30 14:18:54.553680+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-3
2017-11-30 14:18:54.659157+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-4
2017-11-30 14:18:54.764694+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-5
2017-11-30 14:18:54.870224+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-6
2017-11-30 14:18:54.971051+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-7
2017-11-30 14:18:55.073305+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-8
2017-11-30 14:18:55.177415+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-9
2017-11-30 14:18:55.280122+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-10

串行方式增加了完成所有任务所消耗到时间,不过它能够成功确保任务按照正确到顺序执行。

5.3 自定义操作

运行以下代码,表视图中显示了5个初始元素,在长线任务执行时它们仍然可以滑动。在操作完成前快速点击表视图上方都Cancel按钮。注意,此时任务立即停止了。

#import "ICFOperationQueueCustomViewController.h"

@interface ICFOperationQueueCustomViewController ()
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueCustomViewController

- (IBAction)cancelButtonTouched:(id)sender
{
    [self.processingQueue cancelAllOperations];
}

- (void)updateTableWithData:(NSArray *)moreData
{
    [self performSelectorOnMainThread:@selector(updateTableData:) withObject:moreData waitUntilDone:YES];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];    
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
    [self.tableView setTableHeaderView:self.statusView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];
    
    ICFCustomOperation *prevOperation = nil;
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        ICFCustomOperation *operation =
        [[ICFCustomOperation alloc] initWithIteration:iteration
                                          andDelegate:self];
        NSLog(@"...not cancelled, execute logic here");
        if (prevOperation)
        {
            [operation addDependency:prevOperation];
        }
        
        [operationsToAdd addObject:operation];
        
        prevOperation = operation;
    }
    
    for (ICFCustomOperation *operation in operationsToAdd)
    {
        [self.processingQueue addOperation:operation];
    }
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationCustomCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

初始数据和操作队列都设置几乎同串行操作方式一样。唯一都区别时这里用到都自定义NSOperation子类称作ICFCustomOperation:
ICFCustomOperation.h

#import <Foundation/Foundation.h>

@protocol ICFCustomOperationDelegate <NSObject>

- (void)updateTableWithData:(NSArray *)moreData;

@end

@interface ICFCustomOperation : NSOperation

@property (nonatomic, weak) id<ICFCustomOperationDelegate> delegate;
@property (nonatomic, strong) NSNumber *iteration;

- (id)initWithIteration:(NSNumber *)iterationNumber andDelegate:(id)myDelegate;

@end

ICFCustomOperation.m

#import "ICFCustomOperation.h"

@implementation ICFCustomOperation

- (id)initWithIteration:(NSNumber *)iterationNumber
            andDelegate:(id)myDelegate
{
    if (self = [super init])
    {
        self.iteration = iterationNumber;
        self.delegate = myDelegate;
    }
    return self;
}

- (void)main
{
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        //在for循环开始时,检查取消状态
        if ([self isCancelled])
        {
            break;
        }
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          self.iteration,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Custom Added %@-%d",self.iteration,i);
    }
    
    [self.delegate updateTableWithData:newArray];
}

@end

在for循环开始是,检查取消状态:

 if ([self isCancelled])
        {
            break;
        }

这个检查可以让操作对取消请求做到立即响应。当设计一个自定义操作时,要谨慎考虑取消操作应该如何执行,以及是否需要任何回滚逻辑。
正确处理取消动作不仅对创建一个自定义操作子类有帮助,同时也是一个将复杂逻辑进行封装对有效方法,这样它就可以很好地在操作队列中运行了。

六、在调度队列中运行

调度队列由 Grand Central Dispathc 提供,用于在受控环境内执行一段代码。GCD被设计为最大化实现并发处理,同时基于系统对状态充分利用多核处理器的性能对大量动态分布的队列进行管理。
GCD提供了3中类型的队列,分别是主队列、并发队列、串行队列。主队列是由系统创建的特殊队列,同应用的主线程绑定。在iOS中,可以使用一些全局并发的队列,分为高、普通、低和后台运行这4个优先级队列。私有的并发和串行队列可以由应用创建并一定要像其他应用资源一样被系统管理。

注意:
从iOS6开始,创建的调度队列都由ARC管理,不需要对他们进行retain和release操作。

6.1 GCD并发调度队列

执行以下代码,表视图中显示5个初始元素,在长线任务执行时它们仍然可以滑动。任务完成后,额外的行就可见了。

#import "ICFDispatchQueueConcurrentViewController.h"

@interface ICFDispatchQueueConcurrentViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFDispatchQueueConcurrentViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;

    //用于跟踪所创建元素的 newArray 对象需要一个 _block 修饰符,这样代码块才能更新它
    __block NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    //获取一个对低优先级并发调度队列的引用
    dispatch_queue_t detailQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

    //同步处理整个枚举
    dispatch_apply(10, detailQueue, ^(size_t i)
    {
        [NSThread sleepForTimeInterval:.1];
        
        [newArray addObject:[NSString stringWithFormat:
                             @"Item %@-%zu",iterationNumber,i+1]];
        
        NSLog(@"DispQ Concurrent Added %@-%zu",iterationNumber,i+1);
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    dispatch_queue_t workQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];

        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFDispatchConcurrentCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

注意,这种方式的完成速度明显比前面任何一种方式都要快。

要在 viewDidAppear: 方法中开始一个长线任务,应用需要获得一个对高优先级发调度队列的引用:

dispatch_queue_t workQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

使用 dispatch_get_global_queue 可以访问3个全局且有系统维护的并发调度队列。对这些队列的引用不需要进行 retain 和 release 操作。当队列引用完成后,可以在代码段内编写任务的处理逻辑。

for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];

        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }

使用 dispatch_async 表示逻辑是异步执行的。如果这样,这段代码内的工作将会提交给队列,并且对其调用会立即返回,不会阻塞主线程。该代码还可以使用 dispatch_sync 同步提交给队列,这样在代码段内对逻辑处理完成前调用线程只能在那里等待。

performLongRunningTaskForIteration: 方法中,需要强调与前面方式相比不同的几点。
用于跟踪所创建元素的 newArray 对象需要一个 _block 修饰符,这样代码块才能更新它。

__block NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];

之后该方法会得到一个对低优先级并发调度队列的引用

dispatch_queue_t detailQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

之后,低优先级调度队列会用在强大的GCD技术上,即同步处理整个枚举。

//同步处理整个枚举
    dispatch_apply(10, detailQueue, ^(size_t i)
    {
        [NSThread sleepForTimeInterval:.1];
        
        [newArray addObject:[NSString stringWithFormat:
                             @"Item %@-%zu",iterationNumber,i+1]];
        
        NSLog(@"DispQ Concurrent Added %@-%zu",iterationNumber,i+1);
    });

使用 dispatch_apply 所需的参数包括迭代次数、关于调度队列的引用、用于表示迭代正在进行的变量(一定要为 size_t 类型)和每次迭代需要用到的逻辑代码块。GCD会使用可能的迭代队列填满,在系统约束的范围内它们执行会尽可能同步。这一技术使得该方式在任务执行方面比其他方式都快,如果任务的顺序不重要,这种方式就非常高效。

注意:
方法通过高级别的抽象后,用在集合类中也可以起到同样的效果。比如,NSArray具有一个名为 enumerateObjectsWithOptions:usingBlock:的方法。这个方法可以按数组序列遍历对象,也可以反向遍历或并发遍历。

迭代完成并且由新元素组成的数组创建好之后,dispatch_async 方法需要通知UI更新表视图。

dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });

dispatch_async调用使用函数dispatch_get_main_queue来访问主队列。注意,该技术可以在任何地方访问主队列,并且可以很容易地更新UI,通知长线任务当前的状态。

6.2 GCD串行调度队列

执行以下代码,表视图中显示了5个初始元素,在长线任务执行时它们仍然可以滑动。任务完成后,额外的行就可见了。这种方式没有并发调度队列方式那么快,不过可以按照任务添加到队列中的顺序来执行它们。

#import "ICFDispatchQueueSerialViewController.h"

@interface ICFDispatchQueueSerialViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFDispatchQueueSerialViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:[NSString stringWithFormat:
                            @"Item %@-%d",iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"DispQ Serial Added %@-%d",iterationNumber,i);
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    //创建一个串行队列
    dispatch_queue_t workQueue =
    dispatch_queue_create("com.icf.serialqueue", NULL);
    
    //用异步的方法向串行队列中添加实际工作的代码段
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFDispatchSerialCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

要在 viewDidAppear: 中开始一个长线任务,应用需要创建一个串行调度队列:

dispatch_queue_t workQueue =
    dispatch_queue_create("com.icf.serialqueue", NULL);

可以用异步的方法向串行队列中添加实际工作的代码段:

for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }

performLongRunningTaskForIteration: 方法执行的任务同主线程非常类似,可以后台运行和使用并发操作队列方法;不过这个方法使用 dispatch_async 对主队列调用 updateTableData:方法。

串行调度队列将会按照任务添加到队列中对顺序来执行长线任务:先进先出。调试控制台会显示正确顺序执行对这些操作信息。

2017-11-30 15:19:49.208132+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-1
2017-11-30 15:19:49.313596+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-2
2017-11-30 15:19:49.419005+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-3
2017-11-30 15:19:49.524378+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-4
2017-11-30 15:19:49.626864+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-5
2017-11-30 15:19:49.732279+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-6
2017-11-30 15:19:49.834632+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-7
2017-11-30 15:19:49.940270+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-8
2017-11-30 15:19:50.043781+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-9
2017-11-30 15:19:50.149343+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-10
2017-11-30 15:19:50.277230+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-1
2017-11-30 15:19:50.382699+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-2
2017-11-30 15:19:50.484582+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-3
2017-11-30 15:19:50.590035+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-4
2017-11-30 15:19:50.693442+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-5
2017-11-30 15:19:50.794298+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-6
2017-11-30 15:19:50.895178+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-7
2017-11-30 15:19:50.996306+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-8
2017-11-30 15:19:51.100765+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-9
2017-11-30 15:19:51.204737+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-10
2017-11-30 15:19:51.311088+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-1
2017-11-30 15:19:51.416623+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-2
2017-11-30 15:19:51.522090+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-3
2017-11-30 15:19:51.626325+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-4
2017-11-30 15:19:51.727578+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-5
2017-11-30 15:19:51.828270+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-6
2017-11-30 15:19:51.929195+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-7
2017-11-30 15:19:52.030298+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-8
2017-11-30 15:19:52.131340+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-9
2017-11-30 15:19:52.232437+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-10
2017-11-30 15:19:52.338867+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-1
2017-11-30 15:19:52.439489+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-2
2017-11-30 15:19:52.544343+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-3
2017-11-30 15:19:52.649939+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-4
2017-11-30 15:19:52.754357+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-5
2017-11-30 15:19:52.856091+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-6
2017-11-30 15:19:52.958520+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-7
2017-11-30 15:19:53.061895+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-8
2017-11-30 15:19:53.165070+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-9
2017-11-30 15:19:53.270664+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-10
2017-11-30 15:19:53.377775+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-1
2017-11-30 15:19:53.478816+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-2
2017-11-30 15:19:53.584319+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-3
2017-11-30 15:19:53.689961+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-4
2017-11-30 15:19:53.794692+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-5
2017-11-30 15:19:53.899292+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-6
2017-11-30 15:19:54.004695+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-7
2017-11-30 15:19:54.107205+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-8
2017-11-30 15:19:54.212483+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-9
2017-11-30 15:19:54.318010+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-10

同样,串行方法增加来完成所有任务所需的时间开销,不过可以很好地确保任务按正确的顺序执行。使用调度队列串行执行任务要比管理有依赖关系的操作队列容易,不过没有提供同样高级别的管理选项。

七、小结

本文介绍了几种在不影响UI界面的前提下处理长线任务的技术,包括:

  • performSelectorInBackground:withObject:
  • 操作队列
  • GCD队列

对 NSObject 使用 performSelectorInBackground:withObject: 函数让任务在后台执行是最简单对方法,不过有关这个方法对支持和管理则很有限。

操作队列可以并发或串行地处理任务,这通过使用方法调用、代码块或自定义操作类来实现。操作队列可以指定最大并发操作数,操作还可以挂起和重启,所有尚未完成的操作都可以被取消。操作队列可以处理自定义操作类。操作队列通过GCD来实现。

调度队列也可以并发或串行地处理任务。一共有3个全局并发队列,应用还可以创建自己都串行队列。调度队列能够接手要执行都代码块,可以同步或异步执行代码块。

没有一种技术是“最好”都,因为每种技术都有自身都优势和劣势,所以要通过充分的测试才知道哪一种技术是最适合的。

本文摘自 精通iOS框架 第2版

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容