開發中經常遇到異步任務之間有依賴關系,需要對執行順序進行調度的情況。
比如,一個頁面要組合多個后端接口的數據,必須所有請求都完成后,再進行數據組裝,最后刷新UI。
如果是同步任務,解決方案很簡單。可以用dispatch_group
,更方便的是用NSOperation
的addDependency
功能,讓最后執行的任務依賴前面幾個任務即可。
我們知道,NSOperation
內部維護了一個狀態機來表示內部任務的執行狀態。一共有下面幾個狀態:
- ready
- executing
- finished
- cancelled
如果我們用addDependency
給兩個NSOperation
設置了依賴關系,那么一個NSOperation
對應的方法或block執行完畢后,會變為finished狀態,這時另一個NSOperation
才會執行。
如果我們能讓異步任務表現得像同步任務一樣,在異步任務收到回調后才變為finished
狀態,不就可以用addDependency
來控制異步任務的執行順序了嗎?
事實上蘋果在NSOperation
的接口里給我們留了一個口子。看NSOperation
的接口:
NS_CLASS_AVAILABLE(10_5, 2_0)
@interface NSOperation : NSObject {
@private
id _private;
int32_t _private1;
#if __LP64__
int32_t _private1b;
#endif
}
- (void)start;
- (void)main;
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
@property (readonly, getter=isReady) BOOL ready;
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
@property NSOperationQueuePriority queuePriority;
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end
我們需要重點關注的是asynchronous
屬性。如果它值為true
,那么這個NSOperation
執行完畢后不會自動變為finished狀態,需要手動設置。這正是我們想要的。
我們可以寫一個NSOperation
的子類,給異步任務提供一個設為finished狀態的接口。
上代碼:
typealias MLAsyncOperationBlock = (operation:MLAsyncOperation)->Void
class MLAsyncOperation: NSOperation {
private var ml_executing = false{
willSet {
willChangeValueForKey("isExecuting")
}
didSet {
didChangeValueForKey("isExecuting")
}
}
private var ml_finished = false{
willSet {
willChangeValueForKey("isFinished")
}
didSet {
didChangeValueForKey("isFinished")
}
}
private var block:MLAsyncOperationBlock?
override var asynchronous:Bool {
return true
}
override var concurrent:Bool {
return true
}
override var finished:Bool{
return ml_finished
}
override var executing:Bool{
return ml_executing
}
convenience init(operationBlock:MLAsyncOperationBlock) {
self.init()
block = operationBlock
}
override func start() {
if cancelled {
ml_finished = true
return
}
ml_executing = true
block?(operation: self)
}
func finishOperation(){
ml_executing = false
ml_finished = true
}
deinit{
print("operation deinited")
}
}
幾個需要說明的點:
一:
NSOperation
內部有一組成員變量來維護它的executing、finished這些狀態,我們訪問不到。但我們可以另外加一組成員變量,自己來維護這些狀態。一個子類不一定要訪問父類的成員變量,只要接口表現得和父類一樣就行了。
二:
NSOperationQueue
是通過KVO觀察內部的NSOperation
狀態的變化,來自動管理NSOperation
的執行的。我們在設置自己的ml_executing
屬性的時候,需要表現得像executing
屬性被設置了一樣。也就是需要調用一下willChangeValueForKey("isExecuting")
和didChangeValueForKey("isExecuting")
兩個方法。利用Swift屬性的willSet和didSet特性,可以非常方便地實現。
finished屬性同理。
三:
finishOperation
這個方法,是用戶在收到異步回調,任務完成后需要調用的。調用后這個operation就會變為finished狀態。為了方便,我給MLAsyncOperationBlock
加了一個MLAsyncOperation
類型的參數,在調用內部block的時候會把self
傳進去。這樣用戶在構造任務block的時候,通過這個參數就直接可以訪問到operation本身了。
來簡單測試一下好不好用。
先寫一個類實現一個同步方法和兩個異步方法:
class TestClass {
let queue = dispatch_queue_create("TestClass_background_queue", DISPATCH_QUEUE_CONCURRENT)
func method1(){
print("method 1 begin")
for _ in 0 ... 100000 {
continue
}
print("method 1 end")
}
func asyncMethod1(done:()->Void){
print("async method 1 begin")
dispatch_async(queue) { () -> Void in
for _ in 0 ... 100000 {
continue
}
print("async method 1 end")
done()
}
return
}
func asyncMethod2(done:()->Void){
print("async method 2 begin")
dispatch_async(queue) { () -> Void in
for _ in 0 ... 100000 {
continue
}
print("async method 2 end")
done()
}
return
}
}
測試代碼:
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 5
let object = TestClass()
let op1 = NSBlockOperation {
object.method1()
}
let asyncOp1 = MLAsyncOperation { (operation) in
object.asyncMethod1{ () -> Void in
operation.finishOperation()
}
}
let asyncOp2 = MLAsyncOperation { (operation) in
object.asyncMethod2{
operation.finishOperation()
}
}
op1.addDependency(asyncOp1)
op1.addDependency(asyncOp2)
operationQueue.addOperation(asyncOp1)
operationQueue.addOperation(asyncOp2)
operationQueue.addOperation(op1)
let runloop = NSRunLoop.currentRunLoop()
while runloop.runMode(NSDefaultRunLoopMode, beforeDate: NSDate.distantFuture()){
continue
}
由于測試工程是一個command line tool,我用runloop阻塞住了主線程,避免主線程執行完之后整個程序退出,operationQueue
中的代碼來不及執行。
跑一下,輸出結果如下:
async method 1 begin
async method 2 begin
async method 1 end
async method 2 end
method 1 begin
method 1 end
可以看到,兩個異步方法并行執行,在兩個方法都收到回調完成之后,最后一個方法開始執行。完美實現了我們的需求。
完整的代碼可以從我的Github下載。