在开发中经常会用到多线程来处理一些比较耗时的任务,比如下载的时候存储数据、当进入一个新页面的时候将网络请求放在后台,数据下来之后再到主线程来将数据展示出来等操作,以此来满足用户大老爷的体验,让他们开开心心的用我们开发出来的应用而不是用的时候一脸懵逼的等待响应T T。平常在开发的过程中,我们只需将耗时应用放在后台的子线程、任务结束之后回到主线程来刷新页面就好了。基本下面的几行代码是我们最常用到的:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //后台处理代码 etc.... dispatch_async(dispatch_get_main_queue(), ^{ //回到主线程刷新页面 。。。 }); });
GCD的这几行代码跟万金油一样解决了我们经常遇到的下载东西的时候界面切换卡顿的问题,屡试不爽,导致如果我们遇到了其他的一些需求,比如:为了最大限度的减少对应用效率的影响,将cell中需要显示的几张小图片下载下来之后渲染成一张大图片来显示(毕竟GPU渲染一张图片要比渲染几张小图片要快的多,如果这种cell比较多的话,那么效果就更明显了),遇到这样的问题怎么办呢?首先下载过程肯定是要放在后台的,那么怎么判断图片都下载好了呢?肯定都会想到“添加线程依赖呀,用group管理呀”什么的..但是一写总是会遇到各种问题,最主要的原因就是那几行万金油让我们变得越来越懒了,越来越不想去研究后台处理的其他东西了。这一段时间比较闲,就仔细的研究了下多线程的东西,包括很多帖子以及官方文档,一看不得了啊,总结了一堆使用方法,少年看你骨骼惊奇,是一块打LOL的。。呸。。写程序好材料,不如写下来我自己查阅的同时给你们看看咯。。。
多线程主要会用到有三大类,NSThread,NSOperation,GCD,且听我细细道来
NSThread 比其他两个轻量级,使用简单。需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销
NSOperation 不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上
Grand Central Dispatch(GCD)是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用,是替代NSThread, NSOperation的高效和强大的技术,GCD是基于C语言的。
一 、NSThread
- 创建方法
- (instancetype)init; - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
如果要使用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
使用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];
当将operation添加到队列以后,operation会自行启动,不可以再调用start方法了再次开启了,这样会导致程序的异常,因为一个Operation在同一时间最多只能在一个线程执行。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:invocation];
当我们创建好了一个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加入队列的话,那么这三个操作将按顺序执行。
[email protected](copy) void (^completionBlock)(void) 为operation设置一个comletionBlock,当operation执行完之后,就会调用这个Block.
三、GCD
最后一个就是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.测试出来的,仅供参考,并不是手机创建线程的时间。
四、结语:
这篇博文总结了在开发中经常用到多线程的一些方法,不过多线程涉及的内容很多,很重要的就是多线程访问统一资源造成资源竞争以及线程间互相等待造成线程死锁问题,这些东西的篇幅都是很大的,我会在以后的博文中慢慢讨论。