异步处理<认真写好,日后不断补充>

异步处理


本章将会介绍如何在不中断操作主线程的情况下向应用添加耗时的任务。OC提供了多种不同的方式来解决这个问题,本章将会介绍其中的3种最重要的方式:NSThread、Grand Central Dispatch(GCD)与NS OperationQueue
本章内容

  • 为后台处理创建新的线程
  • 向主线程发送消息以更新用户界面
  • 锁定线程以保持数据结构同步
  • 使用GCD实现异步处理
  • 使用操作队列通过更加面向对象的方式实现异步处理
  • 在不锁定线程的情况下使用顺序队列保护数据结构以提升多线程的性能

1.在新线程中执行处理

问题:应用需要执行非常耗时的任务,但你又希望用户界面保持响应,不会受到新操作的影响

解决方法:将需要长时间执行的任务放到方法中,然后使用NSThread从主线程(也就是重新操作发生的地方)创建线程。

说明:我们将诸如应用之类的可执行程序称为进程,它们是由操作系统执行的(这里指的是iOSs或OSX)。进程是由线程构成的,而线程种的操作是同时执行的。这些操作可能在不同的处理器上同时发生,也可能使用某种分时策略在同一处理器上执行(每个线程都轮流使用计算机的处理器)。
所有程序都至少有一个主要的线程,叫做主线程。应用使用主线程来管理用户界面,但还可能其他线程同时运行,这些线程执行的任务并非与用户界面直接相关,或是作为主线程的一部分。

autoreleasepool:
内存管理对于线程来说非常重要。需要将bigTask方法的代码放到autorelesaepool中,OC可以凭借autoreleasepool使用内存资源,然后在需要时回收资源。每个线程都需要有autoreleasepool,否则应用中会出现内存泄漏

- (void)bigTask{

    @autoreleasepool {
        for (int i =0; i<40000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i",i];
            NSLog(@"%@", newString);
        }
    }
}

- (void)bigTaskAction {

    [NSThread detachNewThreadSelector:@selector(bigTask) toTarget:self withObject:nil];
}

当用户触摸应用中的按钮时,bigTask就会在自己的线程中执行,完全不会妨碍用户界面了

2.主线程与后台线程之间的通信

问题:如果想要从后台线程更新用户界面,那么直到后台线程完成处理后,改变才会发生,这会导致诸如进度条之类的组件失效。你想在后台任务的处理过程中更新用户界面。
解决方案:使用NSObject的performSelectorOnMainThread:withObject:waitUntilDone:方法执行主线程中的方法。你需要将用于更新用户界面(或是主线程)的代码放在自己的方法中。

- (void)updateProgressViewWithPercentage:(NSNumber *)percentDone{

    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
}

- (void)bigTask {

    @autoreleasepool {
        int updateUIWhen = 1000;
        for (int i =0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            if (i == updateUIWhen) {
                float f = (float)i/10000; //计算出完成率
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                [self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:) withObject:percentDone waitUntilDone:YES];
                updateUIWhen = updateUIWhen + 1000;
            }
        }
        // 任务完成后需要向主线程发送另一条消息以确保进度条视图已经被填满
        [self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:) withObject:[NSNumber numberWithFloat:1.0] waitUntilDone:YES];
        [self.myActivityIndicator stopAnimating];
    }

}

- (void)bigTaskAction {

    [self.myActivityIndicator startAnimating];
    [NSThread detachNewThreadSelector:@selector(bigTask) toTarget:self withObject:nil];
}



- (void)viewDidLoad {
    [super viewDidLoad];

    // Create button
    self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    self.myButton.frame = CGRectMake(20, 403, 280, 37);
    [self.myButton addTarget:self action:@selector(bigTaskAction) forControlEvents:UIControlEventTouchUpInside];
    [self.myButton setTitle:@"Do Long Task" forState:UIControlStateNormal];
    [self.view addSubview:self.myButton];

    // Create activity indicator
    self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
    self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
    self.myActivityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
    self.myActivityIndicator.hidesWhenStopped = NO;
    [self.view addSubview:self.myActivityIndicator];

    // Create label
    self.myProgressView = [[UIProgressView alloc] init];
    self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
    [self.view addSubview:self.myProgressView];

}

