級別: ★★☆☆☆
標籤:「iOS」「多執行緒」「NSThread」
作者: dac_1033
審校: QiShare團隊
iOS多執行緒系列計劃3篇,分別為:NSThread、NSOperation、GCD。
本篇是系列文章第一篇。
1. 執行緒的概念
首先簡單敘述一下這兩個概念,我們在電腦上單獨執行的每個程式就是一個獨立的程式,通常程式之間是相互獨立存在的,程式是系統分配資源的最小單元。程式中的最小執行單位就是執行緒,並且一個程式中至少有一個執行緒,程式中的所有執行緒共用這個程式的資源,執行緒也是系統進行排程的最小單元。
1.1 多執行緒在多核CPU中處理任務
1.2 執行緒狀態的切換
1.3 執行緒安全問題
執行緒安全問題是在多個執行緒處理任務的情況下產生。例如多個執行緒在同事執行下面的任務時,如果多個執行緒可以同時執行這段程式碼(任務),當多個執行緒同時在getTicket方法中執行count--時,count的結果就會出現不準確的情況,這個處理過程就不是執行緒安全的。
NSInteger ticketCount = 100;
- (void)getTicket() {
count--;
NSLog(@"剩餘票數 = %d", ticketCount);
}
複製程式碼
1.4 iOS 中的多執行緒
在iOS中每個app啟動後都會建立一個主執行緒,也稱UI執行緒。由於除了主執行緒的其他子執行緒都是獨立於Cocoa Touch的,所以一般只使用主執行緒來更新UI介面。iOS中的多執行緒有三種方式: NSThread、NSOperation、GCD,其中GCD是目前蘋果比較推薦的方式。對於這篇文章,我們主要了解一下NSThread。
2. NSThread簡介
NSThread是輕量級的多執行緒開發,優點是我們可以直接例項化一個NSThread物件並直接操作這個執行緒物件,但是使用NSThread需要自己管理執行緒生命週期。iOS開發過程中,NSThread最常用到的方法就是 [NSThread currentThread]獲取當前執行緒,其他常用屬性及方法如下:
// 執行緒字典
@property (readonly, retain) NSMutableDictionary *threadDictionary;
// 執行緒名稱
@property (nullable, copy) NSString *name;
// 優先順序
@property double threadPriority ;
// 是否為主執行緒
@property (readonly) BOOL isMainThread
// 讀取執行緒狀態
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
// 直接將操作新增到執行緒中並啟動
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
// 建立一個執行緒物件
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
// 啟動
- (void)start;
// 撤銷
- (void)cancel;
// 退出
+ (void)exit;
// 休眠
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
複製程式碼
在NSObject(NSThreadPerformAdditions)類中的幾個常用方法,實現了在特定執行緒上執行任務的功能,該分類也定義在NSThread.h中:
// 在主執行緒上執行一個方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 在指定的執行緒上執行一個方法,需要使用者建立一個執行緒物件
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 在後臺執行一個操作,本質就是重新建立一個執行緒執行當前方法
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
複製程式碼
2.1 NSThread的使用過程
例如在app從網路下載圖片時,由於網路原因可能需要較長時間,這時如果只在主執行緒中進行下載,則這個過程中使用者將無法進行其他操作,直到網路圖片下載完成之前介面都處於卡死狀態(執行緒阻塞)。我們在主執行緒中另起一個新執行緒來單獨下載即可解決這一問題,不管資源是否下載完成都可以繼續操作介面,不會造成阻塞。示例程式碼如下:
@interface MultiThread_NSThread ()
// 顯示圖片
@property (nonatomic, strong) UIImageView *imgView;
@end
@implementation MultiThread_NSThread
- (void)viewDidLoad {
[super viewDidLoad];
[self setTitle:@"NSThread"];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self layoutViews];
}
- (void)layoutViews {
CGSize size = self.view.frame.size;
_imgView =[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, 300)];
[self.view addSubview:_imgView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(15, CGRectGetMaxY(_imgView.frame) + 30, size.width - 15 * 2, 45);
[button setTitle:@"點選載入" forState:UIControlStateNormal];
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
#pragma mark - 多執行緒下載圖片
- (void)loadImageWithMultiThread {
////1. 物件方法
//NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImage) object:nil];
//[thread start];
//2. 類方法
[NSThread detachNewThreadSelector:@selector(downloadImg) toTarget:self withObject:nil];
}
#pragma mark - 載入圖片
- (void)downloadImg {
// 請求資料
NSData *data = [self requestData];
// 回到主執行緒更新UI
[self performSelectorOnMainThread:@selector(updateImg:) withObject:data waitUntilDone:YES];
}
#pragma mark - 請求圖片資料
- (NSData *)requestData {
NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
NSData *data = [NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark - 將圖片顯示到介面
- (void)updateImg:(NSData *)imageData {
UIImage *image = [UIImage imageWithData:imageData];
_imgView.image = image;
}
@end
複製程式碼
在請求資料的程式碼上打一個斷點,可以看出NSThread是對pthread的封裝:
2.2 使用NSThread實現多執行緒併發
下面我們使用NSThread實現多執行緒載入多張網路圖片,來了解NSThread多執行緒處理任務的過程。示例程式碼如下:
@implementation NSThreadImage
@end
#define ColumnCount 4
#define RowCount 5
#define Margin 10
@interface MultiThread_NSThread1 ()
// imgView陣列
@property (nonatomic, strong) NSMutableArray *imgViewArr;
// thread陣列
@property (nonatomic, strong) NSMutableArray *threadArr;
@end
@implementation MultiThread_NSThread1
- (void)viewDidLoad {
[super viewDidLoad];
[self setTitle:@"NSThread1"];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self layoutViews];
}
- (void)layoutViews {
CGSize size = self.view.frame.size;
CGFloat imgWidth = (size.width - Margin * (ColumnCount + 1)) / ColumnCount;
_imgViewArr = [NSMutableArray array];
for (int row=0; row<RowCount; row++) {
for (int colomn=0; colomn<ColumnCount; colomn++) {
UIImageView *imageView=[[UIImageView alloc] initWithFrame:CGRectMake(Margin + colomn * (imgWidth + Margin), Margin + row * (imgWidth + Margin), imgWidth, imgWidth)];
imageView.backgroundColor=[UIColor cyanColor];
[self.view addSubview:imageView];
[_imgViewArr addObject:imageView];
}
}
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(15, (imgWidth + Margin) * RowCount + Margin, size.width - 15 * 2, 45);
[button addTarget:self action:@selector(loadImgWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"點選載入" forState:UIControlStateNormal];
[self.view addSubview:button];
}
#pragma mark - 多執行緒下載圖片
- (void)loadImgWithMultiThread {
_threadArr = [NSMutableArray array];
for (int i=0; i<RowCount*ColumnCount; ++i) {
NSThreadImage *threadImg = [[NSThreadImage alloc] init];
threadImg.index = i;
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(loadImg:) object:threadImg];
thread.name = [NSString stringWithFormat:@"myThread%i",i];
//// 優先順序
//thread.threadPriority = 1.0;
[thread start];
[_threadArr addObject:thread];
}
}
#pragma mark - 載入圖片
- (void)loadImg:(NSThreadImage *)threadImg {
//// 休眠
//[NSThread sleepForTimeInterval:2.0];
//// 撤銷(停止載入圖片)
//[[NSThread currentThread] cancel];
//// 退出當前執行緒
//[NSThread exit];
// 請求資料
threadImg.imgData = [self requestData];
// 回到主執行緒更新UI
[self performSelectorOnMainThread:@selector(updateImg:) withObject:threadImg waitUntilDone:YES];
// 列印當前執行緒
NSLog(@"current thread: %@", [NSThread currentThread]);
}
#pragma mark - 請求圖片資料
- (NSData *)requestData{
NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
NSData *data = [NSData dataWithContentsOfURL:url];
return data;
}
#pragma mark - 將圖片顯示到介面
- (void)updateImg:(NSThreadImage *)threadImg {
UIImage *image = [UIImage imageWithData:threadImg.imgData];
UIImageView *imageView = _imgViewArr[threadImg.index];
imageView.image = image;
}
//#pragma mark 停止載入網路圖片
//
//- (void)stopLoadingImgs {
//
// for (int i=0; i<RowCount*ColumnCount; ++i) {
//
// NSThread *thread = _threadArr[i];
// if (!thread.isFinished) {
// [thread cancel];
// }
// }
//}
@end
複製程式碼
3. 關於NSThread執行緒狀態的說明
NSThread型別的物件可以獲取到執行緒的三種狀態屬性isExecuting(正在執行)、isFinished(已經完成)、isCancellled(已經撤銷),其中撤銷狀態是可以在程式碼中呼叫執行緒的cancel方法手動設定的(在主執行緒中並不能真正停止當前執行緒)。isFinished屬性標誌著當前執行緒上的任務是否執行完成,cancel一個執行緒只是撤銷當前執行緒上任務的執行,監測到isFinished = YES或呼叫cancel方法都不能代表立即退出了這個執行緒,而呼叫類方法exit方法才可立即退出當前執行緒。
例如在載入多張網路圖片時,中途停止載入動作的執行:
#pragma mark 停止載入網路圖片
- (void)stopLoadingImgs {
for (int i=0; i<RowCount*ColumnCount; ++i) {
NSThread *thread = _threadArr[i];
if (!thread.isFinished) {
[thread cancel];
}
}
}
複製程式碼
PS:
- 更新UI需回到主執行緒中操作;
- 執行緒處於就緒狀態時會處於等待狀態,不一定立即執行;
- 區分執行緒三種狀態的不同,尤其是撤銷和退出兩種狀態的不同;
- 線上程死亡之後,再次點選螢幕嘗試重新開啟執行緒,則程式會掛;
- NSThread可以設定物件的優先順序thread.threadPriority,threadPriority取值範圍是0到1;
- NSThread並沒有提供設定執行緒間的依賴關係的方法,也就不能單純通過NSThread來設定任務處理的先後順序,但是我們可以通過設定NSThread的休眠或優先順序來儘量優化任務處理的先後順序;
- 在自己試驗的工程中,雖然NSThread例項的數量理論上不受限制,但是正常的處理過程中需要控制執行緒的數量。
小編微信:可加並拉入《QiShare技術交流群》。
關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)
推薦文章:
iOS 簽名機制
iOS 掃描二維碼/條形碼
iOS 瞭解Xcode Bitcode
iOS 重繪之drawRect
奇舞週刊