大家都知道,在開發(fā)過程中應(yīng)該盡可能減少用戶等待時(shí)間,讓程序盡可能快的完成運(yùn)算??墒菬o論是哪種語(yǔ)言開發(fā)的程序最終往往轉(zhuǎn)換成匯編語(yǔ)言進(jìn)而解釋成機(jī)器碼來執(zhí)行。但是機(jī)器碼是按順序執(zhí)行的,一個(gè)復(fù)雜的多步操作只能一步步按順序逐個(gè)執(zhí)行。改變這種狀況可以從兩個(gè)角度出發(fā):對(duì)于單核處理器,可以將多個(gè)步驟放到不同的線程,這樣一來用戶完成UI操作后其他后續(xù)任務(wù)在其他線程中,當(dāng)CPU空閑時(shí)會(huì)繼續(xù)執(zhí)行,而此時(shí)對(duì)于用戶而言可以繼續(xù)進(jìn)行其他操作;對(duì)于多核處理器,如果用戶在UI線程中完成某個(gè)操作之后,其他后續(xù)操作在別的線程中繼續(xù)執(zhí)行,用戶同樣可以繼續(xù)進(jìn)行其他UI操作,與此同時(shí)前一個(gè)操作的后續(xù)任務(wù)可以分散到多個(gè)空閑CPU中繼續(xù)執(zhí)行(當(dāng)然具體調(diào)度順序要根據(jù)程序設(shè)計(jì)而定),及解決了線程阻塞又提高了運(yùn)行效率。蘋果從iPad2 開始使用雙核A5處理器(iPhone中從iPhone 4S開始使用),A7中還加入了協(xié)處理器,如何充分發(fā)揮這些處理器的性能確實(shí)值得思考。今天將重點(diǎn)分析iOS多線程開發(fā)。
1、多線程簡(jiǎn)介
當(dāng)用戶播放音頻、下載資源、進(jìn)行圖像處理時(shí)往往希望做這些事情的時(shí)候其他操作不會(huì)被中斷或者希望這些操作過程中更加順暢。在單線程中一個(gè)線程只能做一件事情,一件事情處理不完另一件事就不能開始,這樣勢(shì)必影響用戶體驗(yàn)。早在單核處理器時(shí)期就有多線程,這個(gè)時(shí)候多線程更多的用于解決線程阻塞造成的用戶等待(通常是操作完UI后用戶不再干涉,其他線程在等待隊(duì)列中,CPU一旦空閑就繼續(xù)執(zhí)行,不影響用戶其他UI操作),其處理能力并沒有明顯的變化。如今無論是移動(dòng)操作系統(tǒng)還是PC、服務(wù)器都是多核處理器,于是“并行運(yùn)算”就更多的被提及。一件事情我們可以分成多個(gè)步驟,在沒有順序要求的情況下使用多線程既能解決線程阻塞又能充分利用多核處理器運(yùn)行能力。
下圖反映了一個(gè)包含8個(gè)操作的任務(wù)在一個(gè)有兩核心的CPU中創(chuàng)建四個(gè)線程運(yùn)行的情況。假設(shè)每個(gè)核心有兩個(gè)線程,那么每個(gè)CPU中兩個(gè)線程會(huì)交替執(zhí)行,兩個(gè)CPU之間的操作會(huì)并行運(yùn)算。單就一個(gè)CPU而言兩個(gè)線程可以解決線程阻塞造成的不流暢問題,其本身運(yùn)行效率并沒有提高,多CPU的并行運(yùn)算才真正解決了運(yùn)行效率問題,這也正是并發(fā)和并行的區(qū)別。當(dāng)然,不管是多核還是單核開發(fā)人員不用過多的擔(dān)心,因?yàn)槿蝿?wù)具體分配給幾個(gè)CPU運(yùn)算是由系統(tǒng)調(diào)度的,開發(fā)人員不用過多關(guān)心系統(tǒng)有幾個(gè)CPU。開發(fā)人員需要關(guān)心的是線程之間的依賴關(guān)系,因?yàn)橛行┎僮鞅仨氃谀硞€(gè)操作完成完才能執(zhí)行,如果不能保證這個(gè)順序勢(shì)必會(huì)造成程序問題。

