一、学习笔记
( 摘自《iOS开发:从零基础到精通》)
多线程
-
NSThread 类:
-
NSThread 类是苹果官方提供的管理线程的类,提供了一些线程管理的方法,但是随着 GCD 和 NSOperation 的推出,NSThread 的使用场景已经大大减少,在实际的应用开发中,偶尔会使用 NSThread 类来获取一些线程信息,常用的方法如下:
// 获取当前线程对象 +(NSThread *)currentThread; // 获取主线程对象 +(NSThread *)mainThread; // 使主线程休眠ti秒 +(void)sleepForTimeInterval:(NSTimeInterval)ti;
-
-
GCD(Grand Central Dispatch):
-
GCD 是 Apple 开发的一个多核编程的较新的解决方法,它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统,是一个在线程池模式的基础上执行的并发任务,GCD 已经完全屏蔽了有关线程的概念,而是引入了任务和队列,使用时只需要把任务(通常封装到一个 Block 中)添加到一个队列中执行即可,有关线程调用的工作将完全由 GCD 完成
-
任务和队列:
-
任务:即要执行的操作,通常封装到一个 Block 里,执行任务有两种方式:同步执行(sync)和异步执行(async),两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力
// 异步:不需要等待,可以直接执行,具备开启新线程的能力 dispatch_sync(queue, ^{ // 这里放异步执行任务代码 }); // 同步:等到队列里面所有任务完成之后再执行,不具备开启新线程的能力 dispatch_async(queue, ^{ // 这里放同步执行任务代码 });
-
队列:即要执行任务的等待队列,队列是一种特殊的线性表,按照 FIFO 的原则,按任务添加到队列的顺序来对队列进行处理,可以分为并行队列和串行队列,两者的主要区别是:执行顺序不同,以及开启线程数不同
// 获取系统定义的全局并行队列,一般不需要自己创建并行队列,而是用系统提供的 // 第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT;第二个参数暂时没用,用0即可 dispatch_get_global_queue(long identifier, unsigned long flags); // 创建队列,一般用来创建串行队列 // 第一个参数表示队列的唯一标识符,用于DEBUG,可为空,推荐使用应用程序ID这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列,DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列 dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
还有一种常用的队列是主队列,主队列也是串行队列,所有主队列中的任务都在主线程中执行
// 获取主队列 dispatch_get_main_queue(void);
-
队列和任务的组合:因为有两种类型的任务和三种类型的队列,所以总共有六种组合使用的情况,区别如下:
并行队列 串行队列 主队列 同步任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 主线程调用:死锁卡住不执行;其他线程调用:没有开启新线程,串行执行任务 异步任务 有开启新线程,并行执行任务 只会开启一个新线程,串行执行任务 不会开启新线程,串行执行任务
-
-
线程间通信:
- 由于 UI 的更新和操作是由主线程负责的,因此,当使用子线程获取到数据时(例如通过网络获取到了服务器返回的数据),需要返回主线程对 UI 进行更新,这就涉及了线程之间的通信,通常是在任务的最后加上一个访问主线程并更新 UI 的子任务
- (IBAction)downLoadImageView:(id)sender { // 获取并行队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); NSLog(@"0---%@", [NSThread currentThread]); // 异步任务 dispatch_async(queue, ^{ // 从网络下载图片 NSString *urlString = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlString]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:imageData]; NSLog(@"1---%@", [NSThread currentThread]); // 返回主线程设置UI dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; NSLog(@"2---%@", [NSThread currentThread]); }); }); NSLog(@"3---%@", [NSThread currentThread]); }
-
队列组:
-
使用 GCD 时,有时候会希望若干个任务之间有先后执行的依赖关系,比如当 A 、B 两个异步任务完成后再去做 C 任务,如果是用串行队列自然是可以实现,但是对于并行队列,任务会被自动分配到不同的线程中执行,任务完成的顺序是不确定的,这时候就要用到任务组 dispatch group
-
相关的 API 如下:
// 创建队列组 dispatch_group_t dispatch_group_create(void); // 向队列组中插入一个异步任务 void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block); // 队列组中其他任务完成后执行的任务,通常可以用来设置 UI void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
-
示例:点击按钮后启动两个异步下载任务,图片下载完成后更新屏幕上的 UIImageView,等两个下载都完成后,更新界面上的下载任务状态 Label 为下载完成
- (IBAction)startTask:(id)sender { NSLog(@"0--%@", [NSThread currentThread]); // 创建队列组 dispatch_group_t group = dispatch_group_create(); // 创建并行队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 创建队列组中的第一个异步任务 dispatch_group_async(group, queue, ^{ NSLog(@"1.1--%@", [NSThread currentThread]); // 下载网络图片 NSString *urlStr = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlStr]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:imageData]; // 返回主队列设置图片 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"1.2--%@", [NSThread currentThread]); self.task1ImageView.image = image; }); }); // 创建队列组中的第二个异步任务 dispatch_group_async(group, queue, ^{ NSLog(@"2.1--%@", [NSThread currentThread]); // 下载网络图片 NSString *urlStr = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlStr]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:imageData]; // 返回主队列设置图片 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"2.2--%@", [NSThread currentThread]); self.task2ImageView.image = image; }); }); // 任务组中的任务完成后,执行的动作 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"3--%@", [NSThread currentThread]); self.taskLabel.text = @"下载完成"; }); }
-
-
延迟执行操作:
-
GCD 中的 dispatch_after 方法:
- (void)after { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"asyncMain---begin"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2.0秒后异步追加任务代码到主队列,并开始执行 NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程 }); }
需要注意的是:dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中,严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 函数是很有效的
-
NSRunLoop 类中的方法:
- (void)viewDidLoad { [super viewDidLoad]; // 延迟2秒执行 printLog [self performSelector:@selector(printLog) withObject:nil afterDelay:2.0]; } -(void) printLog { NSLog(@"延迟2.0秒后打印出来的日志!"); }
这个方法是对 NSObject 类的扩展,因此所有类都可以使用
-
-
-
NSOperation:
-
NSOperation 是基于 GCD 更高一层的封装,完全面向对象,但是比 GCD 更简单易用、代码可读性也更高,GCD 中的任务分别对应 NSOperation 中的操作和操作队列:
- 操作(Operation):
- 即在线程中要进行的操作(代码),在 GCD 中任务是放在 block 中的,而在 NSOperation 中,因为NSOperation 是个抽象类,是使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类 CustomOperation(通过实现内部相应的方法)来封装操作的
- 操作队列(Operation Queues):
- 即用来存放操作的队列
- 不同于 GCD 中的调度队列 FIFO(先进先出)的原则,NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(不是结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)
- 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发或者串行
- NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列,主队列运行在主线程之上,而自定义队列在后台执行
- 操作(Operation):
-
创建操作:
- NSInvocationOperation:需要执行的任务直接指定已定义的方法
- (void)useInvocationOperation { // 创建 NSInvocationOperation 对象 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 在不创建队列的情况下,可以直接调用 NSOperation 类的 start 方法来执行某个任务,该任务会在当前线程开始执行 [op start]; } - (void)task1 { for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } }
- NSBlockOperation:需要执行的任务封装在 Block 中
- (void)useBlockOperation { // 创建 NSBlockOperation 对象 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } }]; // 调用 start 方法开始执行操作 [op start]; }
-
自定义继承自 NSOperation 的子类:可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象,重写 main 方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished,当 main 执行完返回的时候,这个操作就结束了
-
先定义一个继承自 NSOperation 的子类,重写 main 方法
// YSCOperation.h 文件 #import <Foundation/Foundation.h> @interface YSCOperation : NSOperation @end // YSCOperation.m 文件 #import "YSCOperation.h" @implementation YSCOperation - (void)main { if (!self.isCancelled) { for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; NSLog(@"1---%@", [NSThread currentThread]); } } } @end
-
然后使用的时候导入头文件 YSCOperation.h
- (void)useCustomOperation { // 创建 YSCOperation 对象 YSCOperation *op = [[YSCOperation alloc] init]; // 调用 start 方法开始执行操作 [op start]; }
-
-
创建队列:
-
主队列:凡是添加到主队列中的操作,都会放到主线程中执行(不包括操作使用addExecutionBlock 方法添加的额外操作,额外操作可能在其他线程执行)
NSOperationQueue *queue = [NSOperationQueue mainQueue];
-
自定义队列:添加到这种队列中的操作,就会自动放到子线程中执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
-
-
在任务中添加新任务:
- 通过 addExecutionBlock 就可以为某个 NSOperation 对象添加额外的操作,当把这些新增的操作放到队列中执行时,也是并行执行的
-(void)addTaskInOperation { NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task1-----%@", [NSThread currentThread]); }]; NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task2-----%@", [NSThread currentThread]); }]; // task1中添加task [task1 addExecutionBlock:^{ NSLog(@"task1 add task-----%@", [NSThread currentThread]); }]; // task2中添加task [task2 addExecutionBlock:^{ NSLog(@"task2 add task-----%@", [NSThread currentThread]); }]; // 创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 添加任务到队列 [queue addOperation:task1]; [queue addOperation:task2]; }
-
在队列中添加任务:
- 通过调用 NSOperationQueue 的 addOperationWithBlock 方法,可以向队列中直接添加任务
-(void) addTaskInQueue { NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task1-----%@", [NSThread currentThread]); }]; NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task2-----%@", [NSThread currentThread]); }]; // 创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 添加任务到队列 [queue addOperation:task1]; [queue addOperation:task2]; // 在queue中添加任务 [queue addOperationWithBlock:^{ NSLog(@"queue task-----%@", [NSThread currentThread]); }]; }
-
NSOperationQueue 控制串行执行、并行执行:
- 通过设置最大并发操作数 maxConcurrentOperationCount 来控制并发或者串行
- maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可以并发执行
- maxConcurrentOperationCount 为1时,队列为串行队列,只能串行执行
- maxConcurrentOperationCount 大于1时,队列为并发队列,操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}
- (void)setMaxConcurrentOperationCount { // 创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 设置最大并发操作数 queue.maxConcurrentOperationCount = 1; // 串行队列 // queue.maxConcurrentOperationCount = 2; // 并发队列 // queue.maxConcurrentOperationCount = 8; // 并发队列 // 添加操作 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程 } }]; }
- 通过设置最大并发操作数 maxConcurrentOperationCount 来控制并发或者串行
-
线程间通信:于 GCD 类似
- (IBAction)downloadImage:(id)sender { // 创建任务 NSBlockOperation *downloadTask = [NSBlockOperation blockOperationWithBlock:^{ // 下载网络图片 NSString *urlStr = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlStr]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:imageData]; // 返回主线程设置 UI NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [mainQueue addOperationWithBlock:^{ self.imageView.image = image; }]; }]; // 创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperation:downloadTask]; }
-
任务间添加依赖:
-
常用接口:
// 添加依赖,使当前操作依赖于操作 op 的完成 - (void)addDependency:(NSOperation *)op; // 移除依赖,取消当前操作对操作 op 的依赖 - (void)removeDependency:(NSOperation *)op; // 在当前操作开始执行之前完成执行的所有操作对象数组 @property (readonly, copy) NSArray<NSOperation *> *dependencies;
-
示例:点击按钮后启动两个异步下载任务,图片下载完成后更新屏幕上的 UIImageView,等两个下载都完成后,更新界面上的下载任务状态 Label 为下载完成
- (IBAction)downloadTwoImage:(id)sender { // 创建两个任务,两个任务完成后,返回主线程执行第三个更新Label的任务 NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{ // 下载网络图片 NSString *urlStr = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlStr]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image1 = [UIImage imageWithData:imageData]; NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [mainQueue addOperationWithBlock:^{ self.imageView1.image = image1; }]; }]; NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{ // 下载网络图片 NSString *urlStr = @"http://7xta2c.com1.z0.glb.clouddn.com/99logo.png"; NSURL *url = [NSURL URLWithString:urlStr]; NSData *imageData = [NSData dataWithContentsOfURL:url]; UIImage *image2 = [UIImage imageWithData:imageData]; NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [mainQueue addOperationWithBlock:^{ self.imageView2.image = image2; }]; }]; NSBlockOperation *task3 = [NSBlockOperation blockOperationWithBlock:^{ NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [mainQueue addOperationWithBlock:^{ self.label.text = @"下载完成"; }]; }]; // 设置任务之间的执行依赖关系 [task3 addDependency:task1]; [task3 addDependency:task2]; [task2 addDependency:task1]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperation:task1]; [queue addOperation:task2]; [queue addOperation:task3]; }
-
-
在任务中创建 completionBlock:
- 可以通过设置 completionBlock 来创建所有任务执行完后自动调用的一个 Block,这个 Block 中的任务会在新的线程中执行
-(void) addCompletionBlock { NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task1-----%@", [NSThread currentThread]); }]; // task1中添加task [task1 addExecutionBlock:^{ NSLog(@"task1 add task-----%@", [NSThread currentThread]); }]; task1.completionBlock = ^{ NSLog(@"task1 end!!! %@", [NSThread currentThread]); }; // 创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 添加任务到队列 [queue addOperation:task1]; }
-
设置操作的优先级:
-
只适用于同一操作队列中的操作,不适用于不同操作队列中的操作
-
当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行
-
进入就绪状态的操作的开始执行顺序(不是非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)
-
优先级不能取代依赖关系
-
默认情况下,所有新创建的操作对象优先级都是 NSOperationQueuePriorityNormal,但是我们可以通过 setQueuePriority 方法来改变当前操作在同一队列中的执行优先级
// 优先级的取值 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { NSOperationQueuePriorityVeryLow = -8L, NSOperationQueuePriorityLow = -4L, NSOperationQueuePriorityNormal = 0, NSOperationQueuePriorityHigh = 4, NSOperationQueuePriorityVeryHigh = 8 };
-
-
-
GCD 和 NSOperation 的区别:
- GCD 是底层的 C 语言构成的 API,而 NSOperationQueue 及相关对象是 Objective-C 的对象,在 GCD 中,在队列中执行的是由 Block 构成的任务,这是一个轻量级的数据结构;而 NSOperation 作为一个对象,提供了更多选择
- 在 NSOperationQueue 中,可以随时取消已经设定要准备执行的任务(已经开始的任务无法阻止),而 GCD 要停止已经加入 queue 的 Block 需要极其复杂的操作
- NSOperation 能够方便地设置依赖关系,可以让一个 Operation 依赖于另一个 Operation,这样的话尽管两个 Operation 在同一个并行队列中,前者也会等到后者执行完再执行
- KVO 可以应用在 NSOperation 中,可以监听一个 Operation 是否完成或取消,这样能比 GCD 更有效地掌控执行的后台任务
- 在 NSOperation 中,能够设置 NSOperation 的 priority 优先级,能够使同一个并行队列中的任务区分先后地进行;而在 GCD 中,只能区分不同任务队列的优先级,如果要区分 Block 任务的优先级,需要大量复杂的代码
- 用户能对 NSOperation 进行继承,在这之上添加成员变量和成员方法,提高整个代码的复用度,这比简单地将 Block 任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能