概覽
大家都知道,在開發過程中應該盡可能減少用戶等待時間,讓程序盡可能快的完成運算。可是無論是哪種語言開發的程序最終往往轉換成匯編語言進而解釋成機器碼來執行。但是機器碼是按順序執行的,一個復雜的多步操作只能一步步按順序逐個執行。改變這種狀況可以從兩個角度出發:對于單核處理器,可以將多個步驟放到不同的線程,這樣一來用戶完成UI操作后其他后續任務在其他線程中,當CPU空閑時會繼續執行,而此時對于用戶而言可以繼續進行其他操作;對于多核處理器,如果用戶在UI線程中完成某個操作之后,其他后續操作在別的線程中繼續執行,用戶同樣可以繼續進行其他UI操作,與此同時前一個操作的后續任務可以分散到多個空閑CPU中繼續執行(當然具體調度順序要根據程序設計而定),及解決了線程阻塞又提高了運行效率。蘋果從iPad2 開始使用雙核A5處理器(iPhone中從iPhone 4S開始使用),A7中還加入了協處理器,如何充分發揮這些處理器的性能確實值得思考。今天將重點分析iOS多線程開發:
簡介
iOS多線程
解決線程阻塞問題
多線程并發
線程狀態
擴展-NSObject分類擴展
NSInvocationOperation
NSBlockOperation
線程執行順序
串行隊列
并發隊列
其他任務執行方法
NSLock同步鎖
@synchronized代碼塊
擴展--使用GCD解決資源搶占問題
擴展--控制線程通信
目錄
當用戶播放音頻、下載資源、進行圖像處理時往往希望做這些事情的時候其他操作不會被中斷或者希望這些操作過程中更加順暢。在單線程中一個線程只能做一件事情,一件事情處理不完另一件事就不能開始,這樣勢必影響用戶體驗。早在單核處理器時期就有多線程,這個時候多線程更多的用于解決線程阻塞造成的用戶等待(通常是操作完UI后用戶不再干涉,其他線程在等待隊列中,CPU一旦空閑就繼續執行,不影響用戶其他UI操作),其處理能力并沒有明顯的變化。如今無論是移動操作系統還是PC、服務器都是多核處理器,于是“并行運算”就更多的被提及。一件事情我們可以分成多個步驟,在沒有順序要求的情況下使用多線程既能解決線程阻塞又能充分利用多核處理器運行能力。
下圖反映了一個包含8個操作的任務在一個有兩核心的CPU中創建四個線程運行的情況。假設每個核心有兩個線程,那么每個CPU中兩個線程會交替執行,兩個CPU之間的操作會并行運算。單就一個CPU而言兩個線程可以解決線程阻塞造成的不流暢問題,其本身運行效率并沒有提高,多CPU的并行運算才真正解決了運行效率問題,這也正是并發和并行的區別。當然,不管是多核還是單核開發人員不用過多的擔心,因為任務具體分配給幾個CPU運算是由系統調度的,開發人員不用過多關心系統有幾個CPU。開發人員需要關心的是線程之間的依賴關系,因為有些操作必須在某個操作完成完才能執行,如果不能保證這個順序勢必會造成程序問題。
在iOS中每個進程啟動后都會建立一個主線程(UI線程),這個線程是其他線程的父線程。由于在iOS中除了主線程,其他子線程是獨立于Cocoa Touch的,所以只有主線程可以更新UI界面(新版iOS中,使用其他線程更新UI可能也能成功,但是不推薦)。iOS中多線程使用并不復雜,關鍵是如何控制好各個線程的執行順序、處理好資源競爭問題。常用的多線程開發有三種方式:
1.NSThread
2.NSOperation
3.GCD
三種方式是隨著iOS的發展逐漸引入的,所以相比而言后者比前者更加簡單易用,并且GCD也是目前蘋果官方比較推薦的方式(它充分利用了多核處理器的運算性能)。做過.Net開發的朋友不難發現其實這三種開發方式 剛好對應.Net中的多線程、線程池和異步調用,因此在文章中也會對比講解。
NSThread是輕量級的多線程開發,使用起來也并不復雜,但是使用NSThread需要自己管理線程生命周期。可以使用對象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接將操作添加到線程中并啟動,也可以使用對象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument創建一個線程對象,然后調用start方法啟動線程。
在資源下載過程中,由于網絡原因有時候很難保證下載時間,如果不使用多線程可能用戶完成一個下載操作需要長時間的等待,這個過程中無法進行其他操作。下面演示一個采用多線程下載圖片的過程,在這個示例中點擊按鈕會啟動一個線程去下載圖片,下載完成后使用UIImageView將圖片顯示到界面中。可以看到用戶點擊完下載按鈕后,不管圖片是否下載完成都可以繼續操作界面,不會造成阻塞。
//
// ?NSThread實現多線程
// ?MultiThread
//
// ?Created by Kenshin Cui on 14-3-22.
// ?Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//#import"KCMainViewController.h"@interfaceKCMainViewController (){
UIImageView *_imageView;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}#pragmamark 界面布局
-(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];
}#pragmamark 將圖片顯示到界面
-(void)updateImage:(NSData *)imageData{
UIImage *image=[UIImage imageWithData:imageData];
_imageView.image=image;
}#pragmamark 請求圖片數據
-(NSData *)requestData{//對于多線程操作建議把線程操作放到@autoreleasepool中@autoreleasepool {
NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
NSData *data=[NSData dataWithContentsOfURL:url];returndata;
}
}#pragmamark 加載圖片
-(void)loadImage{//請求數據NSData *data= [self requestData];/*將數據顯示到UI控件,注意只能在主線程中更新UI,
另外performSelectorOnMainThread方法是NSObject的分類方法,每個NSObject對象都有此方法,
它調用的selector方法是當前調用控件的方法,例如使用UIImageView調用的時候selector就是UIImageView的方法
Object:代表調用方法的參數,不過只能傳遞一個參數(如果有多個參數請使用對象進行封裝)
waitUntilDone:是否線程任務完成執行
*/[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}#pragmamark 多線程下載圖片
-(void)loadImageWithMultiThread{//方法1:使用對象方法
//創建一個線程,第一個參數是請求的操作,第二個參數是操作方法的參數
// ? ?NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
// ? ?//啟動一個線程,注意啟動一個線程并非就一定立即執行,而是處于就緒狀態,當系統調度時才真正執行
// ? ?[thread start];
//方法2:使用類方法[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
@end
運行效果:
程序比較簡單,但是需要注意執行步驟:當點擊了“加載圖片”按鈕后啟動一個新的線程,這個線程在演示中大概用了5s左右,在這5s內UI線程是不會阻塞的,用戶可以進行其他操作,大約5s之后圖片下載完成,此時調用UI線程將圖片顯示到界面中(這個過程瞬間完成)。另外前面也提到過,更新UI的時候使用UI線程,這里調用了NSObject的分類擴展方法,調用UI線程完成更新。
上面這個演示并沒有演示多個子線程操作之間的關系,現在不妨在界面中多加載幾張圖片,每個圖片都來自遠程請求。
大家應該注意到不管是使用+ (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方法都只能傳一個參數,由于更新圖片需要傳遞UIImageView的索引和圖片數據,因此這里不妨定義一個類保存圖片索引和圖片數據以供后面使用。
KCImageData.h
//
// ?KCImageData.h
// ?MultiThread
//
// ?Created by Kenshin Cui on 14-3-22.
// ?Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//#import@interfaceKCImageData : NSObject#pragmamark 索引
@property(nonatomic,assign)intindex;#pragmamark 圖片數據
@property(nonatomic,strong) NSData *data;
@end
接下來將創建多個UIImageView并創建多個線程用于往UIImageView中填充圖片。
KCMainViewController.m
//
// ?NSThread實現多線程
// ?MultiThread
//
// ?Created by Kenshin Cui on 14-3-22.
// ?Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT 5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH ROW_HEIGHT#defineCELL_SPACING 10
@interfaceKCMainViewController (){
NSMutableArray *_imageViews;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}#pragmamark 界面布局
-(void)layoutUI{//創建多個圖片控件用于顯示圖片_imageViews=[NSMutableArrayarray];for(intr=0; r
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];
}#pragmamark 將圖片顯示到界面
-(void)updateImage:(KCImageData *)imageData{
UIImage *image=[UIImage imageWithData:imageData.data];
UIImageView *imageView= _imageViews[imageData.index];
imageView.image=image;
}#pragmamark 請求圖片數據
-(NSData *)requestData:(int)index{//對于多線程操作建議把線程操作放到@autoreleasepool中@autoreleasepool {
NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
NSData *data=[NSData dataWithContentsOfURL:url];returndata;
}
}#pragmamark 加載圖片
-(void)loadImage:(NSNumber *)index{// ? ?NSLog(@"%i",i);
//currentThread方法可以取得當前操作線程NSLog(@"current thread:%@",[NSThread currentThread]);inti=[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];
}#pragmamark 多線程下載圖片
-(void)loadImageWithMultiThread{//創建多個線程用于填充圖片for(inti=0; i
thread.name=[NSString stringWithFormat:@"myThread%i",i];//設置線程名稱[thread start];
}
}
@end
通過NSThread的currentThread可以取得當前操作的線程,其中會記錄線程名稱name和編號number,需要注意主線程編號永遠為1。多個線程雖然按順序啟動,但是實際執行未必按照順序加載照片(loadImage:方法未必依次創建,可以通過在loadImage:中打印索引查看),因為線程啟動后僅僅處于就緒狀態,實際是否執行要由CPU根據當前狀態調度。
從上面的運行效果大家不難發現,圖片并未按順序加載,原因有兩個:第一,每個線程的實際執行順序并不一定按順序執行(雖然是按順序啟動);第二,每個線程執行時實際網絡狀況很可能不一致。當然網絡問題無法改變,只能盡可能讓網速更快,但是可以改變線程的優先級,讓15個線程優先執行某個線程。線程優先級范圍為0~1,值越大優先級越高,每個線程的優先級默認為0.5。修改圖片下載方法如下,改變最后一張圖片加載的優先級,這樣可以提高它被優先加載的幾率,但是它也未必就第一個加載。因為首先其他線程是先啟動的,其次網絡狀況我們沒辦法修改:
-(void)loadImageWithMultiThread{
NSMutableArray *threads=[NSMutableArrayarray];intcount=ROW_COUNT*COLUMN_COUNT;//創建多個線程用于填充圖片for(inti=0; i
thread.name=[NSString stringWithFormat:@"myThread%i",i];//設置線程名稱if(i==(count-1)){
thread.threadPriority=1.0;
}else{
thread.threadPriority=0.0;
}
[threads addObject:thread];
}for(inti=0; i
NSThread *thread=threads[i];
[thread start];
}
}
在線程操作過程中可以讓某個線程休眠等待,優先執行其他線程操作,而且在這個過程中還可以修改某個線程的狀態或者終止某個指定線程。為了解決上面優先加載最后一張圖片的問題,不妨讓其他線程先休眠一會等待最后一個線程執行。修改圖片加載方法如下即可:
-(NSData *)requestData:(int)index{//對于多線程操作建議把線程操作放到@autoreleasepool中@autoreleasepool {//對非最后一張圖片加載線程休眠2秒if(index!=(ROW_COUNT*COLUMN_COUNT-1)) {
[NSThread sleepForTimeInterval:2.0];
}
NSURL *url=[NSURL URLWithString:_imageNames[index]];
NSData *data=[NSData dataWithContentsOfURL:url];returndata;
}
}
在這里讓其他線程休眠2秒,此時你就會看到最后一張圖片總是第一個加載(除非網速特別差)。
線程狀態分為isExecuting(正在執行)、isFinished(已經完成)、isCancellled(已經取消)三種。其中取消狀態程序可以干預設置,只要調用線程的cancel方法即可。但是需要注意在主線程中僅僅能設置線程狀態,并不能真正停止當前線程,如果要終止線程必須在線程中調用exist方法,這是一個靜態方法,調用該方法可以退出當前線程。
假設在圖片加載過程中點擊停止按鈕讓沒有完成的線程停止加載,可以改造程序如下:
//
// ?NSThread實現多線程
// ?MultiThread
//
// ?Created by Kenshin Cui on 14-3-22.
// ?Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT 5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH ROW_HEIGHT#defineCELL_SPACING 10
@interfaceKCMainViewController (){
NSMutableArray *_imageViews;
NSMutableArray *_imageNames;
NSMutableArray *_threads;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}#pragmamark 界面布局
-(void)layoutUI{//創建多個圖片空間用于顯示圖片_imageViews=[NSMutableArrayarray];for(intr=0; r
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 *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect];
buttonStart.frame=CGRectMake(50, 500, 100, 25);
[buttonStart setTitle:@"加載圖片"forState:UIControlStateNormal];
[buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:buttonStart];//停止按鈕UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect];
buttonStop.frame=CGRectMake(160, 500, 100, 25);
[buttonStop setTitle:@"停止加載"forState:UIControlStateNormal];
[buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:buttonStop];//創建圖片鏈接_imageNames=[NSMutableArrayarray];
[_imageNames addObject:@for(inti=0; i
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
}}#pragmamark 將圖片顯示到界面
-(void)updateImage:(KCImageData *)imageData{
UIImage *image=[UIImage imageWithData:imageData.data];
UIImageView *imageView= _imageViews[imageData.index];
imageView.image=image;
}#pragmamark 請求圖片數據
-(NSData *)requestData:(int)index{//對于多線程操作建議把線程操作放到@autoreleasepool中@autoreleasepool {
NSURL *url=[NSURL URLWithString:_imageNames[index]];
NSData *data=[NSData dataWithContentsOfURL:url];returndata;
}
}#pragmamark 加載圖片
-(void)loadImage:(NSNumber *)index{inti=[index integerValue];
NSData *data= [self requestData:i];
NSThread *currentThread=[NSThread currentThread];// ? ?如果當前線程處于取消狀態,則退出當前線程if(currentThread.isCancelled) {
NSLog(@"thread(%@) will be cancelled!",currentThread);
[NSThread exit];//取消當前線程}
KCImageData *imageData=[[KCImageData alloc]init];
imageData.index=i;
imageData.data=data;
[self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}#pragmamark 多線程下載圖片
-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;
_threads=[NSMutableArray arrayWithCapacity:count];//創建多個線程用于填充圖片for(inti=0; i
NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
thread.name=[NSString stringWithFormat:@"myThread%i",i];//設置線程名稱[_threads addObject:thread];
}//循環啟動線程for(inti=0; i
NSThread *thread= _threads[i];
[thread start];
}
}#pragmamark 停止加載圖片
-(void)stopLoadImage{for(inti=0; i
NSThread *thread= _threads[i];//判斷線程是否完成,如果沒有完成則設置為取消狀態
//注意設置為取消狀態僅僅是改變了線程狀態而言,并不能終止線程if(!thread.isFinished) {
[thread cancel];
}
}
}
@end
運行效果(點擊加載大概1秒后點擊停止加載):
使用NSThread在進行多線程開發過程中操作比較簡單,但是要控制線程執行順序并不容易(前面萬不得已采用了休眠的方法),另外在這個過程中如果打印線程會發現循環幾次就創建了幾個線程,這在實際開發過程中是不得不考慮的問題,因為每個線程的創建也是相當占用系統開銷的。
為了簡化多線程開發過程,蘋果官方對NSObject進行分類擴展(本質還是創建NSThread),對于簡單的多線程操作可以直接使用這些擴展方法。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在后臺執行一個操作,本質就是重新創建一個線程執行當前方法。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的線程上執行一個方法,需要用戶創建一個線程對象。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主線程上執行一個方法(前面已經使用過)。
例如前面加載圖多個圖片的方法,可以改為后臺線程執行:
-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;for(inti=0; i
[self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]];
}
}