2、iOS多線程
在iOS中每個(gè)進(jìn)程啟動(dòng)后都會(huì)建立一個(gè)主線程(UI線程),這個(gè)線程是其他線程的父線程。由于在iOS中除了主線程,其他子線程是獨(dú)立于Cocoa Touch的,所以只有主線程可以更新UI界面(新版iOS中,使用其他線程更新UI可能也能成功,但是不推薦)。iOS中多線程使用并不復(fù)雜,關(guān)鍵是如何控制好各個(gè)線程的執(zhí)行順序、處理好資源競(jìng)爭(zhēng)問題。常用的多線程開發(fā)有三種方式:
2.1、NSThread
2.2、NSOperation
2.3、GCD
三種方式是隨著iOS的發(fā)展逐漸引入的,所以相比而言后者比前者更加簡(jiǎn)單易用,并且GCD也是目前蘋果官方比較推薦的方式(它充分利用了多核處理器的運(yùn)算性能)。做過.Net開發(fā)的朋友不難發(fā)現(xiàn)其實(shí)這三種開發(fā)方式 剛好對(duì)應(yīng).Net中的多線程、線程池和異步調(diào)用,因此在文章中也會(huì)對(duì)比講解。
3、NSThread
NSThread是輕量級(jí)的多線程開發(fā),使用起來并不復(fù)雜,但是使用NSThread需要自己管理多線程的生命周期??梢允褂脤?duì)象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接將操作添加到線程中并啟動(dòng),也可以使用對(duì)象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 創(chuàng)建一個(gè)線程對(duì)象調(diào)用start啟動(dòng)。
創(chuàng)建解決線程阻塞的問題
在資源下載過程中,由于網(wǎng)絡(luò)元嬰有時(shí)候很難保證下載時(shí)間,如果不使用多線程很可能用戶完成一個(gè)下載操作需要長(zhǎng)時(shí)間的等待,這個(gè)過程中無法進(jìn)行其他操作。下面演示一個(gè)多線程下載圖片的過程,在這個(gè)示例中點(diǎn)擊按鈕啟用一個(gè)線程去下載圖片,下載完成后使用UIImageView將圖片顯示到界面中??梢钥吹接脩酎c(diǎn)擊完成下載按鈕后,不管圖片是否下載完成都可以繼續(xù)操作界面,不會(huì)造成阻塞。
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
_imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
_imageView.contentMode=UIViewContentModeScaleAspectFit;
[self.view addSubview:_imageView];
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
#pragma mark 將圖片顯示到界面
-(void)updateImage:(NSData *)imageData{
UIImage *image=[UIImage imageWithData:imageData];
_imageView.image=image;
}
#pragma mark 請(qǐng)求圖片數(shù)據(jù)
-(NSData *)requestData{
NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark 加載圖片
-(void)loadImage{
//請(qǐng)求數(shù)據(jù)
NSData *data= [self requestData];
/*將數(shù)據(jù)顯示到UI控件,注意只能在主線程中更新UI,
另外performSelectorOnMainThread方法是NSObject的分類方法,每個(gè)NSObject對(duì)象都有此方法,
它調(diào)用的selector方法是當(dāng)前調(diào)用控件的方法,例如使用UIImageView調(diào)用的時(shí)候selector就是UIImageView的方法
Object:代表調(diào)用方法的參數(shù),不過只能傳遞一個(gè)參數(shù)(如果有多個(gè)參數(shù)請(qǐng)使用對(duì)象進(jìn)行封裝)
waitUntilDone:是否線程任務(wù)完成執(zhí)行
*/
[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
//方法1:使用對(duì)象方法
//創(chuàng)建一個(gè)線程,第一個(gè)參數(shù)是請(qǐng)求的操作,第二個(gè)參數(shù)是操作方法的參數(shù)
// NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
// //啟動(dòng)一個(gè)線程,注意啟動(dòng)一個(gè)線程并非就一定立即執(zhí)行,而是處于就緒狀態(tài),當(dāng)系統(tǒng)調(diào)度時(shí)才真正執(zhí)行
// [thread start];
//方法2:使用類方法
[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
程序比較簡(jiǎn)單,但是需要注意執(zhí)行步驟:當(dāng)點(diǎn)擊了“加載圖片”按鈕后啟動(dòng)一個(gè)新的線程,這個(gè)線程在演示中大概用了5s左右,在這5s內(nèi)UI線程是不會(huì)阻塞的,用戶可以進(jìn)行其他操作,大約5s之后圖片下載完成,此時(shí)調(diào)用UI線程將圖片顯示到界面中(這個(gè)過程瞬間完成)。另外前面也提到過,更新UI的時(shí)候使用UI線程,這里調(diào)用了NSObject的分類擴(kuò)展方法,調(diào)用UI線程完成更新。
多個(gè)線程并發(fā)
上面這個(gè)演示并沒有演示多個(gè)子線程操作之間的關(guān)系,現(xiàn)在不妨在界面中多加載幾張圖片,每個(gè)圖片都來自遠(yuǎn)程請(qǐng)求。
大家應(yīng)該注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument、- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法還是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能傳一個(gè)參數(shù),由于更新圖片需要傳遞UIImageView的索引和圖片數(shù)據(jù),因此這里不妨定義一個(gè)類保存圖片索引和圖片數(shù)據(jù)以供后面使用。
#import <Foundation/Foundation.h>
@interface KCImageData : NSObject
#pragma mark 索引
@property (nonatomic,assign) int index;
#pragma mark 圖片數(shù)據(jù)
@property (nonatomic,strong) NSData *data;
@end
接下來將創(chuàng)建多個(gè)UIImageView并創(chuàng)建多個(gè)線程用于往UIImageView中填充圖片。
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
@interface KCMainViewController (){
NSMutableArray *_imageViews;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個(gè)圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
// imageView.backgroundColor=[UIColor redColor];
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
#pragma mark 將圖片顯示到界面
-(void)updateImage:(KCImageData *)imageData{
UIImage *image=[UIImage imageWithData:imageData.data];
UIImageView *imageView= _imageViews[imageData.index];
imageView.image=image;
}
#pragma mark 請(qǐng)求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
// NSLog(@"%i",i);
//currentThread方法可以取得當(dāng)前操作線程
NSLog(@"current thread:%@",[NSThread currentThread]);
int i=[index integerValue];
// NSLog(@"%i",i);//未必按順序輸出
NSData *data= [self requestData:i];
KCImageData *imageData=[[KCImageData alloc]init];
imageData.index=i;
imageData.data=data;
[self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
//創(chuàng)建多個(gè)線程用于填充圖片
for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) {
// [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
thread.name=[NSString stringWithFormat:@"myThread%i",i];//設(shè)置線程名稱
[thread start];
}
}
@end
通過NSThread的currentThread可以取得當(dāng)前操作的線程,其中會(huì)記錄線程名稱name和編號(hào)number,需要注意主線程編號(hào)永遠(yuǎn)為1。多個(gè)線程雖然按順序啟動(dòng),但是實(shí)際執(zhí)行未必按照順序加載照片(loadImage:方法未必依次創(chuàng)建,可以通過在loadImage:中打印索引查看),因?yàn)榫€程啟動(dòng)后僅僅處于就緒狀態(tài),實(shí)際是否執(zhí)行要由CPU根據(jù)當(dāng)前狀態(tài)調(diào)度。
從上面的運(yùn)行效果大家不難發(fā)現(xiàn),圖片并未按順序加載,原因有兩個(gè):第一,每個(gè)線程的實(shí)際執(zhí)行順序并不一定按順序執(zhí)行(雖然是按順序啟動(dòng));第二,每個(gè)線程執(zhí)行時(shí)實(shí)際網(wǎng)絡(luò)狀況很可能不一致。當(dāng)然網(wǎng)絡(luò)問題無法改變,只能盡可能讓網(wǎng)速更快,但是可以改變線程的優(yōu)先級(jí),讓15個(gè)線程優(yōu)先執(zhí)行某個(gè)線程。線程優(yōu)先級(jí)范圍為0~1,值越大優(yōu)先級(jí)越高,每個(gè)線程的優(yōu)先級(jí)默認(rèn)為0.5。
線程狀態(tài)
在線程操作過程中可以讓某個(gè)線程休眠等待,優(yōu)先執(zhí)行其他線程操作,而且在這個(gè)過程中還可以修改某個(gè)線程的狀態(tài)或者終止某個(gè)指定線程。為了解決上面優(yōu)先加載最后一張圖片的問題,不妨讓其他線程先休眠一會(huì)等待最后一個(gè)線程執(zhí)行。修改圖片加載方法如下即可:
-(NSData *)requestData:(int )index{
//對(duì)非最后一張圖片加載線程休眠2秒
if (index!=(ROW_COUNT*COLUMN_COUNT-1)) {
[NSThread sleepForTimeInterval:2.0];
}
NSURL *url=[NSURL URLWithString:_imageNames[index]];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
在這里讓其他線程休眠2秒,此時(shí)你就會(huì)看到最后一張圖片總是第一個(gè)加載(除非網(wǎng)速特別差)。
線程狀態(tài)分為isExecuting(正在執(zhí)行)、isFinished(已經(jīng)完成)、isCancellled(已經(jīng)取消)三種。其中取消狀態(tài)程序可以干預(yù)設(shè)置,只要調(diào)用線程的cancel方法即可。但是需要注意在主線程中僅僅能設(shè)置線程狀態(tài),并不能真正停止當(dāng)前線程,如果要終止線程必須在線程中調(diào)用exist方法,這是一個(gè)靜態(tài)方法,調(diào)用該方法可以退出當(dāng)前線程。
NSOperation
使用NSOperation和NSOperationQueue進(jìn)行多線程開發(fā)類似于C#中的線程池,只要將一個(gè)NSOperation(實(shí)際開中需要使用其子類NSInvocationOperation、NSBlockOperation)放到NSOperationQueue這個(gè)隊(duì)列中線程就會(huì)依次啟動(dòng)。NSOperationQueue負(fù)責(zé)管理、執(zhí)行所有的NSOperation,在這個(gè)過程中可以更加容易的管理線程總數(shù)和控制線程之間的依賴關(guān)系。
NSOperation有兩個(gè)常用子類用于創(chuàng)建線程操作:NSInvocationOperation和NSBlockOperation,兩種方式本質(zhì)沒有區(qū)別,但是是后者使用Block形式進(jìn)行代碼組織,使用相對(duì)方便。
NSInvocationOperation
首先使用NSInvocationOperation進(jìn)行一張圖片的加載演示,整個(gè)過程就是:創(chuàng)建一個(gè)操作,在這個(gè)操作中指定調(diào)用方法和參數(shù),然后加入到操作隊(duì)列。其他代碼基本不用修改,直接修加載圖片方法如下:
-(void)loadImageWithMultiThread{
/*創(chuàng)建一個(gè)調(diào)用操作
object:調(diào)用方法參數(shù)
*/
NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
//創(chuàng)建完NSInvocationOperation對(duì)象并不會(huì)調(diào)用,它由一個(gè)start方法啟動(dòng)操作,但是注意如果直接調(diào)用start方法,則此操作會(huì)在主線程中調(diào)用,一般不會(huì)這么操作,而是添加到NSOperationQueue中
// [invocationOperation start];
//創(chuàng)建操作隊(duì)列
NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
//注意添加到操作隊(duì)后,隊(duì)列會(huì)開啟一個(gè)線程執(zhí)行此操作
[operationQueue addOperation:invocationOperation];
}
NSBlockOperation
下面采用NSBlockOperation創(chuàng)建多個(gè)線程加載圖片。
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
@interface KCMainViewController (){
NSMutableArray *_imageViews;
NSMutableArray *_imageNames;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個(gè)圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
// imageView.backgroundColor=[UIColor redColor];
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
//創(chuàng)建圖片鏈接
_imageNames=[NSMutableArray array];
for (int i=0; i<IMAGE_COUNT; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
}
}
#pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
UIImage *image=[UIImage imageWithData:data];
UIImageView *imageView= _imageViews[index];
imageView.image=image;
}
#pragma mark 請(qǐng)求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSURL *url=[NSURL URLWithString:_imageNames[index]];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
int i=[index integerValue];
//請(qǐng)求數(shù)據(jù)
NSData *data= [self requestData:i];
NSLog(@"%@",[NSThread currentThread]);
//更新UI界面,此處調(diào)用了主線程隊(duì)列的方法(mainQueue是UI主線程)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self updateImageWithData:data andIndex:i];
}];
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
int count=ROW_COUNT*COLUMN_COUNT;
//創(chuàng)建操作隊(duì)列
NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
operationQueue.maxConcurrentOperationCount=5;//設(shè)置最大并發(fā)線程數(shù)
//創(chuàng)建多個(gè)線程用于填充圖片
for (int i=0; i<count; ++i) {
//方法1:創(chuàng)建操作塊添加到隊(duì)列
// //創(chuàng)建多線程操作
// NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
// [self loadImage:[NSNumber numberWithInt:i]];
// }];
// //創(chuàng)建操作隊(duì)列
//
// [operationQueue addOperation:blockOperation];
//方法2:直接使用操隊(duì)列添加操作
[operationQueue addOperationWithBlock:^{
[self loadImage:[NSNumber numberWithInt:i]];
}];
}
}
@end
對(duì)比之前NSThread加載張圖片很發(fā)現(xiàn)核心代碼簡(jiǎn)化了不少,這里著重強(qiáng)調(diào)兩點(diǎn):使用NSBlockOperation方法,所有的操作不必單獨(dú)定義方法,同時(shí)解決了只能傳遞一個(gè)參數(shù)的問題。
調(diào)用主線程隊(duì)列的addOperationWithBlock:方法進(jìn)行UI更新,不用再定義一個(gè)參數(shù)實(shí)體(之前必須定義一個(gè)KCImageData解決只能傳遞一個(gè)參數(shù)的問題)。
使用NSOperation進(jìn)行多線程開發(fā)可以設(shè)置最大并發(fā)線程,有效的對(duì)線程進(jìn)行了控制(上面的代碼運(yùn)行起來你會(huì)發(fā)現(xiàn)打印當(dāng)前進(jìn)程時(shí)只有有限的線程被創(chuàng)建,如上面的代碼設(shè)置最大線程數(shù)為5,則圖片基本上是五個(gè)一次加載的)。
線程執(zhí)行順序
前面使用NSThread很難控制線程的執(zhí)行順序,但是使用NSOperation就容易多了,每個(gè)NSOperation可以設(shè)置依賴線程。假設(shè)操作A依賴于操作B,線程操作隊(duì)列在啟動(dòng)線程時(shí)就會(huì)首先執(zhí)行B操作,然后執(zhí)行A。對(duì)于前面優(yōu)先加載最后一張圖的需求,只要設(shè)置前面的線程操作的依賴線程為最后一個(gè)操作即可。
//創(chuàng)建完NSInvocationOperation并不會(huì)立即調(diào)用,需要調(diào)用start,如果直接調(diào)用start方法,則此操作會(huì)在主線程中調(diào)用,一般不會(huì)這么操作,而是添加到NSOperationQueue中
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(click:) object:@"456"];
//注意添加到操作隊(duì)后,隊(duì)列會(huì)開啟一個(gè)線程執(zhí)行此操作
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
operationQueue.maxConcurrentOperationCount = 5;
for (NSInteger i = 0; i < 10; i++) {
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
[self click:[NSString stringWithFormat:@"%ld", i]];
}];
[blockOperation addDependency:invocationOperation];
[operationQueue addOperation:blockOperation];
}
[operationQueue addOperation:invocationOperation];
[operationQueue addOperationWithBlock:^{
[self click:@"123"];
}];
- (void)click:(NSString *)a {
NSLog(@"main: %@", a);
}
GCD
GCD(Grand Central Dispatch)是基于C語(yǔ)言開發(fā)的一套多線程開發(fā)機(jī)制,也是目前蘋果官方推薦的多線程開發(fā)方法。前面也說過三種開發(fā)中GCD抽象層次最高,當(dāng)然是用起來也最簡(jiǎn)單,只是它基于C語(yǔ)言開發(fā),并不像NSOperation是面向?qū)ο蟮拈_發(fā),而是完全面向過程的。對(duì)于熟悉C#異步調(diào)用的朋友對(duì)于GCD學(xué)習(xí)起來應(yīng)該很快,因?yàn)樗cC#中的異步調(diào)用基本是一樣的。這種機(jī)制相比較于前面兩種多線程開發(fā)方式最顯著的優(yōu)點(diǎn)就是它對(duì)于多核運(yùn)算更加有效。
GCD中也有一個(gè)類似于NSOperationQueue的隊(duì)列,GCD統(tǒng)一管理整個(gè)隊(duì)列中的任務(wù)。但是GCD中的隊(duì)列分為并行隊(duì)列和串行隊(duì)列兩類:
串行隊(duì)列:只有一個(gè)線程,加入到隊(duì)列中的操作按添加順序依次執(zhí)行。
并發(fā)隊(duì)列:有多個(gè)線程,操作進(jìn)來之后它會(huì)將這些隊(duì)列安排在可用的處理器上,同時(shí)保證先進(jìn)來的任務(wù)優(yōu)先處理。
其實(shí)在GCD中還有一個(gè)特殊隊(duì)列就是主隊(duì)列,用來執(zhí)行主線程上的操作任務(wù)(從前面的演示中可以看到其實(shí)在NSOperation中也有一個(gè)主隊(duì)列)
串行隊(duì)列
使用串行隊(duì)列時(shí)首先要?jiǎng)?chuàng)建一個(gè)串行隊(duì)列,然后調(diào)用異步調(diào)用方法,在此方法中傳入串行隊(duì)列和線程操作即可自動(dòng)執(zhí)行。
dispatch_queue_t serialQueue = dispatch_queue_create(0, DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 10; i ++ ) {
dispatch_async(serialQueue, ^{
[self click:[NSString stringWithFormat:@"%ld", i]];
});
}
//非ARC情況下
//dispatch_release(serialQueue);
并發(fā)隊(duì)列
并發(fā)隊(duì)列同樣是使用dispatch_queue_create()方法創(chuàng)建,只是最后一個(gè)參數(shù)指定為DISPATCH_QUEUE_CONCURRENT進(jìn)行創(chuàng)建,但是在實(shí)際開發(fā)中我們通常不會(huì)重新創(chuàng)建一個(gè)并發(fā)隊(duì)列而是使用dispatch_get_global_queue()方法取得一個(gè)全局的并發(fā)隊(duì)列(當(dāng)然如果有多個(gè)并發(fā)隊(duì)列可以使用前者創(chuàng)建)。
/*取得全局隊(duì)列
第一個(gè)參數(shù):線程優(yōu)先級(jí)
第二個(gè)參數(shù):標(biāo)記參數(shù),目前沒有用,一般傳入0
*/
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 10; i ++ ) {
dispatch_async(concurrentQueue, ^{
[self click:[NSString stringWithFormat:@"%ld", i]];
});
}
或
dispatch_queue_t concurrentQueue = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 10; i ++ ) {
dispatch_async(concurrentQueue, ^{
[self click:[NSString stringWithFormat:@"%ld", i]];
});
}
tips:
1、在GDC中一個(gè)操作是多線程執(zhí)行還是單線程執(zhí)行取決于當(dāng)前隊(duì)列類型和執(zhí)行方法,
只有隊(duì)列類型為并行隊(duì)列并且使用異步方法執(zhí)行時(shí)才能在多個(gè)線程中執(zhí)行。
2、串行隊(duì)列可以按順序執(zhí)行,并行隊(duì)列的異步方法無法確定執(zhí)行順序。
3、UI界面的更新最好采用同步方法,其他操作采用異步方法。
其他任務(wù)執(zhí)行方法
GCD執(zhí)行任務(wù)的方法并非只有簡(jiǎn)單的同步調(diào)用方法和異步調(diào)用方法,還有其他一些常用的方法:
1、dispatch_apply():重復(fù)執(zhí)行某個(gè)任務(wù),但是注意這個(gè)方法沒有辦法異步執(zhí)行,為了不阻塞線程可以使用dispatch_async()包裝下在執(zhí)行:
dispatch_async(concurrentQueue, ^{
dispatch_apply(NSIntegerMax, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
NSLog(@"%zu", index);
});
});
2、dispatch_once():?jiǎn)未螆?zhí)行一個(gè)任務(wù),此方法中的任務(wù)只會(huì)執(zhí)行一次,重復(fù)調(diào)用也沒辦法重復(fù)執(zhí)行,單例模式中常用的方法
3、dispatch_time():延遲一定的時(shí)間后執(zhí)行。
4、dispatch_barrier_async():使用此方法創(chuàng)建的任務(wù)首先會(huì)查看隊(duì)列中有沒有其它要執(zhí)行的任務(wù),如果有,則會(huì)等待已有任務(wù)執(zhí)行完畢在執(zhí)行;同事在此方法后添加的任務(wù)必須等待此方法執(zhí)行完才能執(zhí)行。(利用這個(gè)方法可以控制執(zhí)行順序)
5、dispatch_group_async():實(shí)現(xiàn)對(duì)任務(wù)分組管理,如果一組任務(wù)全部完成后可以通過dispatch_group_notify()方法獲得完成通知(需要定義dispatch_group_t作為分組標(biāo)示)。
線程同步
說到線程同步就不得不提多線程中的鎖機(jī)制,多線程操作過程中往往多個(gè)線程是并發(fā)執(zhí)行的,同一個(gè)資源可能被多個(gè)線程同時(shí)訪問,造成資源搶奪,這個(gè)過程中如果沒有鎖機(jī)制往往會(huì)造成重大問題。
要解決資源搶奪的問題在iOS中有兩種常用的方法:一種是使用NSLock同步鎖,另一種是使用@synchronized代碼塊。兩種方法實(shí)現(xiàn)原理類似,只是在處理上大媽快使用起來更加方便(C#中也有類似的處理機(jī)制synchronized和lock)。
NSLock
iOS中對(duì)于資源搶占的問題可以使用同步鎖NSLock來解決,使用時(shí)把需要加鎖的代碼(以后暫時(shí)稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間,一個(gè)線程A進(jìn)入加鎖代碼之后由于已經(jīng)加鎖,另一個(gè)線程B就無法訪問,只有等待前一個(gè)線程A執(zhí)行完加鎖代碼后解鎖,B線程才能訪問加鎖代碼。需要注意的是lock和unlock之間的”加鎖代碼“應(yīng)該是搶占資源的讀取和修改代碼,不要將過多的其他操作代碼放到里面,否則一個(gè)線程執(zhí)行的時(shí)候另一個(gè)線程就一直在等待,就無法發(fā)揮多線程的作用了。
另外,在定義成成員變量應(yīng)該定義為“原子屬性”。對(duì)于被搶占資源來說將其定義為原子屬性是一個(gè)很好的習(xí)慣,因?yàn)橛袝r(shí)候很難保證同一個(gè)資源不在別處讀取和修改。nonatomic屬性讀取的是內(nèi)存數(shù)據(jù)(寄存器計(jì)算好的結(jié)果),而atomic就保證直接讀取寄存器的數(shù)據(jù),這樣一來就不會(huì)出現(xiàn)一個(gè)線程正在修改數(shù)據(jù),而另一個(gè)線程讀取了修改之前(存儲(chǔ)在內(nèi)存中)的數(shù)據(jù),永遠(yuǎn)保證同時(shí)只有一個(gè)線程在訪問一個(gè)屬性。
@synchronized代碼塊
使用@synchronized解決線程同步問題相比較NSLock要簡(jiǎn)單一些,日常開發(fā)中也更推薦使用此方法。首先選擇一個(gè)對(duì)象作為同步對(duì)象(一般使用self),然后將”加鎖代碼”(爭(zhēng)奪資源的讀取、修改代碼)放到代碼塊中。@synchronized中的代碼執(zhí)行時(shí)先檢查同步對(duì)象是否被另一個(gè)線程占用,如果占用該線程就會(huì)處于等待狀態(tài),直到同步對(duì)象被釋放。下面的代碼演示了如何使用@synchronized進(jìn)行線程同步:
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
//線程同步
@synchronized(self){
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[NSThread sleepForTimeInterval:0.001f];
[_imageNames removeObject:name];
}
}
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
擴(kuò)展--使用GCD解決資源搶占問題
在GCD中提供了一種信號(hào)機(jī)制,也可以解決資源搶占問題(和同步鎖的機(jī)制并不一樣)。GCD中信號(hào)量是dispatch_semaphore_t類型,支持信號(hào)通知和信號(hào)等待。每當(dāng)發(fā)送一個(gè)信號(hào)通知,則信號(hào)量+1;每當(dāng)發(fā)送一個(gè)等待信號(hào)時(shí)信號(hào)量-1,;如果信號(hào)量為0則信號(hào)會(huì)處于等待狀態(tài),直到信號(hào)量大于0開始執(zhí)行。根據(jù)這個(gè)原理我們可以初始化一個(gè)信號(hào)量變量,默認(rèn)信號(hào)量設(shè)置為1,每當(dāng)有線程進(jìn)入“加鎖代碼”之后就調(diào)用信號(hào)等待命令(此時(shí)信號(hào)量為0)開始等待,此時(shí)其他線程無法進(jìn)入,執(zhí)行完后發(fā)送信號(hào)通知(此時(shí)信號(hào)量為1),其他線程開始進(jìn)入執(zhí)行,如此一來就達(dá)到了線程同步目的。
擴(kuò)展--控制線程通信
由于線程的調(diào)度是透明的,程序有時(shí)候很難對(duì)它進(jìn)行有效的控制,為了解決這個(gè)問題iOS提供了NSCondition來控制線程通信(同前面GCD的信號(hào)機(jī)制類似)。NSCondition實(shí)現(xiàn)了NSLocking協(xié)議,所以它本身也有l(wèi)ock和unlock方法,因此也可以將它作為NSLock解決線程同步問題,此時(shí)使用方法跟NSLock沒有區(qū)別,只要在線程開始時(shí)加鎖,取得資源后釋放鎖即可,這部分內(nèi)容比較簡(jiǎn)單在此不再演示。當(dāng)然,單純解決線程同步問題不是NSCondition設(shè)計(jì)的主要目的,NSCondition更重要的是解決線程之間的調(diào)度關(guān)系(當(dāng)然,這個(gè)過程中也必須先加鎖、解鎖)。NSCondition可以調(diào)用wati方法控制某個(gè)線程處于等待狀態(tài),直到其他線程調(diào)用signal(此方法喚醒一個(gè)線程,如果有多個(gè)線程在等待則任意喚醒一個(gè))或者broadcast(此方法會(huì)喚醒所有等待線程)方法喚醒該線程才能繼續(xù)。
iOS中的其他鎖
在iOS開發(fā)中,除了同步鎖有時(shí)候還會(huì)用到一些其他鎖類型,在此簡(jiǎn)單介紹一下:
NSRecursiveLock :遞歸鎖,有時(shí)候“加鎖代碼”中存在遞歸調(diào)用,遞歸開始前加鎖,遞歸調(diào)用開始后會(huì)重復(fù)執(zhí)行此方法以至于反復(fù)執(zhí)行加鎖代碼最終造成死鎖,這個(gè)時(shí)候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個(gè)線程中反復(fù)獲取鎖而不造成死鎖,這個(gè)過程中會(huì)記錄獲取鎖和釋放鎖的次數(shù),只有最后兩者平衡鎖才被最終釋放。
NSDistributedLock:分布鎖,它本身是一個(gè)互斥鎖,基于文件方式實(shí)現(xiàn)鎖機(jī)制,可以跨進(jìn)程訪問。
pthread_mutex_t:同步鎖,基于C語(yǔ)言的同步鎖機(jī)制,使用方法與其他同步鎖機(jī)制類似。
提示:在開發(fā)過程中除非必須用鎖,否則應(yīng)該盡可能不使用鎖,因?yàn)槎嗑€程開發(fā)本身就是為了提高程序執(zhí)行順序,而同步鎖本身就只能一個(gè)進(jìn)程執(zhí)行,這樣不免降低執(zhí)行效率。
總結(jié)
1>無論使用哪種方法進(jìn)行多線程開發(fā),每個(gè)線程啟動(dòng)后并不一定立即執(zhí)行相應(yīng)的操作,具體什么時(shí)候由系統(tǒng)調(diào)度(CPU空閑時(shí)就會(huì)執(zhí)行)。
2>更新UI應(yīng)該在主線程(UI線程)中進(jìn)行,并且推薦使用同步調(diào)用,常用的方法如下:
- (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適合輕量級(jí)多線程開發(fā),控制線程順序比較難,同時(shí)線程總數(shù)無法控制(每次創(chuàng)建并不能重用之前的線程,只能創(chuàng)建一個(gè)新的線程)。
4>對(duì)于簡(jiǎn)單的多線程開發(fā)建議使用NSObject的擴(kuò)展方法完成,而不必使用NSThread。
5>可以使用NSThread的currentThread方法取得當(dāng)前線程,使用 sleepForTimeInterval:方法讓當(dāng)前線程休眠。
6>NSOperation進(jìn)行多線程開發(fā)可以控制線程總數(shù)及線程依賴關(guān)系。
7>創(chuàng)建一個(gè)NSOperation不應(yīng)該直接調(diào)用start方法(如果直接start則會(huì)在主線程中調(diào)用)而是應(yīng)該放到NSOperationQueue中啟動(dòng)。
8>相比NSInvocationOperation推薦使用NSBlockOperation,代碼簡(jiǎn)單,同時(shí)由于閉包性使它沒有傳參問題。
9>NSOperation是對(duì)GCD面向?qū)ο蟮腛bjC封裝,但是相比GCD基于C語(yǔ)言開發(fā),效率卻更高,建議如果任務(wù)之間有依賴關(guān)系或者想要監(jiān)聽任務(wù)完成狀態(tài)的情況下優(yōu)先選擇NSOperation否則使用GCD。
10>在GCD中串行隊(duì)列中的任務(wù)被安排到一個(gè)單一線程執(zhí)行(不是主線程),可以方便地控制執(zhí)行順序;并發(fā)隊(duì)列在多個(gè)線程中執(zhí)行(前提是使用異步方法),順序控制相對(duì)復(fù)雜,但是更高效。
11>在GDC中一個(gè)操作是多線程執(zhí)行還是單線程執(zhí)行取決于當(dāng)前隊(duì)列類型和執(zhí)行方法,只有隊(duì)列類型為并行隊(duì)列并且使用異步方法執(zhí)行時(shí)才能在多個(gè)線程中執(zhí)行(如果是并行隊(duì)列使用同步方法調(diào)用則會(huì)在主線程中執(zhí)行)。
12>相比使用NSLock,@synchronized更加簡(jiǎn)單,推薦使用后者。