iOS 多线程(1)-基本概念

1. 线程和进程

在了解多线程之前,我们现在熟悉两个概念,线程和进程.

一. 线程

线程:进程的基本执行单元,一个进程的所有任务都是在线程中执行.
进程要想执行任务,必须得有线程,进程至少有一条线程.
程序启动会默认开始一条线程,这条线程被称为主线程.

二.进程

进程:在系统中正在运行的一个应用程序.
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存中.

三. 进程与线程的区别:

  1. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间.
  2. 资源拥有:同一进程内的线程共享本进程的资源如内存,I/O,CPU等,但是进程之前的资源是独立的.
  3. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉.所以多进程要比多线程健壮.
  4. 进程切换时,消耗的资源大,效率低.所以设计到频繁的切换时,使用线程要高于进程.同样如果要求同时进行又要共享某些变量的并发操作,只能用线程不能用进程.
  5. 执行过程:每个独立的进程有一个程序运行的入口,顺序执行序列.但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制.
  6. 线程是处理器调度的基本单元,但是进程不是.

2. iOS中的多线程

我们知道了进程和线程之间的关系和区别,我们现在再来说一下iOS中有哪些多线程.多线程有哪些?他们之前的区别是什么?我们在开发的时候选择哪种方式会更好一些?

技术方案 简介 语言 线程生命周期 使用频率
pthread 1.一套通用的多线程API.
2.适用于Unix\Linux\windows等系统.
3.可快平台\可移植.
4.使用难度大.
C 程序员管理 几乎不使用
(在加锁的时候使用较多)
NSThread 1.使用更加面向对象.
2.简单易懂,可直接操作线程对象.
OC 程序员管理 偶尔使用
GCD 1.旨在替代NSThread等线程技术.
2.充分利用设备的多核.
C 系统管理 经常使用
NSOperation 1.基于GCD(底层是GCD).
2.比GCD多了一些更简单实用的功能.
3.使用更加面向对象.
OC 系统管理 经常使用

NSThread的底层是pthread,NSPperation的底层是GCD.GCD、NSPperation、NSThread创建线程都是依赖于pethread.

一. pthread的使用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //创建线程
    pthread_t thread;
    /*
     第一个参数pthread_t *restrict:线程对象
     第二个参数const pthread_attr_t *restrict:线程属性
     第三个参数void *(*)(void *) :指向函数的指针
     第四个参数void *restrict:函数的参数
     */
    pthread_create(&thread, NULL,eat ,NULL);
}
//void *(*)(void *)
void *eat(void *param)
{
   NSLog(@"调用了该方法");
}

二. NSThread的使用

A. NSThread创建
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
   // 方法一:创建线程,需要自己开启线程
  NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(eat) object:nil];
  // 开启线程
  [thread start];

  // 方法二:创建线程后自动启动线程
  [NSThread detachNewThreadSelector:@selector(eat) toTarget:self withObject:nil];

  // 方法三:隐式创建并启动线程
  [self performSelectorInBackground:@selector(eat) withObject:nil];
}

后面两种方法都不用我们开启线程,相对方便快捷,但是没有办法拿到子线程对象,没有办法对子线程进行更详细的设置.

