iOS的異步處理神器——Promises

前言

你是否因為多任務(wù)的依賴而頭疼墩剖?你是否被一個個嵌套的block回調(diào)弄得暈頭轉(zhuǎn)向?
快來投入Promises的懷抱吧痒筒。

正文

回調(diào)任務(wù)是很正常的現(xiàn)象宰闰,比如說購買一個商品,需要下單簿透,然后等后臺返回移袍。
單一任務(wù),通常只需要一個block老充,非常清晰葡盗;
以上面的下單為例,傳給網(wǎng)絡(luò)層一個block啡浊,購買完成之后回調(diào)即可觅够。

但是出現(xiàn)多個任務(wù)的時候,邏輯就開始有分支巷嚣,同樣以購買商品為例喘先,在下單完成后,需要和SDK發(fā)起支付廷粒,然后根據(jù)支付結(jié)果再進行一些提示:
任務(wù)1是下單窘拯,執(zhí)行完回調(diào)error指針(或者狀態(tài)碼)表示完成狀態(tài),同時待會下單信息坝茎,此時產(chǎn)生一個分支涤姊,成功繼續(xù)下一步,失敗執(zhí)行錯誤block嗤放;
然后是執(zhí)行任務(wù)2購買砂轻,執(zhí)行異步的支付,根據(jù)支付結(jié)果又會產(chǎn)生一個分支斤吐。

當(dāng)連續(xù)的任務(wù)超過2個之后搔涝,分支會導(dǎo)致代碼邏輯非常混亂和措。


簡單畫一個流程圖來分析庄呈,上述的邏輯變得復(fù)雜的原因是因為每一級的block需要處理下一級block的失敗情況,導(dǎo)致邏輯分支的增多派阱。

其實所有的失敗處理都是類似的:打日志诬留、提示用戶,可以放在一起統(tǒng)一處理。
然后把任務(wù)一文兑、任務(wù)二等串行執(zhí)行盒刚,流程就非常清晰。

Promises就是用來輔助實現(xiàn)這樣設(shè)計的庫绿贞。
實現(xiàn)的代碼效果如下:

- (void)workflow {
    [[[[self order:@"order_id"] then:^id _Nullable(NSString * _Nullable value) {
        return [self pay:value];
    }] then:^id _Nullable(id  _Nullable value) {
        return [self check:value];
    }] catch:^(NSError * _Nonnull error) {
        NSLog(@"error: %@", error);
    }];
}

Promises的使用

Promises庫的引入非常簡單因块,可以使用CocoaPod,Podfile如下:

  pod 'PromisesObjC'

也可以到GitHub手動下載籍铁。

按照Promise設(shè)計模式的規(guī)范涡上,每一個Promise應(yīng)該有三種狀態(tài):pending(等待)、fulfilled(完成)拒名、rejected(失敗)吩愧;
對應(yīng)到Promises分別是:

[FBLPromise pendingPromise]; // pending等待
[FBLPromise resolvedWith:@"anyString"]; // fulfilled完成
[FBLPromise resolvedWith:[NSError new]]; // rejected失敗

實際使用中,我們更多使用的Promises庫已經(jīng)提供好的便捷函數(shù):

啟動一個異步任務(wù)

    [FBLPromise onQueue:dispatch_get_main_queue()
                  async:^(FBLPromiseFulfillBlock fulfill,
                          FBLPromiseRejectBlock reject) {
                      BOOL success = arc4random() % 2;
                      if (success) {
                          fulfill(@"success");
                      }
                      else {
                          reject([NSError errorWithDomain:@"learn_promises_error" code:-1 userInfo:nil]);
                      }
                  }];

或者簡單使用do方法

    [FBLPromise do:^id _Nullable{
        BOOL success = random() % 2;
        if (success) {
            return @"success";
        }
        else {
            return [NSError errorWithDomain:@"learn_promises_error" code:-1 userInfo:nil];
        }
    }];

不管是async方法還是do方法增显,他們的返回值都是創(chuàng)建一個Promise對象雁佳,可以在Promise對象后面掛一個then方法,表示這個Promise執(zhí)行完畢之后同云,要繼續(xù)執(zhí)行的任務(wù):

   [[[FBLPromise do:^id _Nullable{
        BOOL success = arc4random() % 2;
        return success ? @"do_success" : [NSError errorWithDomain:@"learn_promises_do_error" code:-1 userInfo:nil];
    }] then:^id _Nullable(id  _Nullable value) {
        BOOL success = arc4random() % 2;
        return success ? @"then_success" : [NSError errorWithDomain:@"learn_promises_then_error" code:-1 userInfo:nil];
    }] catch:^(NSError * _Nonnull error) {
        NSLog(@"error: %@", error);
    }];

上面的catch方法表示統(tǒng)一的error處理甘穿。
promise在完成任務(wù)之后,如果滿足下面的條件會調(diào)用then的方法:
1梢杭、直接調(diào)用fulfill温兼;
2感混、在do方法中返回一個值(不能為error)比搭;
3、在then方法中返回一個值竖般;

