iOS 多线程详解

在了解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适用一些简单的需求,比如简单的多线程操作等等。
所以根据具体需求选择相应的实现技术。

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

推荐阅读更多精彩内容

  • 欢迎大家指出文章中需要改正或者需要补充的地方,我会及时更新,非常感谢。 一. 多线程基础 1. 进程 进程是指在系...
    xx_cc阅读 7,184评论 11 70
  • 1、简介 NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高...
    WQ_UESTC阅读 957评论 0 6
  • Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么? 1...
    AlanGe阅读 1,734评论 0 17
  • 什么是进程? 进程是指在系统中正在运行的一个应用程序。 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存...
    珍此良辰阅读 1,251评论 1 5
  • 我羡慕那些能够轻轻松松的就把自己委屈跟不开心的讲出来,我羡慕那些家庭美满不知世间丑态的天真小孩,我羡慕那些拿不起又...
    啊曼花阅读 567评论 4 6