iOS中,只有主线程跟Cocoa关联,也即是在这个线程中,更新UI总是有效的,如果在其他线程中更新UI有时候会成功,但可能失败。所以苹果要求开发者在主线程中更新UI。但是如果我们吧所有的操作都放置在主线程中执行,当遇到比较耗时的操作的时候,势必会阻塞线程,出现界面卡顿的情况。这时候采取将耗时的操作放入后台线程中操作,且保持主线程只更新UI是我们推荐的做法。
在iOS中,要实现多线程,一共有四种方式。 它们分别是:
pthreadsPOSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32[1]这篇文章不介绍
NSThread需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销
NSOperation & NSOperationQueue
GCDiOS4开始,苹果发布了GCD,可以自动对线程进行管理。极大的方便了多线程的开发使用
一、pthread
pthread是一套基于C的API,它不接受cocoa框架的控制:当手动创建pthread的时候,cocoa框架并不知道。 苹果不建议在cocoa中使用pthread,但是如果为了方便不得不使用,我们应该小心的使用。
下面这些方法可以创建pthread
OC
pthread_attr_t qosAttribute;
pthread_attr_init(&qosAttribute);
pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0);
pthread_create(&thread, &qosAttribute, f,NULL);
SWIFT
varthread= pthread_t()
var qosAttribute = pthread_attr_t()
pthread_attr_init(&qosAttribute)
pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0)
pthread_create(&thread, &qosAttribute, f,nil)
并且,可以使用下面的API对一个pthread进行修改。
苹果的文档中,有一篇文档讲述了GCD中使用pthread的禁忌:Compatibility with POSIX Threads。
6OBJECTIVE-C
pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND,0);
SWIFT
pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0)
二、NSThread
对于NSThread,在使用的过程中,我们需要手动完成很多动作才能确保线程的顺利运行。但与此同时,它给我们带来更大的定制化空间。
1.创建NSThread。
对于NSThread的创建,苹果给出了三种使用方式。
detachNewThreadSelector(_:toTarget:with:) detachNewThreadSelector 会创建一个新的线程,并直接进入线程执行。
initWith(Target:selector:object:) iOS10.0之前的创建方式,需要手动执行。
initWithBlock iOS10.0之后,可以创建一个执行block的线程。
2.NSThread线程通信。
如果我们想对已经存在的线程进行操作,可以使用
performSelector:onThread:withObject:waitUntilDone:
跳转到目标线程执行,实现线程间跳转,达到线程通信的目的。但是需要注意的是,这个方法不适合频繁的进行通信,尤其是对于一些敏感的操作。
3.NSThread线程的状态。
在一个线程中,可以通过相关的函数获取到它的当前状态。
+ isMainThread:判断当前线程是不是主线程。
+ mainThread:获取当前的主线程。
+ isMultiThreaded :判断当前环境是不是多线程环境
+ threadDictionary :获取包含项目中的线程的字典
@property(readonly, getter=isExecuting)BOOL executingNS_AVAILABLE(10_5, 2_0); 是否处于运行状态
@property(readonly, getter=isFinished)BOOL finishedNS_AVAILABLE(10_5, 2_0); 是否处于完成状态
@property(readonly, getter=isCancelled)BOOL cancelledNS_AVAILABLE(10_5, 2_0); 是否处于取消状态
4.NSThread线程的优先级。
可通过给NSThread设置优先级。以便让开发者更灵活的控制程序的执行。
+ threadPriority Returns the currentthread’s priority. 返回当前线程的优先级别
threadPriority The receiver’s priority 消息发送者的优先级,这个发送者是一个NSThread对象
+ setThreadPriority: Sets the currentthread’s priority. 设置线程的优先级
5.停止线程/终止线程
+ sleepUntilDate: Blocks the currentthreaduntil the time specified. 直到某时刻执行
+ sleepForTimeInterval: Sleeps thethreadfora given time interval. 暂停线程
+ exit Terminates the currentthread. 关闭线程,这里调用之前,为了确保程序的安全,我们应在明确线程的状态是isFinished 和 isCancelled的时候执行。
- cancel Changes the cancelled state of the receiver to indicate that it should exit. 主动进入取消状态,如果当时线程没有完成,会继续执行完成。
6.使用NSThread
- (void)viewDidLoad {
[superviewDidLoad];
_testCount = 100;
_t1 = [[NSThread alloc] initWithTarget:selfselector:@selector(test) object:nil];
_t1.name = @"线程一";
[_t1 start];
NSInvocationOperation
}
-(void)test{
for(inti = 0; i < 5 ; i++) {
[NSThreadsleepForTimeInterval:0.05];
NSLog(@"%ld,%@",(long)_testCount--,[[NSThreadcurrentThread] name]);
}
}
三、NSOperation & NSOperationQueue
1.NSOperation
NSOperation是对于线程对象的抽象封装,不会被直接使用,在日常的开发中,会使用它的两个子类:NSInvocationOperation和NSBlockOperation。NSInvocationOperation类是NSOperation的具体子类,用于管理指定为调用的单个封装任务的执行。 您可以使用此类来启动包含在指定对象上调用选择器的操作。 此类实现非并发操作。NSBlockOperation类也是NSOperation的具体子类,用于管理一个或多个block块的并发执行。 您可以使用此对象一次执行多个block,而无需为每个块创建单独的操作对象。 当执行多个程序段时,只有当所有程序段执行完毕时,才会将操作本身完成。
NSInvocationOperation实现非并发操作。
3_invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];
_invCationOp.name = @"invocation线程";
[_invCationOp start];
打印:
{number = 1, name = main},
{number = 1, name = main}
从打印结果可以看出,NSInvocationOperation实现的是非并法的操作,至于在哪个线程中操作,取决于start的当前调用时的线程。
如果我们需要创建一个并发的Queue,可以使用NSBlockOperation。如果我们像这样创建:
- (void)blockOperation{
NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp addExecutionBlock:^{
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp addExecutionBlock:^{
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp start];
}
打印:
2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name = main}
2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name = main}
2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name = main}
显然,这里都在主线程中执行,不能证明NSBlockOperation具有并发的能力,这是因为,每个NSBlockOperation对象的会优先在主线程中执行。如果主线程受到阻塞的时候才会开辟另一个线程去执行其他的操作。比如向下面这样:
//NSBlockOperation
- (void)blockOperation{
NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp addExecutionBlock:^{
[NSThreadsleepForTimeInterval:2.0];
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp addExecutionBlock:^{
[NSThreadsleepForTimeInterval:2.0];
NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);
}];
[blockOp start];
}
打印:
2017-05-23 15:28:37.780多线程[5645:617976] {number = 1, name = main},{number = 1, name = main}
2017-05-23 15:28:39.848多线程[5645:618027] {number = 3, name = (null)},{number = 1, name = (null)}
2017-05-23 15:28:39.848多线程[5645:618028] {number = 4, name = (null)},{number = 1, name = (null)}=
这里就是异步执行了。NSBlockOperation在使用的过程中,会针对主线程当前的使用情况,选择性的创建其他的线程。在提升流畅度的同时,还节约了资源。
2.NSOperationQueue
NSOperationQueue:手动管理异步执行。 如果我们想使用并发,并且要作到精确掌握并发的线程。可以使用NSOperationQueue。这是一个操作队列,如果将NSOperation的具体子类对象添加进来的时候,开启之后,所有的对象没有先后,会异步执行各自的代码。
- (void)operationQueue{
NSOperationQueue*queue = [[NSOperationQueuealloc] init];
NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{
NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThreadcurrentThread],[NSThreadmainThread]);
}];
NSInvocationOperation*invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];
invCationOp.name = @"invocation线程";
[queue addOperation:invCationOp];
[queue addOperation:blockOp];
}
打印:
2017-05-23 15:38:45.110多线程[5772:633972] 100,{number = 3, name = (null)},{number = 1, name = (null)}
2017-05-23 15:38:45.203多线程[5772:633975] 99,{number = 4, name = (null)},{number = 1, name = (null)}
在NSOperationQueue中,正常情况下,所有的operation的执行次序是随机,如果我们想要某个operation被率先执行,可以将这个operation的优先级调高。对于优先级有以下的选择:
[invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];
对于优先级有以下的选择:
typedefNS_ENUM(NSInteger,NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow= -8L, 最低
NSOperationQueuePriorityLow= -4L, 次低
NSOperationQueuePriorityNormal= 0, 普通 不做任何操作的operation的优先级是这个
NSOperationQueuePriorityHigh= 4, 次高
NSOperationQueuePriorityVeryHigh= 8 最高
};
当然,如果有很多operation,使用的优先级不能满足的时候,还可以设置 operation的依赖关系。 设置依赖之后,将会先执行依赖对象。
[invCationOp addDependency:blockOp];
值得注意的是:优先级和依赖关系不是冲突的。 优先级的选择会在依赖关系下发生效果,也就是,在依赖关系成立的情况下,优先级的才会有效。
三、GCD - Grand Central Dispatch
Grand Central Dispatch早在Mac OS X 10.6雪豹上就已经存在了。后在iOS4.0的时候被引入。Grand Central Dispatch是OS X中的一个低级框架,用于管理整个操作系统中的并发和异步执行任务。本质上,随着处理器核心的可用性,任务排队等待执行。通过允许系统控制对任务的线程分配,GCD更有效地使用资源,这有助于系统和应用程序运行更快,高效和响应。
GCD的一个重要的对象是队列:Dispatch Queue。跟Operationqueue类似,通过将Operation加入到队列中,执行相应的单元。在GCD中,大量采用了block的形式创建类似的operation。
1. Dispatch Queue 创建
Dispatch Queue 分为两类,主要是根据并行和串行来区分:
a. Serial Dispatch Queue: 线性执行的线程队列,遵循FIFO(First In First Out)原则; 又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。 main dipatch属于这个类别。
b. Concurrent Dispatch Queue: 并发执行的线程队列,并发执行的处理数取决于当前状态。又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。系统提供四个全局并发队列,这四个队列有这对应的优先级,用户是不能够创建全局队列的,只能获取。
我们可以自定义队列,默认创建的队列是串行的,但是也可以指定创建一个并行的队列:
//串行队列dispatch_queue_create("com.deafultqueue", 0)
//串行队列dispatch_queue_create("com.serialqueue", DISPATCH_QUEUE_SERIAL)//并行队列dispatch_queue_create("com.concurrentqueue", DISPATCH_QUEUE_CONCURRENT)
除了自定义队列,系统其实也为有一些已经公开的队列。这些队列不需要我们显示的创建,只能通过获取的方式得到:
dispatch_get_main_queue() 获取当前的APP主队列,这个队列在主线程中,通常我们调用它进行界面的刷新。
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 获取全局的Concurrent队列,这里苹果提供了四种不同的优先级,
#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
也即时有四个不同的并行队列。
2. Dispatch Queue 执行
GCD的队列有串行和并行两种队列,同时我们可以同步和异步两种方式执行队列,所以最多有四种不同的场景。
(1)串行同步。 凡涉及到同步的的都会阻塞线程。 UI线程—也即是我们的所说的主线程默认情况下其实就是执行同步的。这个时候如果有一些耗时间的操作,则会出现卡顿的现象。这种方式大部分情况用于能快速响应和后台线程的耗时场景中。
//串行同步
dispatch_queue_t serialQ = dispatch_queue_create("串行", DISPATCH_QUEUE_SERIAL);//创建一个串行队列
NSLog(@"%@",[NSThreadcurrentThread].description);
dispatch_sync(serialQ, ^{
[NSThreadsleepForTimeInterval:3];
NSLog(@"%@ -- %@队列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
dispatch_sync(serialQ, ^{
NSLog(@"%@ -- %@队列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
(2)串行异步。 这种情况下,GCD会开辟另一个新的线程,让队列中的内容在新的线程中按顺序执行。
//串行异步
dispatch_async(serialQ, ^{
NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
dispatch_async(serialQ, ^{
NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
dispatch_async(serialQ, ^{
NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
dispatch_async(serialQ, ^{
NSLog(@"%@ 4-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);
});
(3)并行同步。 因为是同步执行,所以实际上这里的并行是没有意义的。 依然在当前的线程中按顺序执行,并阻塞。
dispatch_queue_t conCurrentQ = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);//创建一个并行行队列
//并行同步
dispatch_sync(conCurrentQ, ^{
[NSThreadsleepForTimeInterval:0.2];
NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
dispatch_sync(conCurrentQ, ^{
[NSThreadsleepForTimeInterval:0.2];
NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
dispatch_sync(conCurrentQ, ^{
[NSThreadsleepForTimeInterval:0.2];
NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
(4)并行异步。 并行异步将极大的利用资源。首先会开辟新的线程,并且,当所有线程备占用的情况下,会继续开辟(如果没有限制的话)。所以这里还涉及线程的最大值的问题。
//并行异步
dispatch_async(conCurrentQ, ^{
NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
dispatch_async(conCurrentQ, ^{
NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
dispatch_async(conCurrentQ, ^{
NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
dispatch_async(conCurrentQ, ^{
NSLog(@"%@ 4-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
});
3. Dispatch Queue 暂停和继续
我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。
4. Dispatch Queue 的销毁
每个队列在执行完添加到其中的所有的block事件的时候,在ARC模式下,会被自动销毁。 但是在手动管理内存的时候,我们需要调用
dispatch_release(queue);
来释放。
5.队列组 Dispatch Group (这些内容来自http://blog.csdn.net/q199109106q/article/details/8566300)
多数情况下,我们可能会遇到这种问题: 对一个页面中的多张图片,每张图片要单独的进行网络请求,我们没有办法保证每次的请求时间是一样的,但是项目经理说必须要在获取所有的图片的情况下,才可以进行对页面的刷新。这里有个很好例子可以解决这个问题。
// 根据url获取UIImage
- (UIImage *)imageWithURLString:(NSString*)urlString {
NSURL*url = [NSURLURLWithString:urlString];
NSData*data = [NSDatadataWithContentsOfURL:url];
return[UIImage imageWithData:data];
}
- (void)downloadImages {
// 异步下载图片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 下载第一张图片
NSString*url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";
UIImage *image1 = [selfimageWithURLString:url1];
// 下载第二张图片
NSString*url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";
UIImage *image2 = [selfimageWithURLString:url2];
// 回到主线程显示图片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView1.image = image1;
self.imageView2.image = image2;
});
});
}
但是我们发现,事实上,图片一和 二两者在请求的过程中是完全独立的, 但是这里明显的,图片一的下载将阻塞,直到下载完才会开始图片二的下载。这种方式毕竟还是有瑕疵的啊哈。
Dispatch Group可以帮助解决这个问题。 它是Dispatch Queue的组合,被加入到group的queue会在组内其他的queue也执行完操作的时候,有group统一调用预设好的一个block。最重要的是,在group中的内容是可以异步执行的。也即是多个队列在不同的线程执行。 如果图片大小差不多的话,这种方式将节省我们不一半的时间。 我们来看看这个模型。
//dispaach group
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue1, ^{
[NSThreadsleepForTimeInterval:5.0];
NSLog(@"第一个项目执行完成。");
});
dispatch_group_async(group, queue2, ^{
[NSThreadsleepForTimeInterval:10.0];
NSLog(@"第二个项目执行完成。");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"集体回调");
});
6.GCD的其他的用法
(1)控制一段代码只执行一次。用在创建单例的时候再好不过了。
//控制代码只执行一次数
for(inti = 1 ;i <= 10 ;i++){
staticdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"被执行 %d次",i);
});
}
打印结果:2017-05-24 18:15:17.704多线程[10542:898532]被执行1次
(2) 只能控制执行一次是不是有点不够完美 。dispatch_apply 可以让你控制一段代码执行任意多次。这里的执行是异步执行的,如果为了确保顺序执行,应该对执行的内容进行加锁。
//控制执行任意多次
dispatch_queue_t queueX = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__blockintcount = 0;
NSLock*lock = [[NSLockalloc]init];
dispatch_apply(5, queueX, ^(size_t index) {
[lock lock];
NSLog(@"%d,%zu",count++,index);
[lock unlock];
});
(3)做一个block式的延时。 除了使用performSelector之外,我们还以使用dispatch_after进行延时,并且是以block的形式进行。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"五秒钟之后执行的代码。");
});