構造異步NSOperation進行異步任務調度

開發中經常遇到異步任務之間有依賴關系,需要對執行順序進行調度的情況。

比如,一個頁面要組合多個后端接口的數據,必須所有請求都完成后,再進行數據組裝,最后刷新UI。

如果是同步任務,解決方案很簡單。可以用dispatch_group,更方便的是用NSOperationaddDependency功能,讓最后執行的任務依賴前面幾個任務即可。

我們知道,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下載。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容