iOS精华iOS 开发每天分享优质文章Objective C开发

iOS 多线程详解

2016-09-07  本文已影响278人  fou7

在了解GCD之前,我们首先要知道几个概念。关于队列和同/异步函数。为了让读者更简单直观的理解这些概念,我尽可能用最简单的话进行解释。

关于队列

主队列:专门在主线程上调度任务。
串行队列:顾名思义,也就是任务一个接着一个的执行(按顺序执行)。
并发队列:顾名思义,任务可以同时执行。
全局队列:本质上还是并发队列。

关于同/异步函数

同步函数:不会开启新线程。在主线程马上执行任务。
异步函数:会开启新线程。不会马上执行任务。

队列和同/异步函数联合使用

主队列+同步函数

上面说过,开启同步任务不会开启新线程,而是在主线程马上执行任务。但是主线程有正在执行的任务,只有主线程的任务执行完毕,才能执行主队列中新加的任务。而主队列也在等待主线程的任务执行完毕,然后执行自己的任务。

主队列+异步函数

上面说过,异步函数不会马上执行任务,并且会开启新线程。但是因为是在主队列中使用异步函数(因为主队列是在主线程上调度任务),所以既不会马上执行任务,也不会开启新线程。而是等主线程的任务全部执行完毕后,等主线程有空了再去执行异步任务。看看代码效果。

-(void)test {
    NSLog(@"touchesBegan");
    // 主队列+异步函数
    // 等主线程任务执行完毕之后,再执行 test2
    dispatch_async(dispatch_get_main_queue(), ^{
          [self test2];
    });
  
    [self test3];     
    NSLog(@"touchesEnd");
}

- (void)test2 {
    NSLog(@"test2-----------%@",[NSThread currentThread]);
}

- (void)test3 {
    NSLog(@"test3-----------%@",[NSThread currentThread]);
}

控制台输出:

output.png

number = 1, name = main,证明任务是在当前线程(主线程)执行的。
使用场景:
1.回到主线程更新UI
2.调整任务执行顺序

同步函数+其他队列

串行队列:会在当前线程同步执行。
并发队列:会在当前线程同步执行,这种使用方式没有意义,不推荐。

异步函数+其他队列

串行队列:因为串行队列是一个执行完后再执行下一个,所以只会多开一条新线程调度任务。
并发队列:因为并发队列是多个任务同时执行,所以会开多条新线程调度任务。

GCD

线程间通信

例:开启一条子线程进行下载图片的耗时操作,图片下载完后回到主线程更新UI。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 开启子线程下载图片
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 下载图片!
        UIImage *image = [self downloadWebImage];
        // 回到主线程显示图片!
        dispatch_async(dispatch_get_main_queue(), ^{
             NSLog(@"显示图片:%@",[NSThread currentThread]);
            // 显示图片!
            // 如果设置按钮的图片,必须改变按钮的类型为 custom!
            //[self.button setImage:image forState:UIControlStateNormal];
            // 设置按钮的背景图片: 
           [self.button setBackgroundImage:image forState:UIControlStateNormal];
        });
    });
 }

// 下载网络图片
// 返回值: 下载好的图片!
-(UIImage *)downloadWebImage{
    NSLog(@"downloadWebImage:%@",[NSThread currentThread]);
    NSString *urlString = @"http://d.hiphotos.baidu.com/image/pic/item/64380cd7912397dd2a7d71d15d82b2b7d1a287db.jpg";
    NSURL *url = [NSURL URLWithString:urlString];
    // 这是一个耗时方法!这一个方法内部封装了很多代码(关于网络的代码!)!
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 下载好的图片
    UIImage *image = [UIImage imageWithData:data];
    return image;
}
调度组

关键字: dispatch_group_t
作用:管理队列的!
(1)参数说明
参数1:标识队列组!
参数2:管理的队列!
参数3:添加到队列中的任务!

dispatch_group_async(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)