調(diào)用reject方法或者返回一個NSError對象咒唆,都會轉(zhuǎn)到catch方法處理届垫。

用上面的do、then全释、catch方法組合装处,就完成多個異步任務(wù)的依賴執(zhí)行:

- (void)workflow {
    [[[[self order:@"order_id"] then:^id _Nullable(NSString * _Nullable value) {
        return [self pay:value];
    }] then:^id _Nullable(id  _Nullable value) {
        return [self check:value];
    }] catch:^(NSError * _Nonnull error) {
        NSLog(@"error: %@", error);
    }];
}

- (FBLPromise<NSString *> *)order:(NSString *)orderParam {
    return [FBLPromise do:^id _Nullable{
        return @"order_success";
    }];
}

- (FBLPromise<NSString *> *)pay:(NSString *)payParam {
    return  [FBLPromise do:^id _Nullable{
        BOOL success = arc4random() % 2;
        return success ? @"pay_success" : [NSError errorWithDomain:@"pay_error" code:-1 userInfo:nil];
    }];
}

- (FBLPromise<NSString *> *)check:(NSString *)checkParam {
    return  [FBLPromise do:^id _Nullable{
        return @"check success";
    }];
}

Promises還提供了很多附加特性,以All和Any為例:
All是所有Promise都fulfill才算完成浸船;
Any是任何一個Promise完成都會執(zhí)行fulfill妄迁;

- (void)testAllAndAny {
    NSMutableArray *arr = [NSMutableArray new];
    [arr addObject:[self work1]];
    [arr addObject:[self work2]];
    
    [[[FBLPromise all:arr] then:^id _Nullable(NSArray * _Nullable value) {
        NSLog(@"then, value:%@", value);
        return value;
    }] catch:^(NSError * _Nonnull error) {
        NSLog(@"all error:%@", error);
    }];
    
    [[[FBLPromise any:arr] then:^id _Nullable(NSArray * _Nullable value) {
        NSLog(@"then, value:%@", value);
        return value;
    }] catch:^(NSError * _Nonnull error) {
        NSLog(@"any error:%@", error);
    }];
}

- (FBLPromise<NSString *> *)work1 {
    return [FBLPromise do:^id _Nullable{
        BOOL success = arc4random() % 2;
        return success ? @"work1 success" : [NSError errorWithDomain:@"work1_error" code:-1 userInfo:nil];
    }];
}

- (FBLPromise<NSNumber *> *)work2 {
    return  [FBLPromise do:^id _Nullable{
        BOOL success = arc4random() % 2;
        return success ? @"work2 success" : [NSError errorWithDomain:@"work2_error" code:-1 userInfo:nil];
    }];
}

Promises原理解析

Promises庫的設(shè)計很簡單,基于Promise設(shè)計模式和iOS的GCD來實現(xiàn)李命。
整個庫由Promise.m/.h和他的Catagory組成登淘。Catagory都是附加特性,基于Promise.m/.h提供的方法做擴展封字,所以這里重點解析下Promise.m/h黔州。
Promise類public頭文件只有寥寥數(shù)個方法:

// 靜態(tài)方法
[FBLPromise pendingPromise]; // pending等待
[FBLPromise resolvedWith:@"anyString"]; // fulfilled完成
[FBLPromise resolvedWith:[NSError new]]; // rejected失敗
// 實例方法
- (void)fulfill:(nullable Value)value; // 完成一個promise
- (void)reject:(NSError *)error;// rejected一個promise

重點在于private.h提供的兩個方法:

/**
 對一個promise添加fulfill和reject的回調(diào)
 */
- (void)observeOnQueue:(dispatch_queue_t)queue
               fulfill:(FBLPromiseOnFulfillBlock)onFulfill
                reject:(FBLPromiseOnRejectBlock)onReject NS_SWIFT_UNAVAILABLE("");

/**
創(chuàng)建一個promise耍鬓,并設(shè)置fulfill、reject方法為傳進來的block
 */
- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue
              chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill
               chainedReject:(FBLPromiseChainedRejectBlock)chainedReject NS_SWIFT_UNAVAILABLE("");

observeOnQueue方法是promise的實例方法流妻,根據(jù)promise當(dāng)前的狀態(tài)牲蜀,如果是fulfilled或者rejected狀態(tài)則會dispatch_group_async到下一次執(zhí)行對應(yīng)的onFulfill和onReject回調(diào);如果是pending狀態(tài)則會創(chuàng)建_observers數(shù)組绅这,往_observers數(shù)組中添加一個block回調(diào)涣达,當(dāng)promise執(zhí)行完畢的時候,根據(jù)state選擇onFulfill或者onReject回調(diào)君躺。