B. NSThread的方法
// 获取当前线程
 + (NSThread *)currentThread;
 // 创建启动线程
 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
 // 判断是否是多线程
 + (BOOL)isMultiThreaded;
 // 线程休眠 NSDate 休眠到什么时候
 + (void)sleepUntilDate:(NSDate *)date;
 // 线程休眠时间
 + (void)sleepForTimeInterval:(NSTimeInterval)ti;
 // 结束/退出当前线程
 + (void)exit;
 // 获取当前线程优先级
 + (double)threadPriority;
 // 设置线程优先级 默认为0.5 取值范围为0.0 - 1.0 
 // 1.0优先级最高
 // 设置优先级
 + (BOOL)setThreadPriority:(double)p;
 // 获取指定线程的优先级
 - (double)threadPriority NS_AVAILABLE(10_6, 4_0);
 - (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
 
 // 设置线程的名字
 - (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
 - (NSString *)name NS_AVAILABLE(10_5, 2_0);

 // 判断指定的线程是否是 主线程
 - (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
 // 判断当前线程是否是主线程
 + (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
 // 获取主线程
 + (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
 
 - (id)init NS_AVAILABLE(10_5, 2_0);    // designated initializer
 // 创建线程
 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
 // 指定线程是否在执行
 - (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
 // 线程是否完成
 - (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
 // 线程是否被取消 (是否给当前线程发过取消信号)
 - (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
 // 发送线程取消信号的 最终线程是否结束 由 线程本身决定
 - (void)cancel NS_AVAILABLE(10_5, 2_0);
 // 启动线程
 - (void)start NS_AVAILABLE(10_5, 2_0);
 
 // 线程主函数  在线程中执行的函数 都要在-main函数中调用,自定义线程中重写-main方法
 - (void)main NS_AVAILABLE(10_5, 2_0);    // thread body method
C.NSThread线程的状态
NSThread线程的状态.png
//启动线程
- (void)start; 
// 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态

//阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 进入阻塞状态

//强制停止线程
+ (void)exit;
// 进入死亡状态
D. NSThread线程之间的通信
  • 线程间通信
    在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信.
  • 线程间通信的应用
    一个线程传递数据给另一个线程,在一个线程中执行完特定任务后,转到另一个线程继续执行任务.

线程间通信常用的方法

// 返回主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

demo:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
    // 获取图片url地址 http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg
    NSURL *url = [NSURL URLWithString:@"http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg"];
    // 下载图片二进制文件
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 将图片二进制文件转化为image;
    UIImage *image = [UIImage imageWithData:data];
    // 参数 waitUntilDone 是否等@selector(showImage:) 执行完毕以后再执行下面的操作  YES :等 NO:不等
    // 返回主线程显示图片
    // [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    // self.imageView 也可以直接调用这个方法 直接选择 setImage方法,传入参数image即可
    // [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    // 返回特定的线程,[NSThread mainThread] 获得主线程
    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
    self.imageView.image = image;
}
@end

三. GCD的使用

A. GCD的任务和队列
a. GCD的任务

GCD任务:执行什么操作,任务有两种执行方式: 同步函数 (dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);)和 异步函数(dispatch_async(dispatch_queue_t queue, dispatch_block_t block);).
同步:只能在当前线程中执行任务,不具备开启新线程的能力,任务立刻马上执行,会阻塞当前线程并等待 Block中的任务执行完毕,然后当前线程才会继续往下运行.
异步:可以在新的线程中执行任务,具备开启新线程的能力,但不一定会开新线程,当前线程会直接往下执行,不会阻塞当前线程.

b. GCD的队列

GCD的队列:用来存放任务,分为串行队列 和 并行队列.
串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务).
并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效.

GCD的使用就2个步骤:

  1. 定制任务:确定想做的事情.
  2. 将任务添加到队列中:GCD会自动将队列中的任务取出,放到对应的线程中执行.
  3. 任务的取出遵循队列的FIFO原则:先进先出,后进后出.
B. GCD的创建

a. 队列的创建:

// 第一个参数const char *label : C语言字符串,用来标识
// 第二个参数dispatch_queue_attr_t attr : 队列的类型
// 并发队列:DISPATCH_QUEUE_CONCURRENT
// 串行队列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

创建并发队列:

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);

创建串行队列:

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);

GCD默认已经提供了全局并发队列,供整个应用使用,可以无需手动创建:

 /** 
     第一个参数:优先级 也可直接填后面的数字
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
     第二个参数: 预留参数  0
     */
    dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

获得主队列:

dispatch_queue_t  queue = dispatch_get_main_queue();

b. 任务的执行:
开启同步函数,同步函数:在当前线程中执行任务,不具备开启新线程的能力,要求立刻马上开始执行.

/*
     第一个参数:队列
     第二个参数:block,在里面封装任务
     */
    dispatch_sync(queue, ^{
        
    });

开启异步函数,异步函数 :在新的线程中执行任务,具备开启新线程的能力(如果在主队的话,是不能开启新线程的),等主线程执行完毕之后,回过头开线程执行任务.

 dispatch_async(queue, ^{
        
    });

c. 任务和队列的组合
任务:同步函数 异步函数.
队列:串行 并行.
异步函数+并发队列:会开启新的线程,并发执行.

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务2-%@",[NSThread crruentThraed]);  
    }
});
//因为是并发队列的异步执行,所以开启新的线程去完成queue中的任务.
//所以执行任务1和执行任务2是交替进行的.