3.使用NSLock锁定线程

问题:应用使用了多个线程,但有时需要确保两个线程不会使用同一代码块,否则应用可能发生冲突,导致用户产生混乱或是文件被多次访问。

例如:运行《主线程与后台线程之间的通信》应用,但在进度条视图开始填充后再次触摸按钮。如果仔细查看,你会发现进度条视图会前后跳动,因为每个线程都会将进度条视图的值改为该线程当前的百分比值。

解决方案:使用NSLock让其他线程等待,直到当前线程处理完关键代码为止。

- (void)viewDidLoad {
    [super viewDidLoad];

    self.threadLock = [[NSLock alloc] init];
}

- (void)bigTask {

    [self.threadLock lock];
    @autoreleasepool {
        int updateUIWhen = 10;
        for (int i =0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            if (i == updateUIWhen) {
                float f = (float)i/10000; //计算出完成率
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                [self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:) withObject:percentDone waitUntilDone:YES];
                updateUIWhen = updateUIWhen + 10;
            }
        }
        // 任务完成后需要向主线程发送另一条消息以确保进度条视图已经被填满
        [self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:) withObject:[NSNumber numberWithFloat:1.0] waitUntilDone:YES];
        [self.myActivityIndicator stopAnimating];
    }
    [self.threadLock unlock];

}

//按钮触摸两次,在两个后台线程中运行bigTask
//当bigTask运行时,你会看到进度条视图以每次10%的速度填充,直到任务完成为止。注意,进度条视图在填充至100%后会回到0%,接着又会填充至100%,NSLock的行为与预期一致。

4.使用@synchronized锁定线程

问题:应用使用了多个线程 但有时需要确保两个线程不会使用同一代码块,而你同时又想使用除NSLock之外的其他方式。

注意:@synchronized与NSLock解决的都是同样的线程问题,因此本攻略非常类似于使用NSLock锁定线程,但实现方式却不同,@synchronized可以处理异常。此外,@synchronized方式要比NSLock具备更好的性能。

解决方案:要想确保在同一时刻只有一个线程会使用某个代码块,请将整个代码块放在以@synchronized指令开头的花括号中。

5.使用Grand Central Dispatch(GCD)进行异步处理

问题:你想在应用中实现异步处理,打算在更新的OSX与iOS系统中支持自己的应用,并且不打算使用NSThread及各种锁定机制来实现应用的线程安全。

注意:使用了多线程的应用可能会变得更加复杂,有时还会变慢,这是因为开发者需要考虑到代码、资源或数据结构可能在同一时刻被多个线程访问的情况。在短时间内锁定线程可以实现代码的线程安全(多个线程的使用安全)。然而,这么做会导致应用无法充分利用可用资源。

解决方案:GCD解决的问题与NSThread一样,并且在异步执行代码方面遵循同样的基本思路。GCD是一种较新的技术,在拥有多个处理器的计算机上具有更高的效率。GCD需要使用名为“块”的编程技术。块是可看做对象的代码区域。这意味着可以在花括号之间设置代码行,然后将它们看作对象。通常情况下,块被用作方法的参数,这正是块在GCD中的使用方式。

说明:GCD使用块而不是方法(带有@selector指令),这意味着无需将希望执行的代码都放到新方法中。相反,需要从动作方法bigTaskAction中将代码作为参数传递给GCD函数,使用GCD函数dispatch_async可以完成该任务:

- (void)bigTaskAction {

    [self.myActivityIndicator startAnimating];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        int updateUIWhen = 1000;
        for (int i=0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            NSLog(@"%@", newString);
            if (i == updateUIWhen) {
                float f = (float)i/10000;
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                updateUIWhen = updateUIWhen + 1000;
            }
        }
    });
}

