Objective-C 异步任务

Objective-C提供了多种不同的异步处理方式,这里主要看3种比较常用的方式:NSThread、Grand Central Dispatch与NSOperationQueue。


一、NSThread

NSThread是OC提供的线程类,基于此,我们可以很方便地开辟线程。在OS X V10.5之前,该类只提供了一种开启新线程的方法(如:

[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil]
), 该方法调用后线程会马上执行;在OS X V10.5之后(包括V10.5),我们可以用NSThread创建一个线程(),然后通过调用start方法开始执行线程(如:
NSThread *thread=[[[NSThread alloc] initWithTarget:self selector:@selector(task) object:nil] autorelease];
[thread start];
)。


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


二、主线程与后台线程通信

NSObject类提供了performSelectorOnMainThread:withObject:waitUntilDone:方法用执行主线程中方法。我们可以通过这个方法从后台线程通知主线程做一些界面处理。这里说明一下,该方法最后一个参数是一个BOOL值。如果是YES,则表示当前线程会阻塞,直到指定的任务在主线程中执行完;如果是NO,则不阻塞当前线程。


三、线程同步

我们可以使用NSLock锁定线程,然后在任务执行完后再解锁。如:

-(void)task
{
    [threadLock lock];
    @autoreleasepool {
       
       ... ...
    }
    [threadLock unlock];
}

除了NSLock,我们还可以使用@synchronized关键字。@synchronized与NSLock解决的都是同样的线程问题,但实现方式不同,@synchronized可以处理异常。此外,@synchronized方式要比NSLock具备更好的性能


四、GCD

GCD技术在OSX 10.6与IOS 4中被引入,其针对多核应用进行了优化,在拥有多个处理器的计算机上应用的性能会极大提升。GCD的出现减轻了再多核上编程的负担,也减少了不少线程管理的麻烦。GCD是一个系统级别的技术,因此我们可以在任意级别的代码中使用它。GCD决定需要多少线程并安排它们的运行进度。因为它是运行在系统级别上得,所以可以平衡应用程序所有内容的加载。


1、添加任务的方式

有两种方式可以向队列中添加任务。

同步:队列会一直等待前面的任务结束。

异步:添加后,不必等待任务结束,函数会立即返回。推荐优先使用这种方式,因为它不会阻塞其他任务的执行。


2、调度队列

GCD使用调度队列(dispatch queue),它与线程很相似但使用起来更简单。只需要将任务添加到队列中,系统就会执行它。一共有以下3种类型的队列。


连续队列:每个连续队列都会根据指派的顺序执行任务。我们可以按自己的想法创建多个连续队列,它们会并行操作任务。有时候有一串任务需要按照一定的顺序执行,这时便可以使用连续队列,任务执行的顺序为先入先出(FIFO)。

(1)异步添加任务