异步函数+串行队列:会开启一条线程,任务串行执行.

dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务2-%@",[NSThread crruentThraed]);  
    }
});
//因为是串行队列,所以会在子线程执行,会在任务1执行结束采取执行任务2

同步函数+并发队列:不会开线程,任务串行执行

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
   dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
   });

同步函数+串行队列:不会开线程,任务串行执行

dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_sync(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"执行任务2-%@",[NSThread crruentThraed]);  
    }
});
//因为是串行队列,所以会在主线程执行,会在任务1执行结束采取执行任务2

异步函数+主队列:不会开线程,任务串行执行
使用主队列(跟主线程相关联的队列).
主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行.

//1.获得主队列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.异步函数
dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });

同步函数+主队列:死锁

//1.获得主队列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.同步函数
dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

因为这个方法在主线程中,给主线程中添加任务,而同步函数要求立刻马上执行,因此就会相互等待而发生死锁。将这个方法放入子线程中,则不会发生死锁,任务串行执行。

并发队列 手动创建串行队列 主队列
同步(sync) 1.没有开启新线程.
2.串行执行任务.
1.没有开启新线程.
2.串行执行任务.
1.没有开启新线程.
2.串行执行任务.
异步(async) 1.有开启新线程.
2.并发执行任务.
1.有开启新线程.
2.串行执行任务.
1.没有开启新线程.
2.串行执行任务.

d.同步函数和异步函数的执行顺序
同步函数:立刻马上执行,会阻塞当前线程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self syncConcurrent];
}
//同步函数+并发队列:不会开线程,任务串行执行
-(void)syncConcurrent
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSLog(@"--syncConcurrent--start-");
    dispatch_sync(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
    NSLog(@"--syncConcurrent--end-");
}

我们看一下Log

同步函数会阻塞线程,立即执行.png

异步函数:当前线程会直接往下执行,不会阻塞当前线程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
      [self syncConcurrent];
}
//异步函数+并发队列:会开启新的线程,并发执行
-(void)asyncCONCURRENT
{
    NSLog(@"--asyncCONCURRENT--start-");
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
      NSLog(@"--asyncCONCURRENT--end-");
}

我们来看一下Log

异步函数不会阻塞当前线程.png

注意:GCD中开多少条线程是由系统根据CUP繁忙程度决定的,如果任务很多,GCD会开启适当的子线程,并不会让所有任务同时执行。

C. GCD线程间的通信

demo

import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // 获得图片URL
        NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        // 将图片URL下载为二进制文件
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 将二进制文件转化为image
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"%@",[NSThread currentThread]);
        // 返回主线程 这里用同步函数不会发生死锁,因为这个方法在子线程中被调用。
        // 也可以使用异步函数
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@",[NSThread currentThread]);
        });
    }); 
}
@end

GCD线程间的通信非常简单,使用同步或异步函数,传入主队列即可。

D. GCD其他常用函数

a. 栅栏函数(控制任务的执行顺序)

