IOS高级编程之三:IOS 多线程编程
多线程的概念在各个操作系统上都会接触到,windows、Linux、mac os等等这些常用的操作系统,都支持多线程的概念。
当然ios中也不例外,但是线程的运行节点可能是我们平常不太注意的。
例如:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 for(int i = 0 ; i < 100 ; i++) 5 { 6 NSLog(@"===%@===%d" , [NSThread currentThread].name , i); 7 if(i == 20) 8 { 9 // 创建线程对象 10 NSThread *thread = [[NSThread alloc]initWithTarget:self 11 selector:@selector(run) object:nil]; 12 // 启动新线程 13 [thread start]; 14 // // 创建并启动新线程 15 // [NSThread detachNewThreadSelector:@selector(run) toTarget:self 16 // withObject:nil]; 17 } 18 } 19 } 20 - (void)run 21 { 22 for(int i = 0 ; i < 100 ; i++) 23 { 24 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 25 } 26 }
上面打印的内容每一次都是不同的,什么意思呢?
当我们创建了4个线程后,加上UI主线程一共5个线程。
新的线程在执行start方法之后,并不会立即执行。他们会被cpu随机的执行,只是间隔非常短,以至于我们感觉上是多个线程在同时执行。
所以线程有这么一个特点:执行的随机性。
但是我们可以设置线程的优先级,让优先级更高的线程获得更多的执行机会。
那么什么时候要使用多线程编程呢?
相信有过开发经验的程序员都知道,当我们把代码写完后,程序是一行一行逐行执行代码的,当其中一行代码需要执行较长时间(例如select一个教复杂的语句或者较多的数据时),那么程序就会出现卡顿的现象,不会相应用户的操作。
因为开启程序后会默认开启一个主线程,即UI线程。当处于刚才那种情况时,比如一个windows程序,就会出现程序暂时无响应的提示,好像电脑卡主的感觉,这是非常不好的一种感受。。。。
当我们要避免这种情况的时候,最好的方式就是多线程,开启一个新的线程,用来执行一个耗时的操作,执行完成后再让主线程来修改ui页面(如果需要的话)。
介绍完了线程的一些知识,那么下面来具体看ios中多线程的几种实现方式,主要有一下三种:
1、NSThread :就是刚刚例子中使用的方式,但是使用上比较繁琐,而且需要控制好数据的同步和异步问题
2、NSOperation 和 NSOperationQueue : 这种方式代码比较简洁,可读性强,而且使用队列的形式管理多个任务,本人比较喜欢
3、使用GCD( Grand Central Dispatch ) :相较于NSThread使用简单,使用队列管理任务
一、首先来介绍NSThread
1、创建NSThread的两种方式
-(id) initWithTarget:(id) target selector:(SEL) selector object:(id) arg:
+(void)detachNewThreadSelector:(SEL) selector toTarget:(id) target withObject:(id) arg:
第二种方式,创建NSThread后会自动启动
2、NSThread的常用方法
+currentThread : 返回当前正在执行的线程对象
3、线程的状态
一开始的例子中提了一下,线程创建后,执行了start方法并不是立即就执行了。可能ui线程执行了几毫秒后,cpu才执行它,执行几毫秒后再执行ui线程,但这个过程是随机发生的。
如果想让线程立即执行,那么可以让ui线程sleep 1毫秒,这样cpu就会执行其他可执行的线程,可以达到立即执行的效果
1 [NSThread sleepForTimeInterval:0.001];//让当前运行的线程睡眠1毫秒
线程正在执行时,调用isExecuting方法返回 YES ,线程执行完成后调用 isFinished 方法就会返回 YES
4、终止子线程
线程会以一下3种方式之一结束,结束后就处于死亡状态
1)线程执行的方法体执行完成,线程正常结束
2)执行过程中出现了错误
3)调用NSThread 类的 exit 方法来终止当前线程
在UI 线程中 ,NSThread 并没有提供方法来结束其他的子线程。但是我们可以利用 NSThread 的cancel 方法,执行该方法后, 该线程的状态为 isCancelled = YES,但并不会结束线程。
1 NSThread* thread; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 // 创建新线程对象 6 thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) 7 object:nil]; 8 // 启动新线程 9 [thread start]; 10 } 11 - (void)run 12 { 13 for(int i = 0 ; i < 100 ; i++) 14 { 15 if([NSThread currentThread].isCancelled) 16 { 17 // 终止当前正在执行的线程 18 [NSThread exit]; 19 } 20 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 21 // 每执行一次,线程暂停0.5秒 22 [NSThread sleepForTimeInterval:0.5]; 23 } 24 } 25 - (IBAction)cancelThread:(id)sender 26 { 27 // 取消thread线程,调用该方法后,thread的isCancelled方法将会返回NO 28 [thread cancel]; 29 }
利用例子中代码的形式,我们就可以达到在UI线程中结束其他子线程的目的了。
5、线程睡眠
要让线程进入阻塞状态或者睡眠状态,可以执行sleepXXX格式的方法:
+(void) sleepUntilDate:(NSDate *) aDate : 让线程睡眠,知道aDate那个时间点再醒过来
-(void)sleepForTimeInterval :让线程睡眠多少秒
6、改变线程优先级
NSThread 提供了如下几个方法来获取和设置线程的优先级
+threadPriority: 获取当前正在执行的线程的优先级
-threadPriority:获取线程实例的优先级
+setThreadPriority :(double) priority : 设置当前正在执行的线程的优先级
-setThreadPriority :(double) priority : 设置线程实例的优先级
(double) priority的 取值范围是0.0~1.0;优先级越高的线程获得的执行机会越多
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 NSLog(@"UI线程的优先级为:%g" , [NSThread threadPriority]); 5 // 创建第一个线程对象 6 NSThread* thread1 = [[NSThread alloc] 7 initWithTarget:self selector:@selector(run) object:nil]; 8 // 设置第一个线程对象的名字 9 thread1.name = @"线程A"; 10 NSLog(@"线程A的优先级为:%g" , thread1.threadPriority); 11 // 设置使用最低优先级 12 thread1.threadPriority = 0.0; 13 // 创建第二个线程对象 14 NSThread* thread2 = [[NSThread alloc] 15 initWithTarget:self selector:@selector(run) object:nil]; 16 // 设置第二个线程对象的名字 17 thread2.name = @"线程B"; 18 NSLog(@"线程B的优先级为:%g" , thread2.threadPriority); 19 // 设置使用最高优先级 20 thread2.threadPriority = 1.0; 21 // 启动2个线程 22 [thread1 start]; 23 [thread2 start]; 24 } 25 - (void)run 26 { 27 for(int i = 0 ; i < 100 ; i++) 28 { 29 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 30 } 31 }
二、使用GCD实现多线程
GCD简化了多线程的实现,主要有两个核心概念:
1、队列:队列负责管理开发者提交的任务,以先进先出的方式来处理任务。
1)串行队列:每次只执行一个任务,当前一个任务执行完成后才执行下一个任务
2)并行队列:多个任务并发执行,所以先执行的任务可能最后才完成(因为具体的执行过程导致)
2、任务:任务就是开发者提供给队列的工作单元,这些任务将会提交给队列底层维护的线程池,因此这些任务将会以多线程的方式执行。
3、创建队列
1)获取系统默认的全局并发队列:
1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2) 获取系统主线程关联的穿行队列
1 dispatch_queue_t queue = dispatch_get_main_queue();
如果将任务提交给主线程关联的串行队列,那么就相当于在程序主线程中去执行该任务。
3)创建穿行队列
1 dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
4)创建并发队列
1 dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);
5)获取当前执行代码所在队列
dispatch_get_current_queue,返回一个dispatch_queue_t类型的值
4、提交任务
使用下面的方法将任务以同步或者异步的方式提交到队列
1 //将代码块以异步的方式提交给指定队列 2 void dispatch_async(dispatch_queue_t queue, dispatch_block_t block); 3 4 //将函数以异步的方式提交给指定队列,一般执行函数的方法与执行代码块的方法比,方法名多了一个_f的后缀 5 void dispatch_async_f(dispatch_queue_t queue, void* context, dispatch_function_t work); 6 7 //将代码块以同步的方式提交给指定队列 8 void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); 9 10 //将函数以同步的方式提交给指定队列,一般执行函数的方法与执行代码块的方法比,方法名多了一个_f的后缀 11 void dispatch_sync_f(dispatch_queue_t queue, void* context, dispatch_function_t work);
1 //将代码块以异步的方式提交给指定队列,队列的线程池负责在指定时间点 when 之后执行 2 void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); 3 4 //将函数以异步的方式提交给指定队列,队列的线程池负责在指定时间点 when 之后执行 5 void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void* context, dispatch_function_t work); 6 7 //将代码块以异步的方式提交给指定队列,队列的线程池将会重复多次执行该任务 8 void dispatch_apply(size_t iterations, dispatch_queue_t queue, void(^block)(size_t)); 9 10 //将函数以异步的方式提交给指定队列,队列的线程池将会重复多次执行该任务 11 void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void* context, void(*work)(void*, size_t)); 12 13 //将代码块提交给指定队列,在应用的某个生命周期内金执行一次 14 void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
下面给出一个以异步方式向串行队列、并发队列添加任务的实例
1 // 定义2个队列 2 dispatch_queue_t serialQueue; 3 dispatch_queue_t concurrentQueue; 4 - (void)viewDidLoad 5 { 6 [super viewDidLoad]; 7 // 创建串行队列 8 serialQueue = dispatch_queue_create("fkjava.queue", DISPATCH_QUEUE_SERIAL); 9 // 创建并发队列 10 concurrentQueue = dispatch_queue_create("fkjava.queue" 11 , DISPATCH_QUEUE_CONCURRENT); 12 } 13 - (IBAction)serial:(id)sender 14 { 15 // 依次将2个代码块提交给串行队列 16 // 必须等到第1个代码块完成后,才能执行第2个代码块。 17 dispatch_async(serialQueue, ^(void) 18 { 19 for (int i = 0 ; i < 100; i ++) 20 { 21 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 22 } 23 }); 24 dispatch_async(serialQueue, ^(void) 25 { 26 for (int i = 0 ; i < 100; i ++) 27 { 28 NSLog(@"%@------%d" , [NSThread currentThread] , i); 29 } 30 }); 31 } 32 - (IBAction)concurrent:(id)sender 33 { 34 // 依次将2个代码块提交给并发队列 35 // 两个代码块可以并发执行 36 dispatch_async(concurrentQueue, ^(void) 37 { 38 for (int i = 0 ; i < 100; i ++) 39 { 40 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 41 } 42 }); 43 dispatch_async(concurrentQueue, ^(void) 44 { 45 for (int i = 0 ; i < 100; i ++) 46 { 47 NSLog(@"%@------%d" , [NSThread currentThread] , i); 48 } 49 }); 50 }
提交同步任务:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 } 5 - (IBAction)clicked:(id)sender 6 { 7 // 以同步方式先后提交2个代码块 8 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 9 , ^(void){ 10 for (int i = 0 ; i < 100; i ++) 11 { 12 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 13 [NSThread sleepForTimeInterval:0.1]; 14 } 15 }); 16 // 必须等第一次提交的代码块执行完成后,dispatch_sync()函数才会返回, 17 // 程序才会执行到这里,才能提交第二个代码块。 18 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 19 , ^(void){ 20 for (int i = 0 ; i < 100; i ++) 21 { 22 NSLog(@"%@-----%d" , [NSThread currentThread] , i); 23 [NSThread sleepForTimeInterval:0.1]; 24 } 25 }); 26 }
多次执行的任务:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 } 5 - (IBAction)clicked:(id)sender 6 { 7 // 控制代码块执行5次 8 dispatch_apply(5 9 , dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 10 // time形参代表当前正在执行第几次 11 , ^(size_t time) 12 { 13 NSLog(@"===执行【%lu】次===%@" , time 14 , [NSThread currentThread]); 15 }); 16 }
只执行一次的任务
1 @implementation FKViewController 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 } 6 - (IBAction)clicked:(id)sender 7 { 8 static dispatch_once_t onceToken; 9 dispatch_once(&onceToken, ^{ 10 NSLog(@"==执行代码块=="); 11 // 线程暂停3秒 12 [NSThread sleepForTimeInterval:3]; 13 }); 14 }
三、使用NSOperation 和 NSOPerationQueue 实现多线程
和GCD差不多,也是有队列和任务的概念
NSOperationQueue:代表一个先进先出的队列,负责管理系统提交的多个NSOperation。底层维护一个线程池,会按顺序启动线程来执行提交给队列的NSOperation
NSOperation:代表多线程任务。一般不直接使用NSOperation,而是使用NSOperation的子类。或者使用NSInvocationOperation和NSBlockOperation(这两个类继承自NSOperation);
1、NSOperation的使用
NSOperation 的使用相较于GCD是面向对象的,OC实现的,而GCD应该是C实现的(看函数的定义和使用)。
使用NSOperation 只需两步:
1)创建 NSOperationQueue 队列,并未该队列设置相关属性
2)创建 NSOperation 子类对象,并将该对象提交给 NSOperationQueue 队列,该队列将会按顺序依次启动每个 NSOperation。
2、NSOperationQueue的常用方法:
1 +currentQueue //类方法,返回执行当前NSOperation的NSOperationQueue队列 2 3 +mainQueue //返回系统主线程的NSOperationQueue队列 4 5 -(void) addOperation:(NSOperation *) operation //将operation添加到NSOperationQueue队列中 6 7 -(void) addOperations:(NSArray *) ops waitUnitlFinished:(BOLL) wait //将NSArray中包含的所有NSOperation添加到NSOperationQueue。如果第二个参数指定为YES,将会阻塞当前线程,直到提交的所有NSOperation执行完成。如果第二个参数为NO,该方法立即返回,NSArray包含的NSOperation将以异步方式执行,不会阻塞当前线程。 8 9 - operations //只读属性,返回该NSOperationQueue管理的所有NSOperation 10 -operationCount //只读属性,返回该NSOperationQueue管理的所有NSOperation数量 11 12 -cancelAllOperations: //取消NSOperationQueue队列中所有正在排队和执行的NSOperation 13 14 -waitUntilAllOperationsAreFinished://阻塞当前线程,直到该NSOperationQueue中所有排队和执行的NSOperation执行完成再接触阻塞 15 16 -(NSInteger) maxConcurrentOperationCount://返回该队列最大支持多少个并发线程 17 18 -setMaxConcurrentOperationCount:(NSInteger) count //设置该队列最大支持多少个并发线程 19 20 -setSuspended:(BOOL) suspend: //设置NSOperationQueue是否已经暂停调度正在排队的NSOperation 21 22 -(BOLL) isSuspended: //返回NSOperationQueue是否已经暂停调度正在排队的NSOperation
3、使用NSInvocationOperation 和 NSBlockOperation
NSInvocationOperation 和 NSBlockOperation 继承自 NSOperation,所以可以直接使用,用于封装需要异步执行的任务。
使用它们实现图片异步下载:
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 设置该队列最多支持10条并发线程 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 NSString* url = @"http://www.......jpg"; 12 // 以传入的代码块作为执行体,创建NSOperation 13 NSBlockOperation* operation = [NSBlockOperation 14 blockOperationWithBlock:^{ 15 // 从网络获取数据 16 NSData *data = [[NSData alloc] 17 initWithContentsOfURL:[NSURL URLWithString:url]]; 18 // 将网络数据初始化为UIImage对象 19 UIImage *image = [[UIImage alloc]initWithData:data]; 20 if(image != nil) 21 { 22 // 在主线程中执行updateUI:方法 23 [self performSelectorOnMainThread:@selector(updateUI:) 24 withObject:image waitUntilDone:YES]; 25 } 26 else 27 { 28 NSLog(@"---下载图片出现错误---"); 29 } 30 }]; 31 // 将NSOperation添加给NSOperationQueue 32 [queue addOperation:operation]; 33 } 34 -(void)updateUI:(UIImage*) image 35 { 36 self.iv.image = image; 37 }
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 设置该队列最多支持10条并发线程 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 NSString* url = @"http://www.......jpg"; 12 // 以self的downloadImageFromURL:方法作为执行体,创建NSOperation 13 NSInvocationOperation* operation = [[NSInvocationOperation alloc] 14 initWithTarget:self selector:@selector(downloadImageFromURL:) 15 object:url]; 16 // 将NSOperation添加给NSOperationQueue 17 [queue addOperation:operation]; 18 } 19 20 // 定义一个方法作为线程执行体。 21 -(void)downloadImageFromURL:(NSString *) url 22 { 23 // 从网络获取数据 24 NSData *data = [[NSData alloc] 25 initWithContentsOfURL:[NSURL URLWithString:url]]; 26 // 将网络数据初始化为UIImage对象 27 UIImage *image = [[UIImage alloc]initWithData:data]; 28 if(image != nil) 29 { 30 // 在主线程中执行updateUI:方法 31 [self performSelectorOnMainThread:@selector(updateUI:) 32 withObject:image waitUntilDone:YES]; 33 } 34 else 35 { 36 NSLog(@"---下载图片出现错误---"); 37 } 38 } 39 -(void)updateUI:(UIImage*) image 40 { 41 self.iv.image = image; 42 }
4、自定义NSOperation 的子类
创建 NSOperation 的子类,需要重写一个方法:-(void) main,该方法的方法体将作为 NSOperationQueue 完成的任务
下面自定义一个NSOperation 子类来实现下载图片的功能
1 @interface MyDownImageOperation : NSOperation 2 @property (nonatomic , strong) NSURL* url; 3 @property (nonatomic , weak) UIImageView* imageView; 4 - (id)initWithURL:(NSURL*)url imageView:(UIImageView*)iv; 5 @end
1 @implementation MyDownImageOperation 2 - (id)initWithURL:(NSURL*)url imageView:(UIImageView*)iv 3 { 4 self = [super init]; 5 if (self) { 6 _imageView = iv; 7 _url = url; 8 } 9 return self; 10 } 11 // 重写main方法,该方法将作为线程执行体 12 - (void)main 13 { 14 // 从网络获取数据 15 NSData *data = [[NSData alloc] 16 initWithContentsOfURL:self.url]; 17 // 将网络数据初始化为UIImage对象 18 UIImage *image = [[UIImage alloc]initWithData:data]; 19 if(image != nil) 20 { 21 // 在主线程中执行updateUI:方法 22 [self performSelectorOnMainThread:@selector(updateUI:) 23 withObject:image waitUntilDone:YES]; // ① 24 } 25 else 26 { 27 NSLog(@"---下载图片出现错误---"); 28 } 29 } 30 -(void)updateUI:(UIImage*) image 31 { 32 self.imageView.image = image; 33 }
viewController代码:
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 设置该队列最多支持10条并发线程 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 // 定义要加载的图片的URL 12 NSURL* url = [NSURL URLWithString:@"http://www.crazyit.org/logo.jpg"]; 13 // 创建FKDownImageOperation对象 14 MyDownImageOperation* operation = [[MyDownImageOperation alloc] 15 initWithURL:url imageView:self.iv]; 16 // 将NSOperation的子类的实例提交给NSOperationQueue 17 [queue addOperation:operation]; 18 }