在开发中常常会用到多线程来处理一些比较耗时的任务,好比下载的时候存储数据、当进入一个新页面的时候将网络请求放在后台,数据下来以后再到主线程来将数据展现出来等操做,以此来知足用户大老爷的体验,让他们开开心心的用咱们开发出来的应用而不是用的时候一脸懵逼的等待响应T T。日常在开发的过程当中,咱们只需将耗时应用放在后台的子线程、任务结束以后回到主线程来刷新页面就行了。基本下面的几行代码是咱们最经常使用到的:php
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //后台处理代码 etc.... dispatch_async(dispatch_get_main_queue(), ^{ //回到主线程刷新页面 。。。 }); });
ios
GCD的这几行代码跟万金油同样解决了咱们常常遇到的下载东西的时候界面切换卡顿的问题,屡试不爽,致使若是咱们遇到了其余的一些需求,好比:为了最大限度的减小对应用效率的影响,将cell中须要显示的几张小图片下载下来以后渲染成一张大图片来显示(毕竟GPU渲染一张图片要比渲染几张小图片要快的多,若是这种cell比较多的话,那么效果就更明显了),遇到这样的问题怎么办呢?首先下载过程确定是要放在后台的,那么怎么判断图片都下载好了呢?确定都会想到“添加线程依赖呀,用group管理呀”什么的..可是一写老是会遇到各类问题,最主要的缘由就是那几行万金油让咱们变得愈来愈懒了,愈来愈不想去研究后台处理的其余东西了。这一段时间比较闲,就仔细的研究了下多线程的东西,包括不少帖子以及官方文档,一看不得了啊,总结了一堆使用方法,少年看你骨骼惊奇,是一块打LOL的。。呸。。写程序好材料,不如写下来我本身查阅的同时给大家看看咯。。。编程
多线程主要会用到有三大类,NSThread,NSOperation,GCD,且听我细细道来数组
NSThread 比其余两个轻量级,使用简单。须要本身管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有必定的系统开销网络
NSOperation 不须要关心线程管理,数据同步的事情,能够把精力放在本身须要执行的操做上多线程
Grand Central Dispatch(GCD)是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用,是替代NSThread, NSOperation的高效和强大的技术,GCD是基于C语言的。并发
建立方法async
- (instancetype)init; - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
ide
若是要使用init方法来建立的话,是比较麻烦的,通常用在比较特殊的状况,好比继承NSTread定义本身须要使用的类,通常使用第二种方法建立,使用方法以下:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(fun) object:nil]; [thread start];
当咱们建立好thread以后,须要使用start方法来启动这个线程,这样就很简单的把方法fun放到子线程中去执行了。须要注意的是,官方API中有这句话:
The objects target and argument are retained during the execution of the detached thread. They are released when the thread finally exits.
当线程执行的时候,会对target对象和传入的参数的引用计数加1,调试的时候要注意下。
取消方法
咱们能够经过线程的状态属性判断当前线程的状态,其有三种属性,好比:
@property (readonly, getter=isExecuting) BOOL executing @property (readonly, getter=isFinished) BOOL finished @property (readonly, getter=isCancelled) BOOL cancelled
咱们能够经过这三种属性来判断当前线程的状态。那么,咱们该如何取消一个线程呢,根据官方的API来看的话,确实是有一个cancel方法,可是实际上当咱们调用这个方法的时候线程并不会中止执行,根据官方文档的描述,当咱们调用cancel方法的时候,仅仅是将线程的状态标记为了cacel,并不会取消当前线程的执行,若是咱们要手动取消的话,建议采用下面的代码
if ([NSThread currentThread].cancelled) { [NSThread exit]; }
将线程标记为取消以后,NSThread将会发送一个通知,若是写了响应通知的方法的话,以下所示:
-(void)listen:(NSNotification*)notification{ NSLog(@"exit===%@",notification.object); }
那么将会得到被结束线程的信息
exit===<NSThread: 0x7fed50f9e820>{number = 2, name = myThread}//myThread是我给线程起的名字
一样的,当线程刚刚启动的时候,也会发送一个NSWillBecomeMultiThreadedNotification通知,这个通知是主线程将会产生子线程,感兴趣的看官能够拦截看一下哦。
NSThread的其余功能
-(void)sleepUntilDate:(NSDate *)aDate 让当前线程休眠,经过阻塞当前的runloop来实现,使得当前进程不能够响应任何方法
+(void)sleepForTimeInterval:(NSTimeInterval)ti 功能、原理同上,只不过传入的参数类型不一样
@property(copy) NSString *name 设置当前线程的名字
使用NSOperation来实现多线程处理任务的话,通常使用的是它的两个子类,NSInvocationOperation和NSBlockOperation
- 建立方法
NSInvocationOperation建立方法 - (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(id)arg - (instancetype)initWithInvocation:(NSInvocation *)inv NSBlockOperation建立方法 + (instancetype)blockOperationWithBlock:(void (^)(void))block
当咱们建立好operation后,调用start方法就能够启动这个operation,以下所示(以NSInvocationOperation为例,NSBlockOperation使用方法相似):
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(fun) object:nil]; [invocation start];
可是须要注意的是,咱们这样建立出来的任务,并非在子线程里运行的,就是说若是是在主线程里启动这个opreation的话,那么这个任务就是在主线程队列运行的;若是是在子线程开启的,则是在子线程运行的,因此咱们须要建立一个NSOperationQueue,而后将这个两个操做添加进去,这样就保证了操做是在咱们自定义的队列里运行的,以下所示:
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(fun) object:nil]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperation:invocation];
当将operation添加到队列之后,operation会自行启动,不能够再调用start方法了再次开启了,这样会致使程序的异常,由于一个Operation在同一时间最多只能在一个线程执行。
当咱们建立好了一个blocKOperation以后,能够调用
- (void)addExecutionBlock:(void (^)(void))block
来向blocKOperation添加任务,可是只能在blocKOperation没启动以前添加,不能够在启动以后或者线程执行完了以后添加。并且添加的任务会开启一个新的线程去执行,不过仍然是在该队列里。
咱们还能够调用
@property(readonly, copy) NSArray <void (^executionBlocks)(void)> *
这个属性会返回当前添加到当前blocKOperation里的block组成的数组。
其余方法
NSOperationQueue常用的方法:
+(NSOperationQueue *)currentQueue 这是个类方法,放回当前线程的队列,若是当前线程是主线程的话,那么返回的就是主线程队列
+(NSOperationQueue *)mainQueue 这也是个类方法,直接获取主线程
-(void)addOperations:(NSArray
The NSOperation class is key-value coding (KVC) and key-value observing (KVO) compliant for several of its properties. As needed, you can observe these properties to control other parts of your application. To observe the properties, use the following key paths: isCancelled - read-only isAsynchronous - read-only isExecuting - read-only isFinished - read-only isReady - read-only dependencies - read-only queuePriority - readable and writable completionBlock - readable and writable
-(void)addDependency:(NSOperation *)operation 添加依赖操做
(void)removeDependency:(NSOperation *)operation 移除依赖操做
@property(readonly, copy) NSArray
[operation_one addDependency:operation_two]; [operation_two addDependency:operation_three];
而后将这三个operation加入队列的话,那么这三个操做将按顺序执行。
4.@property(copy) void (^completionBlock)(void) 为operation设置一个comletionBlock,当operation执行完以后,就会调用这个Block.
最后一个就是GCD的,GCD是基于C的一组API,使用起来很方便,并且上面两个线程能够完成的任务,使用GCD基本上均可以完成。GCD的使用方法有不少,常用的方法以下所示。
建立队列
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL);
使用dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)方法咱们能够建立一个自定义的队列,该方法有两个参数:
label 数须要传入一个C语言字符串,做为该队列的名字;
attr 代表该队列是串行队列仍是并发队列,其有两个枚举值:
DISPATCH_QUEUE_SERIAL 串行队列 DISPATCH_QUEUE_CONCURRENT 并发队列
串行队列里的任务老是会按添加顺序执行,遵循FIFO规则;
而向并发队列里添加的任务根据添加方式会选择并发执行仍是顺序执行,后面咱们会细说。
2. 向获取到的队列里添加方法
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL); //建立一个队列 dispatch_async(myQueue, 0), ^{ //须要执行的任务 });
上面代码的执行效果是,向本身建立的myQueue添加执行任务,dispatch_async该方法会马上返回。不会阻塞当前线程。
须要注意的是,若是dispatch_asyn添加任务的目标队列是串行队列的话,那么该队列里的方法会按顺序执行;若是目标队列是并发队列的话,那么将会在并发队列里开启多个线程来执行添加的任务(通常状况下,一个任务对应一个线程),CPU会切片式的执行这些线程任务。
还有一种添加方法:
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL); //建立一个队列 dispatch_sync(myQueue, 0), ^{ //须要执行的任务 });
dispatch_sync方法会阻塞当前的线程,直到block块里的代码执行完毕才会返回。因此不要在主线程里调用dispatch_sync方法,会致使主线程阻死。
无论dispatch_sync添加任务的队列是串行队列仍是并发队列,添加的任务都是遵循FIFO规则执行的。
3 . 获取线程
dispatch_queue_t queue = dispatch_get_main_queue(); //获取主队列 dispatch_queue_t queue = dispatch_get_global_queue(long identifier, unsigned long flags) //获取全局并发队列
dispatch_get_global_queue方法有两个参数:
identifier 获取到的全局并发队列优先级,该参数有四个值:
#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 后台优先级
当向更高优先级的队列里添加任务的时候,若是同时还有其余的优先级队列任务的在执行的话,高优先级队列里的任务则有更多的执行机会(由于CPU是切片式的在执行任务,高优先级队列里的任务将会得到更多的执行),从高优先级-默认优先级-低优先级,通过个人测试,每一个优先级有30%左右的执行频率差距(该数据仅供参考)。
4.建立及使用队列组
dispatch_group_t group = dispatch_group_create(); //建立一个组
下面几行代码做为一个简单的示例介绍组的用法和重要的功能。
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 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, ^{ for(int i = 0; i < 100; i ++){ NSLog(@"111 --- %d",i); } }); dispatch_group_async(group, queue2, ^{ for(int i = 0; i < 100; i ++){ NSLog(@"222 --- %d",i); } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"完事儿了"); });
上面的代码中,首先获取了两个不一样优先级的队列,而后建立了一个组。接着将队列和要执行的任务添加到组里面,最后添加一个监测组内队列执行的block块,当组里面全部的任务执行完以后,会调用这个block块。下面详细说下添加队列到组和监测组运行的这两个方法:
添加队列到组
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
该方法有三个参数:
- group: 接收添加队列的组
- queue:将要添加的队列
- block:在添加的队列里执行的Block块
监测组运行
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
该方法有三个参数:
- group: 要监测的组
- queue: 后面block执行的队列
- block: 当组里的任务执行完以后回调的Block块
使用方法和功能都一目了然,最好仍是本身练习一下才会有更深的理解。
GCD的内容不少,若是不深刻了解的话没法体会他的博大精深,上面列出来的几个方法只不过是最经常使用的几个方法,于GCD而言只不过是冰山一角,更多深刻的研究我会在之后写出来。附赠一张GCD方法图,小彩蛋:
另外,若是长时间的维持一个子线程(好比须要在子线程里建立一个tmer),那么就须要建立一个runloop来维持该线程,不然线程里的任务执行完以后就会被系统回收掉。下面是引用一苹果开发者文档的一个数据表格,主要包括了线程的建立时间以及内存分配大小。
Item |
Approximate cost |
Notes |
---|---|---|
Kernel data structures |
Approximately 1 KB |
This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk. |
Stack space |
512 KB (secondary threads) |
8 MB (OS X main thread)1 MB (iOS main thread)2The minimum allowed stack size for secondary threads is 16 KB and the stack size must be a multiple of 4 KB. The space for this memory is set aside in your process space at thread creation time, but the actual pages associated with that memory are not created until they are needed. |
Creation time |
Approximately 90 microseconds |
This value reflects the time between the initial call to create the thread and the time at which the thread’s entry point routine began executing. The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5. |
根据这个表格咱们能够看出来,iOS建立一个子线程分配的栈空间是512KB,最小分配空间是16KB,而主线程的分配空间是1M,根据个人测试,建立子线程的分配空间确实是512KB,不过主线程的栈空间查看的话仍然是512KB,你们能够本身查看下,并且只有经过NSThrad建立的线程才能够配置堆栈的大小。另外分配空间是在建立线程的时候完成的,可是实际的页面相关的内存只有到实际运行的时候在建立。可是下面的建立时间是基于酷睿双核处理器和1GB运行内存的MAC OS X v10.5.测试出来的,仅供参考,并非手机建立线程的时间。
这篇博文总结了在开发中常常用到多线程的一些方法,不过多线程涉及的内容不少,很重要的就是多线程访问统一资源形成资源竞争以及线程间互相等待形成线程死锁问题,这些东西的篇幅都是很大的,我会在之后的博文中慢慢讨论。