dispatch_barrier_async(queue, ^{
        NSLog(@"--dispatch_barrier_async-");
    });

我们来看一下栅栏函数的作用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     [self barrier];
}
-(void)barrier
{
    //1.创建队列(并发队列)
    dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
        }
    });
    //栅栏函数
    dispatch_barrier_async(queue, ^{
        NSLog(@"我是一个栅栏函数");
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
        }
    });
}

我们来看一下Log

栅栏函数demo的输出.png

栅栏函数可以控制任务执行的顺序,栅栏函数之前的执行完毕之后,执行栅栏函数,然后在执行栅栏函数之后的.

b. 延迟执行(延迟·控制在哪个线程执行)

/*
     第一个参数:延迟时间
     第二个参数:要执行的代码
     如果想让延迟的代码在子线程中执行,也可以更改在哪个队列中执行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
     */
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"---%@",[NSThread currentThread]);
    });

延迟执行的其他方法:

// 2s中之后调用run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// repeats:YES 是否重复
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

c.单例

-(void)once
    {
        //整个程序运行过程中只会执行一次
        //onceToken用来记录该部分的代码是否被执行过
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"-----");
        });
    }

d.快速迭代(开多个线程并发完成迭代操作)

    /*
     第一个参数:迭代的次数
     第二个参数:在哪个队列中执行
     第三个参数:block要执行的任务
     */
dispatch_apply(10, queue, ^(size_t index) {
});

快速迭代:开启多条线程,并发执行,相比于for循环在耗时操作中极大的提高效率和速度.

e.队列组(同栅栏函数)

// 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 创建并行队列 
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 执行队列组任务
    dispatch_group_async(group, queue, ^{   
    });
    //队列组中的任务执行完毕之后,执行该函数
    dispatch_group_notify(group, queue, ^{
    });

下面看一了实例使用group下载两张图片然后合成在一起

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) UIImage  *image1; /**< 图片1 */
@property (nonatomic, strong) UIImage  *image2; /**< 图片2 */
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self group];
}
-(void)group
{
    //下载图片1
    //创建队列组
    dispatch_group_t group =  dispatch_group_create();
    //1.开子线程下载图片
    //创建队列(并发)
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        //1.获取url地址
        NSURL *url = [NSURL URLWithString:@"http://www.huabian.com/uploadfile/2015/0914/20150914014032274.jpg"];
        //2.下载图片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二进制数据转换成图片
        self.image1 = [UIImage imageWithData:data];
        NSLog(@"1---%@",self.image1);
    });
    //下载图片2
    dispatch_group_async(group, queue, ^{
        //1.获取url地址
        NSURL *url = [NSURL URLWithString:@"http://img1.3lian.com/img2011/w12/1202/19/d/88.jpg"]; 
        //2.下载图片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二进制数据转换成图片
        self.image2 = [UIImage imageWithData:data];
        NSLog(@"2---%@",self.image2);
    });
    //合成,队列组执行完毕之后执行
    dispatch_group_notify(group, queue, ^{  
        //开启图形上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        //画1
        [self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
        //画2
        [self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
        //根据图形上下文拿到图片
        UIImage *image =  UIGraphicsGetImageFromCurrentImageContext();
        //关闭上下文
        UIGraphicsEndImageContext();
        //回到主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@--刷新UI",[NSThread currentThread]);
        });
    });
}

四. NSOperation的使用

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。

NSOperation和NSOperationQueue实现多线程的具体步骤:
1.将需要执行的操作封装到一个NSOperation对象中.
2.将NSOperation对象添加到NSOperationQueue中
系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行.

A. NSOperation的创建

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类.
使用NSOperation子类的方式有3种:
a.NSInvocationOperation

    /*
     第一个参数:目标对象
     第二个参数:选择器,要调用的方法
     第三个参数:方法要传递的参数
     */
NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//启动操作
[op start];

b.NSBlockOperation(最常用)