下面看看这个GCD函数的第一行代码:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

第一部分是函数名dispatch_async,这是个会异步执行的GCD函数。另有名为dispatch_aync的类似函数会同步执行代码。函数的第一个参数是分发队列:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 会依次返回该应用默认的分发队列。

注意:GCD支持名为代码队列的概念,队列会被调度以在下一个可用的处理器上执行。在使用GCD时,需要指定将代码放到哪个队列中。这里使用的是默认队列,默认队列也可以用于后台处理。还可以使用主队列,主队列类似于用户界面的主线程。

函数的第二个参数是代码块,之所以是代码块,是因为以^符号开头并有开始花括号:
^{ 之后的代码行都是块参数的一部分。当下一个处理器可用时,就会调度执行这些代码。整个GCD函数以代码“)”结束。
到目前为止,所做的事调度bigTask在后台执行。但仍然需要在任务处理过程中更新用户界面。相对于在主线程上执行选择器,还可以使用另一个GCD函数在主线程上更新用户界面。任务应该是同步进行的,因此使用GCD函数及分发队列:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
});

这占据了之前方法所处的位置。在将上述GCD调用放在整个代码块的上下文中时,你只需要使用前面定义的变量而无须担心参数的传递:

- (void)bigTaskAction {

    [self.myActivityIndicator startAnimating];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        int updateUIWhen = 1000;
        for (int i=0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            NSLog(@"%@", newString);
            if (i == updateUIWhen) {
                float f = (float)i/10000;
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
                });
                updateUIWhen = updateUIWhen + 1000;
            }
        }
    });
}

注意:对于GCD分发队列来说,在使用dispatch_async时无法确定代码块的执行顺序。系统会选择最高效的方式。因此,如果顺序很重要(比如更新用户界面的场景),那么请使用dispatch_sync。

最后,你想在任务完成时结束进度条视图的填充并停止活动指示器。为此,使用GCD在代码块的最后针对主线队列调度另一个任务;

- (void)bigTaskAction {

    [self.myActivityIndicator startAnimating];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        int updateUIWhen = 10;
        for (int i=0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            NSLog(@"%@", newString);
            if (i == updateUIWhen) {
                float f = (float)i/10000;
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
                });
                updateUIWhen = updateUIWhen + 10;
            }
        }
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.myProgressView setProgress:1.0 animated:YES];
            [self.myActivityIndicator stopAnimating];
        });
    });
}

总结:一般来说,GCD是实现后台处理的首选方式。如果面相的是更新的系统,那么在确定实现技术时,GCD应该是首选。GCD针对多核应用进行了优化,因此在多核MAC上使用GCD时,应用的性能会有极大的提升。
与NSThread相比,GCD的使用更简单,因为既无需额外的对象,也不必像使用NSThread那样需要编写额外的方法。然而,你会看到使用NSThread完成后台处理的大量示例,决定权在你自己。

6.在GCD中使用顺序队列

问题:使用GCD进行异步处理时,要求块每次都按照它们在代码中出现的顺序执行。例如,上一示例中,当用户在耗时较长的任务运行后再次触摸按钮时,你会遇到与之前相同的问题(进度条视图会来回跳跃)

之前是通过NSLock或@synchronized来解决这个问题,但这么做有一定的代价,这些代价抵消了使用GCD所能带来的一些好处。

解决方案:相对于锁定代码,使用GCD顺序队列加载代码块,这些代码块会按照它们在队列中的顺序执行。可以通过GCD函数dispatch_queue_create(DISPATCH_QUEUE_SERIAL,0)来创建顺序队列。确保顺序队列位于所服务对象的生命周期范围内。

首先需要如下针对顺序队列的属性:

@property dispatch_queue_t serialQueue;

也可以将之作为局部实例放在视图控制器中,只要队列能够按照需要存在于作用域即可。
还需要确保顺序队列是在视图控制器的@synthesize语句中实现的:

@synthesize serialQueue;