chainOnQueue方法同樣是promise的實例方法峭判,返回的是一個FBLPromise的對象(狀態(tài)是pending)开缎。
方法首先創(chuàng)建的是promise對象棕叫,接著創(chuàng)建了resolver的回調(diào),然后調(diào)用observeOnQueue方法奕删。
當(dāng)self(也是一個promise)執(zhí)行完畢后俺泣,會根據(jù)fulfill、reject回調(diào)類型接著執(zhí)行chainedFulfill完残、chainedReject伏钠;
最后將結(jié)果拋給resolver執(zhí)行,resolver會根據(jù)返回值value進行判斷谨设,如果仍是promise則遞歸執(zhí)行熟掂,否則直接調(diào)用fulfill方法。
fulfill方法則會判斷value是否為NSError扎拣,如果是NSError則轉(zhuǎn)為reject赴肚,否則將狀態(tài)改為Fulfilled,并且通知observer數(shù)組二蓝。

- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue
              chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill
               chainedReject:(FBLPromiseChainedRejectBlock)chainedReject {
  NSParameterAssert(queue);

  FBLPromise *promise = [[FBLPromise alloc] initPending];
  __auto_type resolver = ^(id __nullable value) {
    if ([value isKindOfClass:[FBLPromise class]]) {
      [(FBLPromise *)value observeOnQueue:queue
          fulfill:^(id __nullable value) {
            [promise fulfill:value];
          }
          reject:^(NSError *error) {
            [promise reject:error];
          }];
    } else {
      [promise fulfill:value];
    }
  };
  [self observeOnQueue:queue
      fulfill:^(id __nullable value) {
        value = chainedFulfill ? chainedFulfill(value) : value;
        resolver(value);
      }
      reject:^(NSError *error) {
        id value = chainedReject ? chainedReject(error) : error;
        resolver(value);
      }];
  return promise;
}

Promises中的dispatch_group_enter() 和 dispatch_group_leave() 是成對使用誉券,但是和平時使用GCD不同,這里并沒有用到dispath_group_notify方法刊愚。
在剛開始看Promises源碼時踊跟,產(chǎn)生過一個疑問,為什么所有Promises的操作要放在同一個group內(nèi)鸥诽?

+ (dispatch_group_t)dispatchGroup {
  static dispatch_group_t gDispatchGroup;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    gDispatchGroup = dispatch_group_create();
  });
  return gDispatchGroup;
}

直到發(fā)現(xiàn)FBLWaitForPromisesWithTimeout方法商玫,里面有一個dispatch_group_wait方法(等待group中所有block執(zhí)行完畢,或者在指定時間結(jié)束后回調(diào))牡借。
dispatch_group_wait方法與dispath_group_notify方法類似决帖,只是多了一個超時時間,如果調(diào)用dispatch_group_wait(DISPATCH_TIME_FOREVER)則和dispath_group_notify方法一樣蓖捶。

總結(jié)

附加的特性有很多地回,類似Retry、Delay等,但實際使用中Promise用do刻像、then畅买、catch、async等少數(shù)幾個已經(jīng)可以滿足需求细睡。
能夠?qū)崿F(xiàn)Promise設(shè)計模式的庫比較多谷羞,Promises是性能和接口調(diào)用清晰度都比較不錯的。
使用設(shè)計模式可以簡化邏輯代碼溜徙,同時也使得代碼的健壯性更強湃缎。

附錄

Promises
PromiseKit

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蠢壹,隨后出現(xiàn)的幾起案子嗓违,更是在濱河造成了極大的恐慌,老刑警劉巖图贸,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹂季,死亡現(xiàn)場離奇詭異,居然都是意外死亡疏日,警方通過查閱死者的電腦和手機偿洁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沟优,“玉大人涕滋,你說我怎么就攤上這事∧痈螅” “怎么了宾肺?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鹃唯。 經(jīng)常有香客問我爱榕,道長,這世上最難降的妖魔是什么坡慌? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任黔酥,我火速辦了婚禮,結(jié)果婚禮上洪橘,老公的妹妹穿的比我還像新娘跪者。我一直安慰自己,他們只是感情好熄求,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布渣玲。 她就那樣靜靜地躺著,像睡著了一般弟晚。 火紅的嫁衣襯著肌膚如雪忘衍。 梳的紋絲不亂的頭發(fā)上逾苫,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音枚钓,去河邊找鬼铅搓。 笑死,一個胖子當(dāng)著我的面吹牛搀捷,可吹牛的內(nèi)容都是我干的星掰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼嫩舟,長吁一口氣:“原來是場噩夢啊……” “哼氢烘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起家厌,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤播玖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后像街,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黎棠,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡晋渺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年镰绎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片木西。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡畴栖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出八千,到底是詐尸還是另有隱情吗讶,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布恋捆,位于F島的核電站照皆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏沸停。R本人自食惡果不足惜膜毁,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望愤钾。 院中可真熱鬧瘟滨,春花似錦、人聲如沸能颁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伙菊。三九已至败玉,卻和暖如春敌土,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背运翼。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工纯赎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人南蹂。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓犬金,卻偏偏與公主長得像,于是被迫代替她去往敵國和親六剥。 傳聞我的和親對象是個殘疾皇子晚顷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容