//1.封装操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       //要执行的操作,在主线程中执行
       NSLog(@"1------%@",[NSThread currentThread]); 
}];
//2.追加操作,追加的操作在子线程中执行,可以追加多条操作
[op addExecutionBlock:^{
        NSLog(@"---download2--%@",[NSThread currentThread]);
    }];
[op start];

c.自定义子类继承NSOperation,实现内部相应的方法

// 重写自定义类的main方法实现封装操作
-(void)main
{
    // 要执行的操作
}
// 实例化一个自定义对象,并执行操作
CLOperation *op = [[CLOperation alloc]init];
[op start];

自定义类封装性高,复用性高。

B. NSOperationQueue的使用

NSOperation中的两种队列.

主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行.
非主队列:直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行.

NSOperationQueue的作用.

NSOperation可以调用start方法来执行任务,但默认是同步执行的
如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作.

添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

注意:将操作添加到NSOperationQueue中,就会自动启动,不需要再自己启动了addOperation 内部调用 start方法. start方法 内部调用 main方法.

C. NSOperation和NSOperationQueue结合使用创建多线程
    注:这里使用NSBlockOperation示例,其他两种方法一样
    // 1. 创建非主队列 同时具备并发和串行的功能,默认是并发队列
    NSOperationQueue *queue =[[NSOperationQueue alloc]init];
    //NSBlockOperation 不论封装操作还是追加操作都是异步并发执行
    // 2. 封装操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"download1 -- %@",[NSThread currentThread]);
    }];
    // 3. 将封装操作加入主队列
    // 也可以不获取封装操作对象 直接添加操作到队列中
    //[queue addOperationWithBlock:^{
    // 操作
    //}];
    [queue addOperation:op1];
D. NSOperation和NSOperationQueue的重要属性和方法

NSOperation
a. NSOperation的依赖 - (void)addDependency:(NSOperation *)op;

// 操作op1依赖op5,即op1必须等op5执行完毕之后才会执行
// 添加操作依赖,注意不能循环依赖,如果循环依赖会造成两个任务都不会执行
// 也可以夸队列依赖,依赖别的队列的操作
    [op1 addDependency:op5];

b.NSOperation操作监听void (^completionBlock)(void);

// 监听操作的完成
// 当op1线程完成之后,立刻就会执行block块中的代码
// block中的代码与op1不一定在一个线程中执行,但是一定在子线程中执行
op1.completionBlock = ^{
        NSLog(@"op1已经完成了---%@",[NSThread currentThread]);
    };

NSOperationQueue
a. maxConcurrentOperationCount

 //1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    /*
     默认是并发队列,如果最大并发数>1,并发
     如果最大并发数==1,串行队列
     系统的默认是最大并发数-1 ,表示不限制
     设置成0则不会执行任何操作
     */
     queue.maxConcurrentOperationCount = 1;

b. suspended

//当值为YES的时候暂停,为NO的时候是恢复
queue.suspended = YES;

c.-(void)cancelAllOperations;

//取消所有的任务,不再执行,不可逆
[queue cancelAllOperations];

注意:暂停和取消只能暂停或取消处于等待状态的任务,不能暂停或取消正在执行中的任务,必须等正在执行的任务执行完毕之后才会暂停,如果想要暂停或者取消正在执行的任务,可以在每个任务之间即每当执行完一段耗时操作之后,判断是否任务是否被取消或者暂停。如果想要精确的控制,则需要将判断代码放在任务之中,但是不建议这么做,频繁的判断会消耗太多时间.

E. NSOperation和NSOperationQueue的一些其他属性和方法

NSOperation

// 开启线程
- (void)start;
- (void)main;
// 判断线程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消当前线程
- (void)cancel;
//NSOperation任务是否在运行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任务是否已结束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依赖
- (void)addDependency:(NSOperation *)op;
// 移除依赖
- (void)removeDependency:(NSOperation *)op;
// 优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
// 操作监听
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞当前线程,直到该NSOperation结束。可用于线程执行顺序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 获取线程的优先级
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end

