iOS多線程使用總結
一.概述與實現方案
1. 線程與進程
多線程在iOS中有著舉足輕重的地位,是每一位開發者都必備的技能,當然也是面試常考的技術點,本文主要是探究我們實際開發或者面試中遇到的多線程問題。比如什么是線程?它跟進程是什么關系,隊列跟線程什么關系,同步、異步、并發(并行)、串行
這些概念又怎么來理解,iOS有哪些常用多線程方案,以及線程同步技術有哪些等等。
線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。 --- 維基百科
這里又多了一個 進程
,那什么是進程呢,說白了就是是指在操作系統中正在運行的一個應用程序,如微信、支付寶app等都是一個進程。線程是就是進程的基本執行單元,一個進程的所有任務都在線程中執行。也就是說 一個進程最少要有一個線程,這個線程就是主線程。當然在我們實際使用過程中不可能只有一條主線程,我們為提高程序的執行效率,往往需要開辟多條子線程去執行一些耗時任務,這里就引出了多線程的概念。
多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程并發執行的技術
根據操作系統與硬件的不同分為兩類:軟件多線程
與硬件多線程
軟件多線程: 即便CPU只能運行一個線程,操作系統也可以通過快速的在不同線程之間進行切換,由于時間間隔很小,來給用戶造成一種多個線程同時運行的假象
硬件多線程: 如果CPU有多個核心,操作系統可以讓每個核心執行一條線程,從而具有真正的同時執行多個線程的能力,當然由于任務數量遠遠多于CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。
以上都是google出來的一大堆東西,比較抽象,沒關系我們來看下我們實際iOS開發中用到的多線程技術。
2.iOS中的多線程方案
iOS 中的多線程方案主要有四種 PThread
、NSThread
、GCD
、NSOperation
,PThread
是一套純粹C
語言的API,能適用于Unix\Linux\Windows等系統,線程生命周期需要程序員自己管理,使用難度較大,在我們的實際開發中幾乎用不到,在這里我們不做過多介紹,感興趣的直接去百度。我們著重介紹另外三中方案。
這里解釋一下線程的生命周期,所謂的線程的生命周期就是線程從創建到死亡的過程。一般會經歷:
新建 - 就緒 - 運行 - 阻塞 - 死亡
的過程。
- 新建:就是初始化線程對象
- 就緒:向線程對象發送start消息,線程對象被加入可調度線程池等待CPU調度。
- 運行:CPU 負責調度可調度線程池中線程的執行,線程執行完成之前,狀態可能會在就緒和運行之間來回切換。就緒和運行之間的狀態變化由CPU負責,程序員不能干預。
- 阻塞:當滿足某個預定條件時,可以使用休眠或鎖,阻塞線程執行
- 死亡:線程執行完畢,退出,銷毀。
(1) NSThread
NSThread是蘋果官方提供面向對象操作線程的技術,簡單方便,可以直接操作線程對象,不過需要自己控制線程的生命周期,我們看下蘋果官方給出的方法。
[1] 初始化方法
- 實例初始化方法
- (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
對應的初始化方法:
//創建線程
NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(demo:) object:@"Thread"];
NSThread *newThread = [[NSThread alloc]init];
NSThread *newThread = [[NSThread alloc]initWithBlock:^{
NSLog(@"Block");
}];
注意三種方法創建完成后都需要執行
[newThread start]
去啟動線程。
- 類初始化方法
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
注意這兩個類方法創建后就可執行,不需手動開啟
[2] 取消退出
既然有了創建,那就得有退出
// 實例方法 取消線程
- (void)cancel;
//類方法 退出
+ (void)exit;
[3] 線程執行狀態
// 線程正在執行
@property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 線程執行結束
@property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 線程是否可取消
@property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
[4] 線程間的通信方法
@interface NSObject (NSThreadPerformAdditions)
/*
* 去主線程執行指定方法
* aSelector: 方法
* arg: 參數
* wait:表示是否等待主線程做完事情后往下走,YES表示做完后執行下面事情,NO表示跟下面事情一起執行
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
/*
* 去指定線程執行指定方法
* aSelector: 方法
* arg: 參數
* wait:表示是否等待本線程做完事情后往下走,YES表示做完后執行下面事,NO表示跟下面事一起執行
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
/*
* 去開啟的子線程執行指定方法
* SEL: 方法
* arg: 參數
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
我們常說的線程間的通信所用的方法其實就是上面的這幾個方法,所有繼承NSObject實例化對象都可調用。當然還有其他方法也可以實現線程間的通信,如:GCD
、NSOperation
、NSMachPort
端口等形式,我們后面用到在做介紹。
舉個簡單的例子:我們在子線程中下載圖片,然后去主線程展示:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 子線程執行下載方法
[self performSelectorInBackground:@selector(download) withObject:nil];
}
- (void)download{
//圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"https://p3.ssl.qhimg.com/t011e94f0b9ed8e66b0.png"];
//下載圖片數據
NSData *data = [NSData dataWithContentsOfURL:url];
//生成圖片
UIImage *image = [UIImage imageWithData:data];
// 回主線程顯示圖片
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
}
- (void)showImage:(UIImage *)image{
self.imageView.image = image;
}
[5] 其他常用方法
-
+(void)currentThread
獲取當前線程 -
+(BOOL)isMultiThreaded
判斷當前是否運行在子線程 -
-(BOOL)isMainThread
判斷是否在主線程 -
+(void)sleepUntilDate:(NSDate *)date;+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
當前線程休眠時間
(3). GCD
在介紹GCD
前我們先來了解下多線程中比較容易混淆的幾個概念
[1]. 同步、異步、并發(并行)、串行
同步和異步主要影響:能不能開啟新的線程
同步:在當前線程中執行任務,不具備開啟新線程的能力
異步:在新的線程中執行任務,具備開啟新線程的能力并發和串行主要影響:任務的執行方式
并發:也叫并行,也叫并行隊列,多個任務并發(同時)執行
串行:也叫串行隊列,一個任務執行完畢后,再執行下一個任務
單純的介紹概念比較抽象,我們還是結合實際使用來說明:
[2] GCD 中的同步、異步方法
- 同步執行方法:
dispatch_sync()
- 異步執行方法:
dispatch_async()
使用方法:
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
可以看到這個兩個方法需要兩個參數,第一個參數需要傳入一個dispatch_queue_t
類型的隊列,第二個是執行的block。下面介紹一下GCD的隊列
[3] GCD 中的隊列
GCD中的隊列有三種:串行隊列、并行隊列、主隊列
,創建方式也非常簡單:
- 串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
第一個參數是隊列名稱,第二個是一個宏定義,常用的兩個宏 DISPATCH_QUEUE_SERIAL
和 DISPATCH_QUEUE_CONCURRENT
分別表示串行隊列和并行隊列,除此之外,宏DISPATCH_QUEUE_SERIAL_INACTIVE
和 DISPATCH_QUEUE_CONCURRENT_INACTIVE
分別表示初始化的串行隊列和并行隊列處于不可活動狀態。看下它的底層實現
dispatch_queue_attr_t
dispatch_queue_attr_make_initially_inactive(
dispatch_queue_attr_t _Nullable attr);
#define DISPATCH_QUEUE_SERIAL_INACTIVE \
dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_SERIAL)
#define DISPATCH_QUEUE_CONCURRENT_INACTIVE \
dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_CONCURRENT)
應當注意的是,初始化后處于不可活動狀態的隊列,添加到其中的任務要想開始執行,必須先調用
dispatch_activate()
函數使其狀態變更為可活動狀態.
- 并行隊列
并行隊列有兩種:
第一種:全局并發隊列創建方法,也是系統為我們創建好的并發隊列,創建方式
/* - QOS_CLASS_USER_INTERACTIVE
* - QOS_CLASS_USER_INITIATED
* - QOS_CLASS_DEFAULT
* - QOS_CLASS_UTILITY
* - QOS_CLASS_BACKGROUND
*/
//dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
這里有兩個參數,第一個參數標識線程執行優先級,第二個是蘋果保留參數傳參:0 就可以。
第二種:手動創建并發隊列
// 串行執行,第一個參數是名稱 ,第二個是標識:DISPATCH_QUEUE_CONCURRENT,并發隊列標識
dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
- 主隊列
主隊列是一種特殊的串行隊列
dispatch_queue_t queue = dispatch_get_main_queue();
同步、異步以及隊列的組合就可以實現對任務進行多線程編程的需求了。
- 同步串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
for(NSInteger i = 0; i < 10; i++){
dispatch_sync(queue1, ^{
NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
});
}
//thread == <NSThread: 0x6000011b8880>{number = 1, name = main} i====n
可以看到沒有開啟新的線程,都是在主線程中執行任務,并且是順序執行的
- 同步并行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
for(NSInteger i = 0; i < 10; i++){
dispatch_sync(queue1, ^{
NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
});
}
// thread == <NSThread: 0x600001db8a00>{number = 1, name = main} i====n
也是在主線程中順序執行。
- 異步串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
for(NSInteger i = 0; i < 10; i++){
dispatch_async(queue1, ^{
NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
});
}
開啟子線程,順序執行任務
- 異步并發隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
for(NSInteger i = 0; i < 10; i++){
dispatch_async(queue1, ^{
NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
});
}
/*
thread == <NSThread: 0x6000024f9440>{number = 4, name = (null)} i====0
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====2
thread == <NSThread: 0x6000024a8780>{number = 3, name = (null)} i====3
thread == <NSThread: 0x6000024ac6c0>{number = 6, name = (null)} i====1
thread == <NSThread: 0x6000024f4a80>{number = 8, name = (null)} i====5
thread == <NSThread: 0x6000024b0b40>{number = 7, name = (null)} i====4
thread == <NSThread: 0x60000249cd00>{number = 9, name = (null)} i====6
thread == <NSThread: 0x6000024b0980>{number = 10, name = (null)} i====7
thread == <NSThread: 0x6000024cb900>{number = 11, name = (null)} i====8
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====9
*/
開啟了多個子線程,并且是并發執行任務。
注意 dispatch_async()
具備開辟新線程的能力,但是不表示使用它就一定會開辟新的線程。 例如 傳入的 queue 是主隊列,就是在主線程中執行任務,沒有開辟新線程。
dispatch_queue_t queue1 = dispatch_get_main_queue();
for(NSInteger i = 0; i < 10; i++){
sleep(2);
dispatch_async(queue1, ^{
NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
});
}
//thread == <NSThread: 0x600002b24880>{number = 1, name = main} i====n
主隊列是一種特殊的串行隊列,從打印結果看出,這里執行方式是串行,而且沒有開啟新的線程。
具體任務的執行方式可以參考下面的表格
[4] dispatch_ group_ t 隊列組
dispatch_group_t
是一個比較實用的方法,通過構造一個組的形式,將各個同步或異步提交任務都加入到同一個組中,當所有任務都完成后會收到通知,用于進一步處理.舉個簡單的例子如下:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
for (int i = 0; i < 10; I++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 10; I++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_async(group, concurrentQueue, ^{
for (int i = 0; i < 10; I++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_notify(group, concurrentQueue, ^{
NSLog(@"All Task Complete");
});
[5] diapatch_barrier_async 柵欄異步調用函數
有異步調用就也有同步調用函數diapatch_barrier_sync()
,兩者的區別:dispatch_barrier_sync
需要等待柵欄執行完才會執行柵欄后面的任務,而dispatch_barrier_async
無需等待柵欄執行完,會繼續往下走,有什么用呢?其實柵欄函數用的最多的地方還是實現線程同步使用,比如我們有這樣一個需求:怎么樣利用GCD實現多讀單寫文件的IO操作?也就是怎么樣實現多讀單寫,看代碼:
@interface UserCenter()
{
// 定義一個并發隊列
dispatch_queue_t concurrent_queue;
// 用戶數據中心, 可能多個線程需要數據訪問
NSMutableDictionary *userCenterDic;
}
// 多讀單寫模型
@implementation UserCenter
- (id)init
{
self = [super init];
if (self) {
// 通過宏定義 DISPATCH_QUEUE_CONCURRENT 創建一個并發隊列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 創建數據容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key
{
__block id obj;
// 同步讀取指定數據,立刻返回讀取結果
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 異步柵欄調用設置數據
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
可以看到把寫操作放入柵欄函數,可以實現線程同步效果
注意:使用dispatch_barrier_async
,該函數只能搭配自定義并發隊列dispatch_queue_t
使用。不能使用全局并發隊列:dispatch_get_global_queue
,否則dispatch_barrier_async
無作用。
[6] 線程死鎖
先來看兩個例子:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"執行任務2");
});// 往主線程里面 同步添加任務 會發生死鎖現象
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(myQueue, ^{
NSLog(@"1111,thread====%@",[NSThread currentThread]);
dispatch_sync(myQueue, ^{
NSLog(@"2222,thread====%@",[NSThread currentThread]);
});
});
// 1111,thread====<NSThread: 0x6000022dd880>{number = 5, name = (null)}
// crash
上面的例子可以看出,不能向當前的串行隊列,同步添加任務,否則會產生死鎖導致crash。線程死鎖的條件:使用sync函數往當前串行隊列里面添加任務,會產生死鎖。
(4). NSOperation
NSOperation 是蘋果對GCD面向對象的封裝,它的底層是基于GCD
實現的,相比于GCD它添加了更多實用的功能
- 可以添加任務依賴
- 任務執行狀態的控制
- 設置最大并發數
它有兩個核心類分別是NSOperation
和NSOperationQueue
,NSOperation就是對任務進行的封裝,封裝好的任務交給不同的NSOperationQueue即可進行串行隊列的執行或并發隊列的執行。
[1] NSOperation
NSOperation 是一個抽象類,并不能直接實用,必須使用它的子類,有三種方式:NSInvocationOperation
、NSBlockOperation
、自定義子類繼承NSOperation
,前兩中是蘋果為我們封裝好的,可以直接使用,自定義子類,需要我們實現相應的方法。
- NSBlockOperation & NSInvocationOperation
使用:
//創建一個NSBlockOperation對象,傳入一個block
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
}];
/*
創建一個NSInvocationOperation對象,指定執行的對象和方法
該方法可以接收一個參數即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];
// 執行
[operation start];
[invocationOperation start];
// 打印: Task1 <NSThread: 0x6000019581c0>{number = 1, name = main} 0
可以看到創建這兩個任務對象去執行任務,并沒有開啟新線程。NSBlockOperation 相比 NSInvocationOperation 多了個addExecutionBlock
追加任務的方法,
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
}
}];
[operation addExecutionBlock:^{
NSLog(@"task2=====%@",[NSThread currentThread]);
}];
[operation addExecutionBlock:^{
NSLog(@"task3=====%@",[NSThread currentThread]);
}];
[operation addExecutionBlock:^{
NSLog(@"task4=====%@",[NSThread currentThread]);
}];
[operation start];
/*
task3=====<NSThread: 0x600000509840>{number = 6, name = (null)}
task4=====<NSThread: 0x600000530200>{number = 3, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 0
task2=====<NSThread: 0x600000511680>{number = 5, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 1
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 2
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 3
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 4
*/
使用
addExecutionBlock
追加的任務是并發執行的,如果這個操作的任務數大于1那么會開啟子線程并發執行任務,這里追加的任務不一定就是子線程,也有可能是主線程。
[2] NSOperationQueue
NSOperationQueue 有兩種隊列,一個是主隊列通過[NSOperationQueue mainQueue]
獲取,還有一個是自己創建的隊列[[NSOperationQueue alloc] init]
,它同時具備并發跟串行的能力,可以通過設置最大并發數來決定。
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
}
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"Task2===== %@ %d", [NSThread currentThread], i);
}
}];
NSOperationQueue *queues = [[NSOperationQueue alloc] init];
[queues setMaxConcurrentOperationCount:2];//設置最大并發數,如果設置為1則串行執行
[queues addOperation:operation];
[queues addOperation:operation2];
/*
Task2===== <NSThread: 0x600000489940>{number = 4, name = (null)} 0
task1=====<NSThread: 0x6000004e15c0>{number = 5, name = (null)} 0
*/
這個例子有兩個任務,如果設置最大并發數為2,則會開辟兩個線程,并發執行這兩個任務。如果設置為1,則會在新的線程中串行執行。
[3] 任務依賴
addDependency
可以建立兩個任務之間的依賴關系,如[operation2 addDependency:operation1];
為任務2依賴任務1,必須等任務1執行完成后才會執行任務2,看個例子
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
}
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; I++)
{
NSLog(@"Task2===== %@ %d", [NSThread currentThread], i);
}
}];
NSOperationQueue *queues = [[NSOperationQueue alloc] init];
[queues setMaxConcurrentOperationCount:2];
//設置任務依賴
[operation addDependency:operation2];
[queues addOperation:operation];
[queues addOperation:operation2];
/*
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4
*/
還是已上面的例子,設置[operation addDependency:operation2];
,可以看到任務2完成后才會執行任務1的操作。
[4] 自定義NSOperation
任務執行狀態的控制是相對于自定義的NSOperation子類來說的。對于自定義NSOperation子類有兩種類型:
- 重寫
main
方法
只重寫operation
的main方法,main方法里面寫要執行的任務,系統底層控制變更任務執行完成狀態,以及任務的退出。看個例子
#import "TestOperation.h"
@interface TestOperation ()
@property (nonatomic, copy) id obj;
@end
@implementation TestOperation
- (instancetype)initWithObject:(id)obj{
if(self = [super init]){
self.obj = obj;
}
return self;
}
- (void)main{
NSLog(@"開始執行任務%@ thread===%@",self.obj,[NSThread currentThread]);
}
調用
TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
[operation4 setCompletionBlock:^{
NSLog(@"執行完成 thread===%@",[NSThread currentThread]);
}];
[operation4 start];
// 打印
開始執行任務我是任務4 thread===<NSThread: 0x6000008d8880>{number = 1, name = main}
執行完成 thread===<NSThread: 0x60000089fa40>{number = 7, name = (null)}
可以看到任務operation的main方法執行是在主線程中的,只是最后完成后的回調setCompletionBlock
是異步的,好像沒什么用,別著急,我們把他放入隊列中執行看下,還是上面的例子,加入隊列執行
NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
TestOperation *operation5 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務5"]];
TestOperation *operation6 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務6"]];
[queue4 addOperation:operation4];
[queue4 addOperation:operation5];
[queue4 addOperation:operation6];
//打印:
開始執行任務我是任務6 thread===<NSThread: 0x600001fc8200>{number = 5, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600001fcc040>{number = 6, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600001fd7c80>{number = 7, name = (null)}
這時候可以看到任務的并發執行了,operation的main方法執行結束后就會調用各自的dealloc
方法進行釋放,任務的生命周期結束。如果我們想讓任務4、5、6 倒序執行,可以添加任務依賴
[operation4 addDependency:operation5];
[operation5 addDependency:operation6];
// 打印
開始執行任務我是任務6 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
這樣做貌似是可以的,但是如果我們的operation 中又存在異步任務(如網絡請求),我們想讓網絡任務6請求完后調用任務5,任務5調用成功后調任務4,那該怎么辦呢,我們先賣個關子,我們在第二節多個請求完成后繼續進行下一個請求的方法總結
中介紹。
- 重寫
start
方法
通過重寫main
方法可以實現任務的串行執行,如果要讓任務并發執行,就需要重寫start
方法。兩者還是有很大區別的:
如果只是重寫main方法,方法執行完畢,那么整個operation就會從隊列中被移除。如果你是一個自定義的operation并且它是某些類的代理,這些類恰好有異步方法,這時就會找不到代理導致程序出錯了。然而start方法就算執行完畢,它的finish屬性也不會變,因此你可以控制這個operation的生命周期了。然后在任務完成之后手動cancel掉這個operation即可。
@interface TestStartOperation : NSOperation
- (instancetype)initWithObject:(id)obj;
@property (nonatomic, copy) id obj;
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@end
@implementation TestStartOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (instancetype)initWithObject:(id)obj{
if(self = [super init]){
self.obj = obj;
}
return self;
}
- (void)start{
//在任務開始前設置executing為YES,在此之前可能會進行一些初始化操作
self.executing = YES;
NSLog(@"開始執行任務%@ thread===%@",self.obj,[NSThread currentThread]);
/*
需要在適當的位置判斷外部是否調用了cancel方法
如果被cancel了需要正確的結束任務
*/
if (self.isCancelled)
{
//任務被取消正確結束前手動設置狀態
self.executing = NO;
self.finished = YES;
return;
}
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// NSLog(@"response==%@",response);
NSLog(@"TASK完成:====%@ thread====%@",weakSelf.obj,[NSThread currentThread]);
//任務執行完成后手動設置狀態
weakSelf.executing = NO;
weakSelf.finished = YES;
}];
[task resume];
}
- (void)setExecuting:(BOOL)executing
{
//手動調用KVO通知
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
//調用KVO通知
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isExecuting
{
return _executing;
}
- (void)setFinished:(BOOL)finished
{
//手動調用KVO通知
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
//調用KVO通知
[self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isFinished
{
return _finished;
}
- (BOOL)isAsynchronous
{
return YES;
}
- (void)dealloc{
NSLog(@"Dealloc %@",self.obj);
}
執行與結果
NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
TestStartOperation *operation4 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
TestStartOperation *operation5 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務5"]];
TestStartOperation *operation6 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務6"]];
//設置任務依賴
[operation4 addDependency:operation5];
[operation5 addDependency:operation6];
[queue4 addOperation:operation4];
[queue4 addOperation:operation5];
[queue4 addOperation:operation6];
/*打印
開始執行任務我是任務6 thread===<NSThread: 0x600002bb8480>{number = 6, name = (null)}
TASK完成:====我是任務6 thread====<NSThread: 0x600002bd4d80>{number = 8, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600002bb0300>{number = 5, name = (null)}
TASK完成:====我是任務5 thread====<NSThread: 0x600002bb0300>{number = 5, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600002bfb080>{number = 7, name = (null)}
TASK完成:====我是任務4 thread====<NSThread: 0x600002bfb080>{number = 7, name = (null)}
2021-06-22 17:57:56.436591+0800 Interview01-打印[15994:9172130] Dealloc 我是任務4
2021-06-22 17:57:56.436690+0800 Interview01-打印[15994:9172130] Dealloc 我是任務5
2021-06-22 17:57:56.436784+0800 Interview01-打印[15994:9172130] Dealloc 我是任務6
*/
在這個例子中我們在任務請求完成后,手動設置其self.executing
和self.finished
狀態,并且手動觸發KVO,隊列會監聽任務的執行狀態。由于我們設置了任務依賴,當任務6請求完成后才會執行任務5,任務5請求完成后 才會執行任務4。最后對各自任務進行移除隊列并釋放。其實這樣也變相解決了上面重寫main
方法中無法解決的問題。
二.實際應用
多個請求完成后繼續進行下一個請求的方法總結
在我們的工作中經常會遇到這樣的請求:一個請求依賴另一個請求的結果,或者多個請求一起發出然后再獲取所有的結果后繼續后續操作。根據這幾種情況總結常用的方法:
1. 使用GCD
的dispatch_group_t
實現
需求:請求順序執行,執行完成后回調結果
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_group_t downloadGroup = dispatch_group_create();
for (int i=0; i<10; i++) {
dispatch_group_enter(downloadGroup);
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"執行完請求=%d",i);
dispatch_group_leave(downloadGroup);
}];
[task resume];
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSLog(@"end");
});
/*
2021-06-22 18:37:56.786878+0800 Interview01-打印[17121:9352056] 請求結束:0
2021-06-22 18:37:56.787770+0800 Interview01-打印[17121:9352057] 請求結束:1
2021-06-22 18:37:56.788492+0800 Interview01-打印[17121:9352057] 請求結束:2
2021-06-22 18:37:56.789148+0800 Interview01-打印[17121:9352057] 請求結束:3
2021-06-22 18:37:56.789837+0800 Interview01-打印[17121:9352057] 請求結束:4
2021-06-22 18:37:56.790433+0800 Interview01-打印[17121:9352059] 請求結束:5
2021-06-22 18:37:56.791117+0800 Interview01-打印[17121:9352059] 請求結束:6
2021-06-22 18:37:56.791860+0800 Interview01-打印[17121:9352059] 請求結束:7
2021-06-22 18:37:56.792614+0800 Interview01-打印[17121:9352059] 請求結束:8
2021-06-22 18:37:56.793201+0800 Interview01-打印[17121:9352059] 請求結束:9
2021-06-22 18:37:56.804529+0800 Interview01-打印[17121:9351753] end*/
主要方法:
-
dispatch_group_t downloadGroup = dispatch_group_create();
創建隊列組 -
dispatch_group_enter(downloadGroup);
每次執行請求前調用 -
dispatch_group_leave(downloadGroup);
請求完成后調用離開方法 -
dispatch_group_notify()
所有請求完成后回調block - 對于enter和leave必須配合使用,有幾次enter就要有幾次leave
2. GCD
信號量dispatch_semaphore_t
(1).需求:順序執行多個請求,都執行完成后回調給end
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"請求結束:%d",i);
dispatch_semaphore_signal(sem);
}];
[task resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"end");
});
主要方法
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_semaphore_signal(sem);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_semaphore
信號量為基于計數器的一種多線程同步機制,dispatch_semaphore_signal(sem);
表示為計數+1操作,dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
信號量-1,遇到dispatch_semaphore_wait
如果信號量的值小于0,就一直阻塞線程,不執行后面的所有程序,直到信號量大于等于0;當第一個for循環執行后dispatch_semaphore_wait
堵塞線程,直到執行到dispatch_semaphore_signal
后繼續下一個for循環進行請求,以此類推完成順序請求。
(2).需求:多個請求同時進行,都執行完成后回調給end
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block int count = 0;
for (int i=0; i<10; i++) {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%d---%d",i,i);
count++;
if (count==10) {
dispatch_semaphore_signal(sem);
count = 0;
}
}];
[task resume];
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"end");
});
/*
2021-06-23 09:47:49.723576+0800 Interview01-打印[21740:9823752] 請求完成:0
2021-06-23 09:47:49.741118+0800 Interview01-打印[21740:9823751] 請求完成:1
2021-06-23 09:47:49.756781+0800 Interview01-打印[21740:9823752] 請求完成:3
2021-06-23 09:47:49.765250+0800 Interview01-打印[21740:9823752] 請求完成:2
2021-06-23 09:47:49.773008+0800 Interview01-打印[21740:9823756] 請求完成:4
2021-06-23 09:47:49.797809+0800 Interview01-打印[21740:9823751] 請求完成:5
2021-06-23 09:47:49.801775+0800 Interview01-打印[21740:9823751] 請求完成:6
2021-06-23 09:47:49.805542+0800 Interview01-打印[21740:9823751] 請求完成:7
2021-06-23 09:47:49.814714+0800 Interview01-打印[21740:9823751] 請求完成:8
2021-06-23 09:47:49.850517+0800 Interview01-打印[21740:9823753] 請求完成:9
2021-06-23 09:47:49.864394+0800 Interview01-打印[21740:9823591] end
*/
這個也比較好理解,for循環運行后堵塞當前線程(當前是主線程,你也可以把這段代碼放入子線程中去執行),當10個請求全部完成后發送信號,繼續下面的流程。
3. 使用NSOperation
與GCD
結合使用
需求:兩個網絡請求,第一個依賴第二個的回調結果
通過自定義operation
實現,我們重寫其main方法
@interface CustomOperation : NSOperation
@property (nonatomic, copy) id obj;
- (instancetype)initWithObject:(id)obj;
@end
@implementation CustomOperation
- (instancetype)initWithObject:(id)obj{
if(self = [super init]){
self.obj = obj;
}
return self;
}
- (void)main{
//創建信號量并設置計數默認為0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSLog(@"開始執行任務%@",self.obj);
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"TASK完成:====%@ thread====%@",self.obj,[NSThread currentThread]);
//請求成功 計數+1操作
dispatch_semaphore_signal(sema);
}];
[task resume];
//若計數為0則一直等待
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
調用與結果
NSOperationQueue *queue3 = [[NSOperationQueue alloc] init];
[queue3 setMaxConcurrentOperationCount:2];
CustomOperation *operation0 = [[CustomOperation alloc] initWithObject:@"我是任務0"];
CustomOperation *operation1 = [[CustomOperation alloc] initWithObject:@"我是任務1"];
CustomOperation *operation2 = [[CustomOperation alloc] initWithObject:@"我是任務2"];
CustomOperation *operation3 = [[CustomOperation alloc] initWithObject:@"我是任務3"];
[operation0 addDependency:operation1];
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];
[queue3 addOperation:operation0];
[queue3 addOperation:operation1];
[queue3 addOperation:operation2];
[queue3 addOperation:operation3];
/**打印結果
開始執行任務我是任務3
TASK完成:====我是任務3 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
開始執行任務我是任務2
TASK完成:====我是任務2 thread====<NSThread: 0x6000039ece80>{number = 7, name = (null)}
開始執行任務我是任務1
TASK完成:====我是任務1 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
開始執行任務我是任務0
TASK完成:====我是任務0 thread====<NSThread: 0x6000039c3d00>{number = 6, name = (null)}
*/
- 設置任務依賴并且添加到隊列后是可以滿足我們的需求
- 由于任務內部是異步回調,可以看到任務內部的執行還是依賴于
dispatch_semaphore_t
來實現的 - 也可以通過重寫
start
方法實現,在上面章節我們已經介紹過了,這里不再贅述。
三. 總結
本文的篇幅有點長了,但是還有一些內容沒有覆蓋到,比如iOS中常用的線程鎖、NSOperationQueue
的暫停與取消等,我們會在后面的文章中逐步完善補充。
由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。
參考資料: