iOS 多執行緒之NSThread

QiShare發表於2019-01-07

級別: ★★☆☆☆
標籤:「iOS」「多執行緒」「NSThread」
作者: dac_1033
審校: QiShare團隊

iOS多執行緒系列計劃3篇,分別為:NSThread、NSOperation、GCD。
本篇是系列文章第一篇。

1. 執行緒的概念

首先簡單敘述一下這兩個概念,我們在電腦上單獨執行的每個程式就是一個獨立的程式,通常程式之間是相互獨立存在的,程式是系統分配資源的最小單元。程式中的最小執行單位就是執行緒,並且一個程式中至少有一個執行緒,程式中的所有執行緒共用這個程式的資源,執行緒也是系統進行排程的最小單元。

1.1 多執行緒在多核CPU中處理任務

多執行緒在多核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的封裝:

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:

  1. 更新UI需回到主執行緒中操作;
  2. 執行緒處於就緒狀態時會處於等待狀態,不一定立即執行;
  3. 區分執行緒三種狀態的不同,尤其是撤銷和退出兩種狀態的不同;
  4. 線上程死亡之後,再次點選螢幕嘗試重新開啟執行緒,則程式會掛;
  5. NSThread可以設定物件的優先順序thread.threadPriority,threadPriority取值範圍是0到1;
  6. NSThread並沒有提供設定執行緒間的依賴關係的方法,也就不能單純通過NSThread來設定任務處理的先後順序,但是我們可以通過設定NSThread的休眠或優先順序來儘量優化任務處理的先後順序;
  7. 在自己試驗的工程中,雖然NSThread例項的數量理論上不受限制,但是正常的處理過程中需要控制執行緒的數量。

工程原始碼GitHub地址


小編微信:可加並拉入《QiShare技術交流群》。

iOS 多執行緒之NSThread

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
iOS 簽名機制
iOS 掃描二維碼/條形碼
iOS 瞭解Xcode Bitcode
iOS 重繪之drawRect
奇舞週刊

相關文章