(2)关键函数
等待队列组中的任务都执行完毕之后,就会发出通知,调用下面的方法!
队列组: 等待哪一个队列组中的任务执行完毕!
队列: 决定后续的任务在哪条线程执行
任务: 后续的任务!

dispatch_group_notify(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)

需求:下载两张图片,下载完成后,合并两张图片并显示

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 创建一个队列组!
    dispatch_group_t group = dispatch_group_create();

    //变量声明
    __block UIImage *image1 ,*image2;

    // 下载第一张图片
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{       
        image1 = [self downloadImageWithUrlString:@"http://g.hiphotos.baidu.com/image/pic/item/95eef01f3a292df54e0e7e08be315c6035a873da.jpg"];
    });

    // 下载第二张图片
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{       
        image2 = [self downloadImageWithUrlString:@"http://e.hiphotos.baidu.com/image/pic/item/cc11728b4710b912d4bb69ffc1fdfc03924522bc.jpg"];
    });

    // 合并图片并且显示
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合并图片
        UIImage *image = [self bingImageWithImage1:image1 Image2:image2];
        // 显示合并之后的图片!
        self.imageView.image = image;       
    });
}

// 合并图片
-(UIImage *)bingImageWithImage1:(UIImage *)image1 Image2:(UIImage *)image2{
    // 1.开启图形上下文
    UIGraphicsBeginImageContext(self.imageView.bounds.size);
    //2.绘制第一张图片
    [image1 drawInRect:self.imageView.bounds];
    // 3.绘制第二张图片
    [image2 drawInRect:CGRectMake(0, self.imageView.bounds.size.height - 80, self.imageView.bounds.size.width, 80)];
    // 4.获取绘制好的图片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    // 5.关闭图形上下文
    UIGraphicsEndImageContext();

    return image;
}

// 下载图片
-(UIImage *)downloadImageWithUrlString:(NSString *)urlString{
    NSURL *url = [NSURL URLWithString:urlString];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    return image;
}
一次性代码

使用场景:单例。它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保代码只被执行一次。

@interface Person : NSObject
+ (instancetype)sharedPerson;
@end

@implementation Person

+ (instancetype)sharedPerson{
    static id instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    }); 
    return instance;
}
@end
阻塞式函数

使用场景:任务1和任务2同时执行,然后执行任务3,然后任务4和任务5同时执行。
需要注意:阻塞式函数只有自己创建的并发队列才有用,对全局并发队列无效。
关键字: dispatch_barrier_async

- (void)test {
    // 需求:任务 1~5 ,执行顺序:
    // 第三个任务必须等待前两个任务执行完毕之后,再执行!
    // 后两个任务必须等待第三个任务完成之后再执行!
    // 任务1和2 同时执行, 任务4和5同时执行!
    // 阻塞式函数! -- 主要是阻塞并发队列的!(只有自己创建的并发队列才有效,全局并发队列不可以使用阻塞式函数).
    // 1.队列:需要阻塞哪一个队列
    // 2.任务:阻塞队列的任务! 任务3/阻塞式任务!
    // dispatch_barrier_async(<#dispatch_queue_t queue#>, <#^(void)block#>)
    // 创建并发队列,按顺序往并发队列中添加任务!
    // 创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

    // 按顺序往并发队列中添加任务
    dispatch_async(queue, ^{
         //模拟耗时操作
         [NSThread sleepForTimeInterval:5];
         NSLog(@"任务1:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
         //模拟耗时操作
         [NSThread sleepForTimeInterval:3];
         NSLog(@"任务2:%@",[NSThread currentThread]);
    });

    // 任务3是一个阻塞式任务,利用阻塞式函数添加
    // 同步和异步都是相对于当前线程来说的!
    dispatch_barrier_sync(queue, ^{
        //模拟耗时操作
        [NSThread sleepForTimeInterval:5];
        NSLog(@"任务3:%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:5];
    });

    dispatch_async(queue, ^{
         NSLog(@"任务4:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
         NSLog(@"任务5:%@",[NSThread currentThread]);
    });
}
延时执行

这里不止有GCD的延时执行方法。在了解这两个延时执行方法之前我们还是要了解一些和Runloop有关的概念性知识,因为这很有必要。当然只是做一些简单的介绍,深入的介绍会在以后陆续讲解。

1、主线程和子线程的区别
主线程的运行循环默认是开启的,子线程的运行循环默认是关闭的!

2、Runloop
运行循环驱动(执行)事件源(UI操作/点击/滚动/定时器/特殊的事件)。

运行循环是一个死循环(do...while...循环)!平时没有事件驱动的时候,运行循环就处于睡眠状态!当有一些事件源发生的时候,就会唤醒运行循环来执行事件!事件执行完毕之后,运行循环接着进入睡眠状态!

运行循环的开启需要事件源的驱动!一个线程中,如果没有事件源,运行循环无法开启!

3、两种延迟执行的方式
(1) GCD

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        <#code to be executed after a specified delay#>
});

(2) other

self performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"touchesBegan");
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        [self performSelector:@selector(test) withObject:nil afterDelay:3.0];
        
        NSLog(@"能够到这里吗?");
    });
    
    NSLog(@"touchesEnd");
}

