1.1 iOS有三种多线程编程的技术,分别是:
1.、NSThread
2、Cocoa NSOperation (iOS多线程编程之NSOperation和NSOperationQueue的使用)
3、GCD 全称:Grand Central Dispatch( iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用)
这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。
这篇我们主要介绍和使用NSThread,后面会继续2、3 的讲解和使用。
1.2 三种方式的优缺点介绍:
NSThread:
优点:NSThread 比其他两个轻量级
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
NSThread实现的技术有下面三种:
Cocoa threads
POSIX threads
Multiprocessing Services
一般使用cocoa thread 技术。
Cocoa operation
优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。
Cocoa operation 相 关的类是 NSOperation ,NSOperationQueue。NSOperation是个抽象类,使用它必须用它的子类,可以实现它或者使用 它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。创建NSOperation子类的对象,把对 象添加到NSOperationQueue队列里执行。
GCD
Grand Central Dispatch (GCD) 是Apple开发的一个多核编程的解决方法。在iOS4.0开始之后才能使用。GCD是一个替代诸如 NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。现在的iOS系统都 升级到6了,所以不用担心该技术不能使用。
介绍完这三种多线程编程方式,我们这篇先介绍NSThread的使用。
2、NSThread的使用
2.1 NSThread 有两种直接创建方式:
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument
第一个是实例方法,第二个是类方法
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];
NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil];
[myThread start];
2.2参数的意义:
selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
target :selector消息发送的对象
argument:传输给target的唯一参数,也可以是nil
第一种方式会直接创建线程并且开始运行线程,第二种方式是先创建线程对象,然后再运行线程操作,在运行线程操作前可以设置线程的优先级等线程信息
2.3 PS:不显式创建线程的方法:
用NSObject的类方法 performSelectorInBackground:withObject: 创建一个线程:
[Obj performSelectorInBackground:@selector(doSomething) withObject:nil];
2.4 下载图片的例子:
2.4.1 新建singeView app
新建项目,并在xib文件上放置一个imageView控件。按住control键拖到viewController.h文件中创建imageView IBOutlet ViewController.m中实现:
// ViewController.m
// NSThreadDemo
//
// Created by rongfzh on 12-9-23.
// Copyright (c) 2012年 rongfzh. All rights reserved.
//
#import "ViewController.h"
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"
@interface ViewController ()
@end
@implementation ViewController
d)downloadImage:(NSString *) url{
NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
UIImage *image = [[UIImage alloc]initWithData:data];
if(image == nil){
}else{
[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
}
}
d)updateUI:(UIImage*) image{
self.imageView.image = image;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL];
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL];
[thread start];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
2.4.2线程间通讯
线程下载完图片后怎么通知主线程更新界面呢?
[selfperformSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
performSelectorOnMainThread是NSObject的方法,除了可以更新主线程的数据外,还可以更新其他线程的比如:
用:performSelector:onThread:withObject:waitUntilDone:
运行下载图片:
图片下载下来了。
2.3 线程同步
我们演示一个经典的卖票的例子来讲NSThread的线程同步:
.h
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
int tickets;
int count;
NSThread* ticketsThreadone;
NSThread* ticketsThreadtwo;
NSCondition* ticketsCondition;
NSLock *theLock;
}
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
tickets = 100;
count = 0;
theLock = [[NSLock alloc] init];
// 锁对象
ticketsCondition = [[NSCondition alloc] init];
ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[ticketsThreadone setName:@"Thread-1"];
[ticketsThreadone start];
ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[ticketsThreadtwo setName:@"Thread-2"];
[ticketsThreadtwo start];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)run{
while (TRUE) {
// 上锁
// [ticketsCondition lock];
[theLock lock];
if(tickets >= 0){
[NSThread sleepForTimeInterval:0.09];
count = 100 - tickets;
NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);
tickets--;
}else{
break;
}
[theLock unlock];
// [ticketsCondition unlock];
}
}
如果没有线程同步的lock,卖票数可能是-1.加上lock之后线程同步保证了数据的正确性。
上面例子我使用了两种锁,一种NSCondition ,一种是:NSLock。 NSCondition我已经注释了。
线程的顺序执行
他们都可以通过
[ticketsConditionsignal]; 发送信号的方式,在一个线程唤醒另外一个线程的等待。
比如:
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
tickets = 100;
count = 0;
theLock = [[NSLock alloc] init];
// 锁对象
ticketsCondition = [[NSCondition alloc] init];
ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[ticketsThreadone setName:@"Thread-1"];
[ticketsThreadone start];
ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[ticketsThreadtwo setName:@"Thread-2"];
[ticketsThreadtwo start];
NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];
[ticketsThreadthree setName:@"Thread-3"];
[ticketsThreadthree start];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
-(void)run3{
while (YES) {
[ticketsCondition lock];
[NSThread sleepForTimeInterval:3];
[ticketsCondition signal];
[ticketsCondition unlock];
}
}
- (void)run{
while (TRUE) {
// 上锁
[ticketsCondition lock];
[ticketsCondition wait];
[theLock lock];
if(tickets >= 0){
[NSThread sleepForTimeInterval:0.09];
count = 100 - tickets;
NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);
tickets--;
}else{
break;
}
[theLock unlock];
[ticketsCondition unlock];
}
}
wait是等待,我加了一个 线程3 去唤醒其他两个线程锁中的wait
其他同步
我们可以使用指令@synchronized来简化 NSLock的使用,这样我们就不必显示编写创建NSLock,加锁并解锁相关代码。
- (void)doSomeThing:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the@synchronizeddirective.
}
}
还有其他的一些锁对象,比如:循环锁NSRecursiveLock,条件锁NSConditionLock,分布式锁NSDistributedLock等等,可以自己看官方文档学习
怎样使用NSOperation
NSOperation本身是一个抽象类,要使用可以通过以下几个办法:
使用NSInvocationOperation
使用NSBlockOperation
自定义NSOperation的子类
使用
NSInvocationOperation
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
// 调用start方法执行操作op操作
[op start];
NSBlockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task0---%@", [NSThread currentThread]);
}];
[op start];
根据打印的结果我们会发现,直接调用start方法时,系统并不会开辟一个新的线程去执行任务,任务会在当前线程同步执行.
注意: 这里我们说的是当前线程而非主线程,意即:如果是在主线程中调用op的start方法,那么该任务是在主线程中执行;但如果是在其他子线程调用start方法,任务则是在其他子线程执行.
当然NSBlockOperation还有一种使用方法addExecutionBlock:使得我们可以给其添加更多的操作,使用如下:
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task0---%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"task1----%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"task2----%@", [NSThread currentThread]);
}];
// 开始必须在添加其他操作之后
[op start];
由打印的结果发现: task0的结果和前面的结论一样,是执行在主线程中的(因为是在主线程中调用start方法),但task1和task2都是在自己的新线程中执行.也就是说:当NSBlockOperation封装的操作数大于1的时候,就会执行异步操作.
自定义NSOperation
自定义NSOperation的方法也很简单,我们需要做到下面几个步骤:
1.子类化NSOperation
2.在.m文件里面实现-(void)main方法
3.初始化该操作的时候直接调用alloc及init即可
4.同样可以通过start方法让你自定义的任务跑在当前线程中
关键代码:
自定义NSOperation的实现文件:
#import "AROperation.h"
@implementation AROperation
- (void)main {
NSLog(@"this is my custom operation ---- %@", [NSThread currentThread]);
}
@end
使用(在任何需要启用该任务的地方):
// 该操作被执行时就会执行op内部定义的任务
AROperation *op = [[AROperation alloc] init];
[op start];
怎样使用NSOperationQueue
NSOperation的start方法默认是同步执行任务,这样的使用并不多见,只有将NSOperation与NSOperationQueue进行结合,才会发挥出这种多线程技术的最大功效.当NSOperation被添加到NSOperationQueue中后,就会全自动地执行异步操作.
NSOperationQueue的种类:
自带主队列[NSOperationQueue mainQueue]: 添加到主队列中的任务都会在主线程中执行
自己创建队列(非主队列)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];: 这种队列同时包含串行、并发的功能,添加到非主队列的任务会自动放到子线程中执行
向NSOperationQueue中添加操作:
直接添加
[queue addOperation:op1];
使用block添加,block的内容会被包装成operation对象添加到队列
[queue addOperationWithBlock:^{
}];
操作一但被到添加到队列中,就会自动异步执行.
设置NSOperationQueue的最大并发数
NSOperationQueue可以通过以下方法设置最大并发数,
setMaxConcurrentOperationCount:,值得注意的是:当并发数为1就变成了串行执行任务
NSOperationQueue的暂停恢复和取消
取消
NSOperation有一个cancel方法可以取消单个操作
NSOperationQueue的cancelAllOperations相当于队列中的每个operation调用了cancel方法,会取消队列里面全部的操作.
但是,不能取消正在进行中的任务,队列调用了cancelAllOperations后会等当前正在进行的任务执行完闭后取消后面的操作
挂起和恢复
isSuspended : 判断是否挂起
setSuspended: YES表示挂起,NO表示恢复
和取消功能类似,我们同样不能挂起正在运行中的操作,队列会等当前操作结束后将后面的操作暂停(挂起)
因此, 我们在自定义NSOperation的时候需要注意,最好可以经常通过判断isCancelled方法检测操作是否被取消,以响应外部可能进行的取消操作.如:
// 自定义NSOperation类.m文件的main方法实现
- (void)main {
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"SubTask---0---%zd",i);
}
// 判断当前任务是否被取消,如果已经取消,及时返回
if (self.cancelled) {
return;
}
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"SubTask---1---%zd", i);
}
if (self.cancelled) {
return;
}
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"SubTask---2---%zd",i);
}
}
添加依赖和监听
通过设置操作间的依赖,可以确定这些操作的执行顺序
如:
[op3 addDependency:op1];
[op3 addDependency:op2];
表示op3会在op1和op2都执行完毕后才执行
添加依赖的时候要注意防止添加循环依赖,此外我们还可以在不同队列的operation之间添加依赖
监听
op.completeBlock可以监听一个操作执行完毕的时刻,这个block里面可以添加一些我们需要执行的操作
这个block里面的操作仍然是在子线程执行,但不一定和被监听的操作在同一个线程
线程间通信
有时我们在子线程中执行完一些操作的时候,需要回到主线程做一些事情(如进行UI操作),因此需要从当前线程回到主线程,以下载并显示图片为例,方法如下:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 子线程下载图片
[queue addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:data];
// 回到主线程进行显示
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
NSOperation的使用还是比较简单的,但是要注意的细节比较多,一些方法比较容易被忽略,
1>无论使用哪种方法进行多线程开发,每个线程启动后并不一定立即执行相应的操作,具体什么时候由系统调度(CPU空闲时就会执行)。
2>更新UI应该在主线程(UI线程)中进行,并且推荐使用同步调用,常用的方法如下:
- - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait (或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法传递主线程[NSThread mainThread])
- [NSOperationQueue mainQueue] addOperationWithBlock:
- dispatch_sync(dispatch_get_main_queue(), ^{})
3>NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。
4>对于简单的多线程开发建议使用NSObject的扩展方法完成,而不必使用NSThread。
5>可以使用NSThread的currentThread方法取得当前线程,使用 sleepForTimeInterval:方法让当前线程休眠。
6>NSOperation进行多线程开发可以控制线程总数及线程依赖关系。
7>创建一个NSOperation不应该直接调用start方法(如果直接start则会在主线程中调用)而是应该放到NSOperationQueue中启动。
8>相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。
9>NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD。
10>在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效。
11>在GCD中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行)。
12>相比使用NSLock,@synchronized更加简单,推荐使用后者。