将创建顺序队列的代码放在viewDidLoad视图控制器方法中:
self.serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL,0);

该函数需要使用参数来指定待创建的队列的类型。这里使用的是DISPATCH_QUEUE_SERIA,这是因为需要使用顺序队列来确保在同一时刻只会有一个代码块执行并且按照代码块在队列中的顺序执行。

- (void)bigTaskAction {

    dispatch_async(self.serialQueue, ^{
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.myActivityIndicator startAnimating];
        });

        int updateUIWhen = 1000;
        for (int i=0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            NSLog(@"%@", newString);
            if (i == updateUIWhen) {
                float f = (float)i/10000;
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
                });
                updateUIWhen = updateUIWhen + 1000;
            }
        }
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.myProgressView setProgress:1.0 animated:YES];
            [self.myActivityIndicator stopAnimating];
        });
    });
}

总结:按钮触摸两次,在两个后台线程中运行bigTask。
当bigTask运行时,你会看到进度条视图以每次10%的速度填充,知道任务完成为止。这个过程会根据你触摸按钮的次数而不断重复。进度条视图不会再前后跳跃。

7.使用NSOperationQueue

问题:应用需要异步处理,相对于GCD,你想要试用更加面向对象的方式

解决方案:如果像使用GCD,但又不想直接使用GCD库,那么请使用NSOperationQueue

注意:如果要想支持运行在旧系统中的应用,同时又不想使用NSThread和线程锁定,那么NSOperationQueue是理想选择。在使用NSOperationQueue时,实现的细节信息是不可见的。旧系统通过线程来支持NSOperationQueue,而新系统则是使用GCD

NSOperationQueue表示待执行的代码队列。可以通过NSOperationQueue在后台运行代码或是在主队列中运行以响应用户界面动作。

NSOperationQueue可以通过多种方式来添加代码。如果操作系统支持块,那么可以通过addOperationWithBlock:方法直接向队列中添加代码。

如果不支持,就需要将待执行的代码放在NSOperation子类中,NSOperation子类的行为类似于块,因为会将数据与待执行的代码封装到队列中。

说明:相对于使用锁定的线程来说,这里将会使用操作队列与主队列来异步分发代码。

首先,在视图控制器的实现中为主队列与顺序队列分别添加如下局部实例:

NSOperationQueue *serialQueue;
NSOperationQueue *mainQueue;

主队列会向用户界面发送指令。顺序队列会在某个时刻执行代码块,并且按照接收到的顺序执行

mainQueue = [NSOperationQueue mainQueue];
serialQueue = [[NSOperationQueue alloc] init];
serialQueue.maxConcurrentOperationCount = 1;

可以通过NSOperationQueue的mainQueue方法获得指向主队列的引用。这是个单例,因此总是返回主队列的实例。使用alloc与init构造函数创建顺序队列,将maxConcurrentOperationCount设为1,从而将队列设定为顺序队列,这是因为队列一次只能进行一个操作。
创建好队列后,可以使用它们调度bigTaskAction:方法中的块

- (void)bigTaskAction {

    [serialQueue addOperationWithBlock:^{
        [mainQueue addOperationWithBlock:^{
            [self.myActivityIndicator startAnimating];
        }];
        int updateUIWhen = 1000;
        for (int i=0; i<10000; i++) {
            NSString *newString = [NSString stringWithFormat:@"i = %i", i];
            NSLog(@"%@", newString);
            if (i == updateUIWhen) {
                float f = (float)i/10000;
                NSNumber *percentDone = [NSNumber numberWithFloat:f];
                [mainQueue addOperationWithBlock:^{
                    [self.myProgressView setProgress:[percentDone floatValue] animated:YES];
                }];
                updateUIWhen = updateUIWhen + 1000;
            }
        }
        [mainQueue addOperationWithBlock:^{
            [self.myProgressView setProgress:1.0 animated:YES];
            [self.myActivityIndicator stopAnimating];
        }];
    }];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值