- (void)test {
    NSLog(@"test");
}

输出结果:


Snip20160907_432.png

通过控制台输出可以看到test方法的Log并没有打印。
为什么呢?
方式(2)是一个事件源,一个特殊的事件源,这句代码执行完后,会自动关闭运行循环。
需要延迟执行的test方法并没有执行,因为这句代码执行完后就关闭了运行循环。
所以为了延迟执行test方法,需要在执行完这句代码后开启运行循环,代码 [[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] run]最好写成 CFRunLoopRun();这样写方便我们关闭它。
因为NSRunLoop并没有为我们提供关闭它的方法,而 CFRunLoopRun()给我们提供了关闭运行循环的方法,如下

CFRunLoopGetCurrent():获得当前线程的运行循环!
CFRunLoopStop(CFRunLoopGetCurrent()); 关闭运行循环!

所以还要注意,如果要在一个子线程中开启一个定时器,把当前定时器添加到运行循环后,一定要开启运行循环。(因为子线程的运行循环默认是关闭的)。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"touchesBegan");
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        //创建一个定时器
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        
        //把定时器加入到当前的运行循环
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        
        //开启运行循环
        CFRunLoopRun();
        
        NSLog(@"come here! %@",[NSThread currentThread]);
        
    });
    
    NSLog(@"touchesEnd");
}

- (void)test {
    static int i = 0;
    
    NSLog(@" i = %d",i);
    
    if (i == 6) {
        CFRunLoopStop(CFRunLoopGetCurrent());
        NSLog(@"关闭运行循环!");
    }
}

控制台输出:

output.png

NSOperation & NSOperationQueue

NSOperation是对GCD的封装,面向操作编程。

NSOperation有两种方式创建操作对象

(1)

NSInvocationOperation *opInvo = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];

(2)

NSBlockOperation *opBlock = [NSBlockOperation blockOperationWithBlock:^{
        //xxxx
}];
NSOperation的执行

第一种情况:
将操作添加到队列中
[queue addOperation:op];
使用这种方式向队列中添加操作,所有操作都会在子线程中执行。
所以,建议如果NSBlockOperation需要在主线程执行,就不要追加操作。

第二种情况:
调用start方法
[op start];
NSBlockOperation 如果追加了任务! 直接调用 start 方法或者将操作添加到主队列, 这个时候,操作中的任务会在不同的线程执行(主线程 + 子线程)。

两种队列

主队列:在主线程中完成操作
非主队列:在子线程中完成操作
(1)创建主队列

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
// 添加操作到队列中!
// 将操作添加到队列中之后,就会自动执行 NSOperation 对象的 main 方法!
[queue1 addOperation:op];

(2)创建非主队列

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加操作到队列中!
// 将操作添加到队列中之后,就会自动执行 NSOperation 对象的 main 方法!
[queue addOperation:op];

(3)往队列中添加操作的简便写法

