多线程整理:
多线程概述: 在iOS中每个应用程序都有一个准们用来更新现实UI界面,处理用户触摸事件的主线程。 因此不能将太耗时的操作放在主线程中执行。不然会造成主线程堵塞(出现卡机现象), 带来极坏的用户体验。 一般的解决方案就是讲哪些耗时的操作放到另外的一个线程中执行, 多线程编程时防止主线程堵塞, 增加效率的最佳方法。 iOS 支持多个层次的多线程编程。 层次越高抽象度也越高。 使用起来也越方便, 也是苹果推荐常用的方法。
根据抽象层次依次列出iOS所支持的多线程编程方法。
1、 NSThread:三种方法中最轻量级的, 但是需要管理线程的生命周期、同步、和加锁问题, 这也会导致一定的性能开销。
2、 Cocoa Operation : 是基于OC的实现。NSOperation以面向对象的思想封装了需要执行的操作, 不必关心线程的管理、同步等问题。NSOperation 是一个抽象的基类,iOS提供了两种默认的实现: NSInvocationOperation 和 NSBlockOperation 当然也可以自定义NSOperation
3、 Grand Center Dispatch (GCD,iOS4 之后支持): 提供了一些新特性、 运行库来支持多核并行编程。 它的关注点更高: 如何在多个CPU上提升效率
下面时对三种多线程编程方法的简单总结:
一、 NSThread 是一个轻量级的多线程实现方法。
三种创建和管理线程的方法:
1)方法一( iOS 5 之后): 先创建后启动
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download:) object:nil];
2)方法二 : 创建后自动启动
NSThread *thread = [NSThread detachNewThreadSelector:@selector(download:) withObject:nil];
3)方法三: 隐式创建
[self performSelectorInBackground:@selector(download:) withObject:nil];
NSThread的常用方法:
1>获取当前线程
+ (NSThread *)currentThread;
2>获得主线程
+ (NSThread *)mainThread;
3>睡眠(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepTiemInterval:(NSTimeInterval)time;
4>设置线程的名字
- (void)setName:(NSString *)name;
- (NSString *)name;
NSThread 中线程加锁-- 线程同步
为了防止多个线程抢夺同一个资源造成的数据安全问题 , 相应的解决办法就是对线程加锁---- @synchronized(self){}(括号中添加需要加锁的操作)
* OC在定义属性时有nonatomic和atomic两种选择 -- atomic(默认):意为原子属性,为setter方法加锁 ; nonatomic:非原子属性,不会为setter方法加锁
* atomic:线程安全,会消耗大量的资源 ; nonatomic:非线程安全,适合内存小的移动设备
使用NSThread的注意事点:
* 不要同时开启太多的线程, 一般1~3条线程即可, 不要超过5条二 、NSOperation 封装了执行的相关操作, 和执行所需的数据。 并且能够以并发或非并发的方式执行这个操作。
NSOperation本身是一个抽象类, 因此必须使用其子类,使用NSOperation子类的方式有两种:NSInvocationOperation 和 NSBlockOperation , 通常情况下NSOperation子类与NSOperationQueue结合使用, 实现对线程队列的管理
1、单独使用NSOperation的子类管理单个子线程
//1.创建对象
//1> 创建NSInvocationOperation对象
NSInvocationOperation *invoOperation = [[NSInvocationOperation alloc] initWithTarget: selector: object: ] autorelease];
//2> 创建NSBlockOperation对象
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock: ^{ //执行的内容 } ];
// 2.执行操作
//NSOperation 调用start方法即可开始执行操作, NSOperation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行
//3.取消操作
//operation开始执行之后, 默认会一直执行操作直到完成,我们也可以调用cancel方法中途取消操作
[operation cancel];
2、 监听操作的执行:
如果我们想要在NSOperation执行完毕后做一些事情,就调用NSOperation的setCompletionBlock方法来设置想要做的事情。
- [operation setCompletionBlock:^() {
- NSLog(@"执行完毕");
- }];
3 使用操作队列来管理操作对象的执行。
NSOperationQueue是一个线程队列, 它用来调节一组NSOperation对象, 其中NSOperation可以在调节它在队列中调中的优先级
//1 创建操作队列, 通过操作队列管理操作对象
// 1.创建主线程队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 2. 创建自己的操作队列,在执行相关操作时, 会根据需求开辟子线程去执行。
[queue setMaxConcurrentOperationCount:2]
// 3. 设置操作在队列中的优先级
[blockOperation setQueuePriority:NSOperationQueuePriorityVeryHight];
// 4. 添加操作对象
[queue addOperation:blockOperation]
4、自定义NSOperation
1.简介
如果NSInvocationOperation和NSBlockOperation对象不能满足需求, 你可以直接继承NSOperation, 并添加任何你想要的行为。继承所需的工作量主要取决于你要实现非并发还是并发的NSOperation。定义非并发的NSOperation要简单许多,只需要重载-(void)main这个方法,在这个方法里面执行主任务,并正确地响应取消事件; 对于并发NSOperation, 你必须重写NSOperation的多个基本方法进行实现(这里暂时先介绍非并发的NSOperation)
2.非并发的NSOperation
比如叫做DownloadOperation,用来下载图片
1> 继承NSOperation,重写main方法,执行主任务
DownloadOperation.h
- #import <Foundation/Foundation.h>
- @protocol DownloadOperationDelegate;
- @interface DownloadOperation : NSOperation
- // 图片的url路径
- @property (nonatomic, copy) NSString *imageUrl;
- // 代理
- @property (nonatomic, retain) id<DownloadOperationDelegate> delegate;
- - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;
- @end
- // 图片下载的协议
- @protocol DownloadOperationDelegate <NSObject>
- - (void)downloadFinishWithImage:(UIImage *)image;
- @end
- #import "DownloadOperation.h"
- @implementation DownloadOperation
- @synthesize delegate = _delegate;
- @synthesize imageUrl = _imageUrl;
- // 初始化
- - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {
- if (self = [super init]) {
- self.imageUrl = url;
- self.delegate = delegate;
- }
- return self;
- }
- // 释放内存
- - (void)dealloc {
- [super dealloc];
- [_delegate release];
- [_imageUrl release];
- }
- // 执行主任务
- - (void)main {
- // 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
- @autoreleasepool {
- // ....
- }
- }
- @end
2> 正确响应取消事件
operation开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能发生在任何时候,甚至在operation执行之前。尽管NSOperation提供了一个方法,让应用取消一个操作,但是识别出取消事件则是我们自己的事情。如果operation直接终止, 可能无法回收所有已分配的内存或资源。因此operation对象需要检测取消事件,并优雅地退出执行
NSOperation对象需要定期地调用isCancelled方法检测操作是否已经被取消,如果返回YES(表示已取消),则立即退出执行。不管是自定义NSOperation子类,还是使用系统提供的两个具体子类,都需要支持取消。isCancelled方法本身非常轻量,可以频繁地调用而不产生大的性能损失
以下地方可能需要调用isCancelled:
* 在执行任何实际的工作之前
* 在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
* 代码中相对比较容易中止操作的任何地方
DownloadOperation的main方法实现如下
- - (void)main {
- // 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
- @autoreleasepool {
- if (self.isCancelled) return;
- // 获取图片数据
- NSURL *url = [NSURL URLWithString:self.imageUrl];
- NSData *imageData = [NSData dataWithContentsOfURL:url];
- if (self.isCancelled) {
- url = nil;
- imageData = nil;
- return;
- }
- // 初始化图片
- UIImage *image = [UIImage imageWithData:imageData];
- if (self.isCancelled) {
- image = nil;
- return;
- }
- if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {
- // 把图片数据传回到主线程
- [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];
- }
- }
- }
三 GCD (Grand Center Dispatch)让程序平行排队执行任务, 根据可用的处理资源, 安排他们在任何可用的处理器核心上执行任务。 (GCD的'艺术'归结为选择合适的队列调度函数以提交你的工作), GCD的底层依然是用线程实现, 不过这样的可以程序员不用在关注实现的细节。
GCD中的FIFO队列称为dispatch queue , 他可以保证任务执行遵循FIFO规则。 GCD中至少有5个队列可以使用。主队列, 四个全局调度队列, 在加上自己创建的任何队列
队列的种类
-
串行队列
-
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
- label:通常为0
- attr:队列类型,DISPATCH_QUEUE_SERIAL
-
-
并发队列
-
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
- label:通常为0
- attr:队列类型 DISPATCH_QUEUE_CONCURRENT
-
-
主队列(串行,只能在主线程中运行)
- dispatch_queue_t dispatch_get_main_queue(void),获取主队列
-
全局队列(并发)
- dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags)
1、主队类 mainQueue
//1. 使用主线程队列
//1> 获取主线程队列, mainQueue会让在主线程中执行。
dispatch_queu_t mainQueue = dispatch_get_main_queue()
//2> 为主线程队列提交一个任务, 以block形成提交(异步函数)
dispatch_asynac(mainQueue, ^{
NSLog(@"第一个任务");
}
//串行 vs. 并发 <!--?xml version="1.0" encoding="UTF-8" standalone="no"?--><span style="font-family:'Helvetica Neue', Helvetica, STheiti, 微软雅黑, 黑体, Arial, Tahoma, sans-serif, serif;font-size:14px;">这些术语描述当任务相对于其它任务被执行,</span><span style="background-color: rgb(255, 250, 165);-evernote-highlight:true;font-family:'Helvetica Neue', Helvetica, STheiti, 微软雅黑, 黑体, Arial, Tahoma, sans-serif, serif;font-size:14px;">任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。</span>
//2 根据自己的需求创建串行队列
//1> dispatch_queue_t mySeruakQueue = dispatch_queue_creat("myDispatch",DISPatch_QUEUE_SERIAL);
/**
参数说明: 第一个参数是给当前创建的队列起名, 苹果官方推荐使用反域名命名, 第二个参数是制定当前的队列类型, 分为 serial 和 concurrent 两种
*/
//2> 自定义串行队列在分发任务是, 开发子线程执行.
//同步 vs. 异步: <!--?xml version="1.0" encoding="UTF-8" standalone="no"?--><span style="font-family:'Helvetica Neue', Helvetica, STheiti, 微软雅黑, 黑体, Arial, Tahoma, sans-serif, serif;font-size:14px;">在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个同步函数只在完成了它预定的任务后才返回。<!--?xml version="1.0" encoding="UTF-8" standalone="no"?--><span style="font-family:'Helvetica Neue', Helvetica, STheiti, 微软雅黑, 黑体, Arial, Tahoma, sans-serif, serif;font-size:14px;">一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。</span></span>
// 异步函数
dispathc_async(mySeruakQueue, ^{
//执行的事件
});
// 同步函数
dispatch_sync(mySeruakQueue, ^{
//执行的事件
});
2、并发队列 concurrent Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);//第一个参数是用来控制队列的优先级的, 第二个参数是苹果预留的参数, 未来才会使用, 目前设置为0;
dispatch_async(globalQueue, ^{
NSLog(@"frist mission: %@",[NSThread isMainThread] ? @"is main thread" : @"isn't main thread");
});
dispatch_async(globalQueue, ^{
NSLog(@"second mission: %@",[NSThread isMainThread] ? @"is main thread" : @"isn't main thread");
});
2. 自己创建并发队列
//1> 创建任务
dispatch_queue_t myConcurrentQueue = dispatch_queue_create("com.lanou3g.GCD.myconcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//2> 提交任务
dispatch_async(myConcurrentQueue, ^{
NSLog(@"frist mission: %@",[NSThread isMainThread] ? @"is main thread" : @"isn't main thread");
});
dispatch_async(myConcurrentQueue, ^{
NSLog(@"second mission: %@",[NSThread isMainThread] ? @"is main thread" : @"isn't main thread");
});
3 任务组
//创建任务组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, myConcurrentQueue, ^{
NSLog(@"任务一");
});
dispatch_group_async(group, myConcurrentQueue, ^{
NSLog(@"任务二");
});
dispatch_group_notify(group, myConcurrentQueue, ^{
NSLog(@"任务三");
});//任务三会等待group中得 任务一和任务二 执行完后在开始执行;
4. 高级函数
</pre><p><pre name="code" class="objc">1. 延时执行任务可以在mainQueue中正常执行, 在自定义的队列中执行就会出现问题.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"延时三秒后执行的任务");
});
}
2. barier_async 会等待它之前被提交的任务并发执行完后,才开始执行, 并且执行完后才执行后来被提交的任务, 像一道墙, 隔开了之前和之后提交的任务. 这样的操作既可以保证读写的安全性,也可以提高读写效率.
dispatch_barrier_async(myConcurrentQueue, ^{
NSLog(@"写入数据任务");
[self calculate];
});
3. 苹果推荐使用dispatch_once来定义单例方法
/*
+(id)sharedInstance
{
static FooClass *object = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object = [[FoolClass alloc] init];
});
return object;
}
*/
快速迭代
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t))
/**
iterations:迭代执行的次数
queue:任务队列
block:迭代执行的代码
size_t:用来定义当前迭代到第几次,需要自己添加,如在size_t后添加index索引,记录当前的迭代次数
*/
GCD定时器
实现原理
创建一个DISPATCH_SOURCE_TYPE_TIMER类型的dispatch source,并添加到dispatch queue,通过dispatch source来响应事件
通过函数void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway),来设置dispatch source的执行事件
实现代码
//获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
//创建一个定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器的各种属性(起止时间)
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(8.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
//设置
dispatch_source_set_timer(self.timer, start, interval, 0);
//设置回调
dispatch_source_set_event_handler(self.timer, ^{
//定时器被触发时所要执行的代码
});
//开启定时器
dispatch_resume(self.timer);
//取消定时器
dispatch_cancel(self.timer);
-
5 线程间通信
-
从主线程到子线程
-
注意
- 只有异步函数与并发队列的组合,才会开启新的线程,使任务并发执行
- 通常使用异步函数将任务添加到并发队列中,来实现从主线程到子线程的通信
-
实现代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //需要在子线程中执行的任务代码 })
-
-
从子线程到主线程
-
注意
- 主队列中的任务只能在主线程中执行
- 通常使用异步/同步函数将任务添加到主队列中,来实现从子线程到主线程的通信
-
实现代码
dispatch_async(dispatch_get_main_queue(), ^{ //需要在主线程中执行的代码 })
-
总结:为了保证数据访问时的安全性, 可以使用serial queue 来解决访问的安全性.
// serial queue缺陷: 后面的任务必须等待前面的任务执行结束后在执行, 降低数据的读取效率// 如果只是读取数据 适合使用 concurrent queue
-