NSOperationQueue

// 获取队列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 队列中的操作数
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并发数,同一时间最多只能执行三个操作
@property NSInteger maxConcurrentOperationCount;
// 暂停 YES:暂停 NO:继续
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞当前线程直到此队列中的所有任务执行完毕
- (void)waitUntilAllOperationsAreFinished;
F. NSOperation线程之间的通信

NSOperation线程之间的通信方法

// 回到主线程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
      self.imageView.image = image;
}];

我们同样使用下载多张图片合成综合案例

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)UIImage *image1;
@property(nonatomic,strong)UIImage *image2;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 创建非住队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    // 下载第一张图片
    NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image1 = [UIImage imageWithData:data];
    }];
    // 下载第二张图片
    NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image2 = [UIImage imageWithData:data];
    }];
    // 合成操作
    NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
        // 开启图形上下文
        UIGraphicsBeginImageContext(CGSizeMake(375, 667));
        // 绘制图片1
        [self.image1 drawInRect:CGRectMake(0, 0, 375, 333)];
        // 绘制图片2
        [self.image2 drawInRect:CGRectMake(0, 334, 375, 333)];
        // 获取合成图片
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        // 关闭图形上下文
        UIGraphicsEndImageContext();
        // 回到主线程刷新UI
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            self.imageView.image = image;
        }];
    }];
    // 添加依赖,合成图片需要等图片1,图片2都下载完毕之后合成
    [combie addDependency:download1];
    [combie addDependency:download2];
    // 添加操作到队列
    [queue addOperation:download1];
    [queue addOperation:download2];
    [queue addOperation:combie];
}
@end

注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。

3. 关于多线程的面试题

一. 面试题1

- (void)interview01
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    NSLog(@"执行任务1");//①
    //队列的特点FIFO,先进先出
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2");//②
    });
    NSLog(@"执行任务3");//③
    // dispatch_sync立马在当前线程同步执行任务,执行完毕才能继续向下执行.
}

执行的顺序是①

二. 面试题2

- (void)interview02
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");//①
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");//②
    });
    NSLog(@"执行任务3");//③
    // dispatch_async不要求立马在当前线程同步执行任务,
}

执行的顺序是①③②

三. 面试题3

- (void)interview03
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    NSLog(@"执行任务1");//①
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");//②
        dispatch_sync(queue, ^{ // 1
            NSLog(@"执行任务3");//③
        });
        NSLog(@"执行任务4");//④
    });
    NSLog(@"执行任务5");//⑤
}

执行的顺序是①⑤②

四. 面试题4

- (void)interview04
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");
        dispatch_sync(queue2, ^{ // 1
            NSLog(@"执行任务3");
        });
        NSLog(@"执行任务4");
    });
    NSLog(@"执行任务5");
}

执行的顺序是①⑤②③④

五. 面试题5

- (void)interview05
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");
        dispatch_sync(queue, ^{ // 1
            NSLog(@"执行任务3");
        });
        NSLog(@"执行任务4");
    });
    NSLog(@"执行任务5");
}

执行的顺序是①⑤②③④

六. 面试题6

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
    NSLog(@"1");
    // 这句代码的本质是往Runloop中添加定时器
    [self performSelector:@selector(test) withObject:nil afterDelay:0];
    NSLog(@"3");
    });
- (void)test
{
    NSLog(@"2");
} 

打印结果是1,3,原因是performSelector:withObject:afterDelay:的本质是在Runloop中的timer中执行的,由于子线程的Runloop没有启动所以test不会执行.

七. 面试题7

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test
{
    NSLog(@"2");
} 

打印的结果是1,原因是线程打印1之后,线程thread就退出了,然后在执行performSelector:onThread:withObject:waitUntilDone:就会崩溃,因为thread已经退出了.

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