dispatch_queue_t serial_queue= dispatch_queue_create("com.jerry.SerialQueue", NULL);
    dispatch_async(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::111-------%i",i);
        }
    
    });
    
    NSLog(@"dispatched task 111");
    
    dispatch_async(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::222-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 222");
    
    dispatch_async(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::333-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 333");

下面是执行结果(由于输出内容太长,这里只截取部分):

      


      

从右上图的输出结果可以看出,dispatch_async函数是异步添加任务的;从全局看,三个任务是顺序执行的。


(2)同步添加任务

dispatch_queue_t serial_queue= dispatch_queue_create("com.jerry.SerialQueue", NULL);
    dispatch_sync(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::111-------%i",i);
        }
    
    });
    
    NSLog(@"dispatched task 111");
    
    dispatch_sync(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::222-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 222");
    
    dispatch_sync(serial_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::333-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 333");

下面是执行结果(由于输出内容太长,这里只截取部分):

      



从输出结果可以看出:当第一个任务结束后第二个任务才添加到队列中,当第二个队列结束后第三个队列才添加到队列中。因此dispatch_sync是同步添加任务的。


从上面的两段代码的输出我们知道,顺序队列里任务的执行过程一定是遵循FIFO原则的。dispatch_asnc和dispatch_sync只是决定添加任务的线程是否阻塞。注意,如果要避免出现死锁,那么绝对不要给运行在同一个队列中得任务调用dispatch_sync或dispatch_sync_f(前者是向队列中添加代码块,后者是向队列中添加函数)函数



并发队列:每个并发队列都能并发执行一个或多个任务。任务会根据指派到队列的顺序开始执行。我们无法创建并发队列,只能从系统提供的3种(IOS5之后有4种选择)并发队列中选择。并发队列使用于那些可以并行运行的任务,也遵从FIFO的规范(这里是指任务开始的顺序。即,任务可以在前一个任务结束之前就开始执行)。


下面是文档上对系统提供的并发队列的优先级的说明(文档描述得非常清楚,所以这里就不转述了):



由于我们推荐使用异步添加任务的方式,所以这里只举dispatch_async的例子。

dispatch_queue_t global_queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(global_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::111-------%i",i);
        }
    
    });
    
    NSLog(@"dispatched task 111");
    
    dispatch_async(global_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::222-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 222");
    
    dispatch_async(global_queue, ^{
        for(int i=0;i<100;i++){
            NSLog(@"task::333-------%i",i);
        }
        
    });
    
    NSLog(@"dispatched task 333");

下面是执行结果(由于输出内容太长,这里只截取部分):

     


由上面的左图能看出,三个任务是异步添加的;又右图可以知道,三个任务是并行执行的。



主队列:它是应用程序中有效的主队列,执行的是应用程序的主线程任务。因为主队列是与主线程相关,所以必须小心安排这个队列中的任务顺序,否则它们可能会阻塞主应用程序的运行。通常要以同步方式使用这个队列(我们使用主队列的一般都是做UI刷新任务,UI刷新必须要以同步的方式进行,否则很容出现undefined的情况),提交多个任务并在它们操作完毕后执行一些动作。


我们从子线程中执行提交任务,由于是演示(不涉及到界面刷新),这里分别看看同步提交和异步提交(在实际项目中刷新界面最好是使用同步的方式提交任务):

(1)同步提交任务到主队列


dispatch_queue_t global_queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(global_queue, ^{
         dispatch_queue_t main_queue= dispatch_get_main_queue();
        dispatch_sync(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::111-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 111");
        
        dispatch_sync(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::222-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 222");
        
        dispatch_sync(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::333-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 333");
    
    });


下面是执行结果(由于输出内容太长,这里只截取部分):


     




由上面结果我们得出,三个任务是同步添加到主队列的,主队列里的任务也是串行执行的。


(2)异步提交任务到主队列

dispatch_queue_t global_queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(global_queue, ^{
         dispatch_queue_t main_queue= dispatch_get_main_queue();
        dispatch_async(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::111-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 111");
        
        dispatch_async(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::222-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 222");
        
        dispatch_async(main_queue, ^{
            for(int i=0;i<100;i++){
                NSLog(@"task::333-------%i",i);
            }
            
        });
        
        NSLog(@"dispatched task 333");
    
    });

下面是执行结果(由于输出内容太长,这里只截取部分):


     




由左上图表明,三个任务是异步添加到主队列的;后面两张图也反映主队列是串行执行的。



另外:

a、我们可以通过dispatch_get_current_queue()来找出当前的队列代码块。如果我们在代码块对象之外调用了这个函数,则它会返回主队列。


b、目前为止我们都是通过dispatch_async/dispatch_sync来添加代码块。除此之外,我们还可以通过dispatch_async_f/dispatch_sync_f来添加函数。函数的标准原型必须是下面的形式:

void function_name(void* argument)

例如:

dispatch_async_f(queue, NULL, (dispatch_function_t)function_name);

第一个参数是调度队列,第二个参数是需要传递的上下文(如果没有数据要发送给函数,可以只给NULL),第三个参数是函数。


这里我们是使用异步添加的方式,如果要同步添加就使用dispatch_sync_f函数。

如果出于某个原因我们要暂停队列,可以使用dispatch_suspend函数;队列暂停后,可以调用dispatch_resume函数来重新启用。



3、队列内存管理

调度队列是引用计数对象,可以使用dispatch_retain()和dispatch_release()来修改队列的保留计数的值。与一般对象的retain和release类似。我们只能对自己创建的队列使用这些函数,而无法用在全局调度队列上。事实上,如果我们向全局队列发送这类消息,它们会被直接忽略掉,所以即使这样做也是无害的。


(1)队列的上下文

我们可以向调度对象(包括调度队列)指派全局数据上下文,可以在上下文中指派任意类型的数据。系统只能知道上下文包含了与队列有关的数据,上下文数据的内存管理只能由我们自己来做。我们必须在需要它的时候分配内存并在队列销毁之前进行清理。在为上下文数据分配内存时,可以使用dispatch_set_context()和dispatch_get_context()函数。

dispatch_queue_t serial_queue=dispatch_queue_create("com.jerry.SerialQueue", NULL);
NSArray *data=@[@"data1",@"data2",@"data3"];
dispatch_set_context(serial_queue, (__bridge_retained void*)data);

上面的代码中,我们把NSArray的指针存储到队列的上下文中。这里我们使用__bridge_retained将可保留对象指针(ROP)的所有权转移到不可保留对象指针(non-ROP)上。注意,__bridge_retained关键字只能在支持ARC时使用。因为ARC只会注意到ROP,所以我们要在不用的时候释放掉这块内存


NSMutableArray *contextData=(__bridge_transfer NSMutableArray*)dispatch_get_context(dispatch_get_current_queue());
[contextData removeAllObjects];

上面代码中我们使用__bridge_transfer(同__bridge_retained,这个关键字也只能在支持ARC时使用)将non-ROP的所有权转交给ROP。当函数结束时,ARC会将对象的保留计数器的值减1,如果保留计数器的值减到了0,对象就会被释放。


要在代码中使用上下文数据,我们可以这样做:

NSMutableArray *contextData=(__bridge NSMutableArray*)dispatch_get_context(dispatch_get_current_queue());

这里我们使用__bridge关键字来做转换,这种类型转换会传递指针但不会传递它的所有权。



五、NSOperationQueue

上面介绍的GCD是基于C语言的,它的层级比较低。我们可以通过NSOperationQueue在Objective-C中使用GCD。在使用NSOperationQueue时,实现的细节信息是不可见的。旧系统通过线程来支持NSOperationQueue,而新系统通过GCD来支持。这样我们写程序就容易多了,不必关心底层的实现细节。


如果想要使用操作,首先需要创建一个操作对象,然后将其指派给操作队列,并让队列执行它。一共有3种方式创建队列:

NSInvocationOperation:如果我们已经拥有一个可以完成工作的类,并且想要在队列上执行,可以尝试这种方式。


NSBlockOperation:这有点像包含了需要执行代码块的dispatch_async函数。


自定义的操作:如果我们需要更灵活的操作类型,可以创建自己的自定义类型,但必须通过NSOperation子类来定义操作。


1、添加执行函数

NSInvocationOperation *operation=[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
[queue addOperation:operation];

这里可以添加多个Operation到队列中(这里的添加动作都是异步执行的),这些Operation是并发执行的,如果想让这些队列串行执行,我们可以设置队列的最大并发数为1。

queue.maxConcurrentOperationCount=1;


2、添加执行块

 NSOperationQueue *queue=[[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        @autoreleasepool {
            for(int i=0;i<100;i++){
                NSLog(@"task::-------i==%i",i);
            }
        }
    }];

这里我们只能一次添加一个块到队列中。除此之外,我们还有一种方式可以一次添加多个块到队列中。

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
    NSBlockOperation *operation=[NSBlockOperation blockOperationWithBlock:^{
        @autoreleasepool {
            for(int i=0;i<1000;i++){
                NSLog(@"task::-------i==%i",i);
            }
        }
    
    }];
    //向BlockOperation中添加更多的块
    [operation addExecutionBlock:^{
        @autoreleasepool {
            for(int i=0;i<1000;i++){
                NSLog(@"task::********i==%i",i);
            }
        }

    }];
    //将块操作添加到调度队列
    [queue addOperation:operation];

需要注意的是,与上面我们添加多个Operation不同,这里一个Operation里面的所有块是串行执行的,如果要并发执行(前提是调度队列支持并发),我们还是得通过将不同的块放到不同的Operation里,然后再将这些Operation添加到队列的方式。

上面我们是用自定义的queue,如果要用主队列的queue,可以调用NSOperationQueue的mainQueue方法获取。


六、NSTimer


关于异步任务说了这么多,最后我们看看OC的Timer。我们可以通过下面的方式来创建一个Timer:

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(task) userInfo:nil repeats:YES];


下面是这个方法的描述:



默认情况下,该方法会返回一个新的NSTimer对象并且在当前的run loop中执行。也就是说,如果我们在主线程中调用这个方法创建Timer,那么这个Timer会在主线程中执行。这个与java区别很大。


这里的run loop指的是NSRunLoop对象。下面是NSRunLoop的文档截图:


从这段文档描述我们知道,每个线程都包含一个NSRunLoop对象,其中主线程的NSRunLoop会被主动创建和执行。


所以,如果我们想让我们的Timer在子线程中运行,就不能在主线程的run loop中创建我们的Timer,应该要在另外的run loop中做这件事:

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(task) userInfo:nil repeats:YES];
}];

这里我们在调度队列中创建Timer,让它运行在子线程中。但是,,,但是实际运行的结果是,我们的Timer根本不会被触发。原因是,子线程的run loop并没有执行。所以我们要让子线程的run loop动起来:

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
        NSRunLoop *loop=[NSRunLoop currentRunLoop];
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(task) userInfo:nil repeats:YES];
        [loop run];
}];

这样,我们的Timer就会隔1秒钟执行一次。



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值