// 缺点: 没有一个操作对象.不能对操作做后续的管理!
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
     NSLog(@"任务:%@",[NSThread currentThread]);
}];
NSBlockOperation追加任务及需要注意的点
  1. 创建一个操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务1 %@",[NSThread currentThread]);
}];

2.NSBlockOperation 可以追加任务!

    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"op1 - %@",[NSThread currentThread]);
    }];
    
    [op addExecutionBlock:^{
        NSLog(@"任务2 %@",[NSThread currentThread]);
    }];
    
    [op addExecutionBlock:^{
        NSLog(@"任务3 %@",[NSThread currentThread]);
    }];
    
    [op addExecutionBlock:^{
        NSLog(@"任务4 %@",[NSThread currentThread]);
    }];
    
    [op start];

控制台输出:


Snip20160907_434.png

也就是说追加的任务全部是在子线程中执行的!

NSOperation的高级使用方法

操作依赖

比如有如下需求:
有5个操作1-5,操作1-3在子线程中执行,操作4-5在主线程中执行,并且操作的执行顺序是1,2,3,4,5。这个时候就需要使用操作依赖。让后一个操作依赖前一个操作,并且操作依赖可以夸队列(线程)。

NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务1 %@",[NSThread currentThread]);
}];

NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务2 %@",[NSThread currentThread]);
}];

NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{       
    NSLog(@"任务3 %@",[NSThread currentThread]);
}];

NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务4 %@",[NSThread currentThread]);
}];

NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务5 %@",[NSThread currentThread]);
}];
// 创建非主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
// 保证操作按顺序执行 --- 添加操作依赖!       
// 能够保证先执行 op2 ,再执行 op1    
// 操作依赖内部使用了:线程同步技术!
// 添加操作依赖注意:
// 注意1: 不要添加循环依赖!
// 注意2: 一定要先添加操作依赖,然后再把操作添加到操作队列中!    
// 注意3: 对不不同队列中的操作,添加操作依赖依然有效!    
[op2 addDependency:op1];    
[op3 addDependency:op2];    
[op4 addDependency:op3];
[op5 addDependency:op4];
[queue addOperation:op1];    
[queue addOperation:op2];    
[queue addOperation:op3];

NSOperationQueue *queue2 = [NSOperationQueue mainQueue];
[queue2 addOperation:op4];
[queue2 addOperation:op5];

控制台的输出一定是按照顺序输出的!

队列操作

操作队列可以管理操作,可以 暂停/恢复/取消操作, 实际开发中,为了在任何时候都能够使用这个队列,做成全局的!

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 暂停队列中的操作
    [queue setSuspended:YES];
    // 恢复队列中的操作
    [queue setSuspended:NO];
    // 取消队列中的所有操作
    // 取消操作:对于已经开始的操作是无法取消的!
    // 系统接收到内存警告的时候!
    [queue cancelAllOperations];
    // 取消单个操作!
    [op cancel];
队列间线程通信
     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
       [queue addOperationWithBlock:^{
        // 1.异步下载图片
        NSURL *url = [NSURL URLWithString:@"http://d.hiphotos.baidu.com/image/pic/item/37d3d539b6003af3290eaf5d362ac65c1038b652.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        // 2.回到主线程,显示图片
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
             self.imageView.image = image;
        }];
    }];

如何选择使用GCD还是NSOperation ?

首先,NSOperation是对GCD的封装,在使用上当然是GCD更具有效率。
NSOperation是面向操作编程,GCD是面向任务编程。
使用NSOperation我们可以将某个操作暂停,恢复或取消操作。可以满足许多使用场景,比如一个tableView上下滚动的时候,如果开始滚动就暂停下载操作,如果停止滚动就恢复下载操作。如果系统提示内存警告还可以取消操作。
但是GCD就不具备NSOperation的上述功能,GCD适用一些简单的需求,比如简单的多线程操作等等。
所以根据具体需求选择相应的实现技术。

上一篇下一篇

猜你喜欢

热点阅读