I. Promise overview

1. Why introduced Promises

In daily development, asynchronous operations are very common. For example, after a network request is initiated, the result of processing the network request is usually asynchronous operations. We usually use blocks or delegates to perform asynchronous operations. But when you have multiple asynchronous operations that need to be nested, the code structure is messy, hard to read, and bug-prone. Suppose there is such a scenario, when playing an encrypted video, it is necessary to obtain the video ID through the resource ID, then decrypt the video, and finally request the playing address of the video or the video itself with the decrypted result. The general writing method is as follows:

// For demonstration purposes only, Did not consider reference cycle and error handling [self getVideoIDWithAssetResourceID: @ "asset - resource - id - 1234" complection: ^ (nsstrings * videoID) {[self decryptVideoWithVideoID:videoID complection:^(NSString *decryptResult) { [self requestVideoDataWithParam:decryptResult complection:^(NSString *videoPlayURLString) { [self.player setVideoURLString:videoPlayURLString]; }]; }]; }];Copy the code

I believe that when you see this code, your head is a little bit confused, at this point promise can play a big role, if you can introduce promise, to achieve the same function of the code is more beautiful. As for how to write it, let me buy a mystery. So promise was introduced to solve the “callback hell” problem of everyday development, making the code structure clearer and easier to read.

2. Promise introduction

  • concept
    • Promise is a writable single-assignment container that sets the value of the future, which is the value, and Promise is the function that sets the value — essentially the return value of the asynchronous function (Promise) (future). See the Wikipedia articles Promise and Future for details.
    • Promise can link multiple asynchronous tasks, enabling chained programming. It’s on the front end so much that there are Promises/A+ specifications.
    • Personally, in iOS, a Promise is a one-time implementation container for a block.
  • State transitionA promise must be in one of three states: pending, fulfilled, and rejected. This is a big pity.
    • When a promise is in the pending state, it can be postponed or rejected
    • This is a big pity. When a promise is in a fulfilled state, it cannot change into any other state. There must be a value, which cannot be changed.
    • When a promise is in the Rejected state, it cannot go into any other state. There must be a reason and this reason cannot be changed.

The most popular Promise open source library in iOS

  • PromiseKit
    • PromiseKit is the work of Max Howell(Homebrew author rejected by Google 😓 for not writing an inversion binary tree), and is probably the most referenced promise implementation library in iOS. It uses Swift and offers a version of OC (bridge references to the base implementation of Swift).
  • Promises
    • Promises is an open source project by Google in the first half of ’18. Promises also implements promise, using OC, and offers a Swift version (bridging to the base implementation of OC).

2, Open-source analysis

Compared with Swift, THE author uses OC more in daily development. Therefore, I focus on the source code of Promises. The following is a simple analysis of its source code.

1, core source code

Look first at fblPromise.h, which provides an interface exposed to the business side. As mentioned above, promise.h provides a promise instance function to initialize the three states and a function to switch from pending to a fulfilled or Rejected state. At the same time, it supports the ability to specify the dispatch queue for callbacks. If you haven’t used class property, you can make up for it a little bit. Use the class attribute modifier in OC

@property (class) dispatch_queue_t defaultDispatchQueue;

+ (instancetype)pendingPromise;
+ (instancetype)resolvedWith:(nullable id)resolution;
- (void)fulfill:(nullable Value)value;
- (void)reject:(NSError *)error;
Copy the code

Fblpromiseprivate. h, which provides the interface and definitions used internally by the library, provides the definition of callback blocks, response functions that listen for promise state, and functions to support chained calls.

typedef void (^FBLPromiseOnFulfillBlock)(Value __nullable value);
typedef void (^FBLPromiseOnRejectBlock)(NSError *error);
typedef id __nullable (^__nullable FBLPromiseChainedFulfillBlock)(Value __nullable value);
typedef id __nullable (^__nullable FBLPromiseChainedRejectBlock)(NSError *error);

- (instancetype)initPending;
- (instancetype)initWithResolution:(nullable id)resolution;
- (void)observeOnQueue:(dispatch_queue_t)queue fulfill:(FBLPromiseOnFulfillBlock)onFulfill reject:(FBLPromiseOnRejectBlock)onReject;
- (FBLPromise *)chainedOnQueue:(dispatch_queue_t)queue chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill chainedReject:(FBLPromiseChainedRejectBlock)chainedReject;
Copy the code

Look again at fblPromise.m, which provides implementations of both files. Think about what objects it needs to hold: state, value, error, array to store block callbacks, objects to hold when a promise is in a pending state (used in the Swift implementation to keep promises alive for chain calls)

typedef NS_ENUM(NSInteger, FBLPromiseState) { FBLPromiseStatePending = 0, FBLPromiseStateFulfilled, FBLPromiseStateRejected, }; typedef void (^FBLPromiseObserver)(FBLPromiseState state, id __nullable resolution); static dispatch_queue_t gFBLPromiseDefaultDispatchQueue; @implement FBLPromise { FBLPromiseState _state; NSMutableSet *__nullable _pendingObjects; id __nullable _value; NSError *__nullable _error; NSMutableArray<FBLPromiseObserver> *_observers; } + (void)initialize { if(self == [FBLPromise class]) { gFBLPromiseDefaultDispatchQueue = dispatch_get_main_queue(); }} /* To ensure thread safety, when reading or writing to a queue or state, Need to lock * / + (dispatch_queue_t) defaultDispatchQueue {@ synchronized (self) {return gFBLPromiseDefaultDispatchQueue; } } + (void)setDefaultDispatchQueue:(dispatch_queue_t)queue { NSParameterAssert(queue); @synchronized(self) { gFBLPromiseDefaultDispatchQueue = queue; }} // Check the value of the container. A status check is required - (void)fulfill: Nullable ID value {if([value isKindOfClass:[NSError Class]]) {[self Reject :(NSError) *)value]; } else { @synchronized(self) { if (_state == FBLPromiseStatePending) { _value = value; _state = FBLPromiseStateFulfilled; _pendingObjects = nil; for (FBLPromiseObserver observer in _observers) { observer(_state, _value); } _observers = nil; dispatch_group_leave(FBLPromise.dispatchGroup); }}}} // Check the error type, pay attention to lock, because it is a one-time block container, - (void) Reject :(NSError *)error {NSAssert([error isKindOfClass:[NSError class]], @"Invalid Error Type. if (! [error isKindOfClass:[NSError class]]) { @throw error; } @synchronized(self) { if (_state == FBLPromiseStatePending) { _state = FBLPromiseStateRejected; _error = error; _pendingObjects = nil; for (FBLPromiseObserver *observer in _observers) { observer(_state, _error); } _observers = nil; dispatch_group_leave(FBLPromise.dispatchGroup); }}} / / why add group, one of the Testing FBLWaitForPromisesWithTimeout method, you need to call dispatch_group_wait method (wait, all block in the group has been completed Or a callback after the specified time). - (instancetype)initPending { self = [super init]; if (self) { dispatch_group_enter(FBLPromise.dispatchGroup); } return self; } - (instancetype)initWithResolution:(nullable id)resolution { self = [super init]; if (self) { if ([resolution isKindOfClass:[NSError class]]) { _state = FBLPromiseStateRejected; _error = resolution; } else { _state = FBLPromiseStateFulfilled; _value = resolution; } } return self; } // check status, Leave group - (void)dealloc {if (_state == FBLPromiseStatePending) { dispatch_group_leave(FBLPromise.dispatchGroup); }} // Listen for the promise state, Call onFulfill or onReject - (void)observeOnQueue:(dispatch_queue_t)queue fulfill:(FBLPromiseOnFulfillBlock)onFullfill reject:(FBLPromiseOnRejectBlock)onReject { NSParameterAssert(queue); NSParameterAssert(onFulfill); NSParameterAssert(onReject); @synchronized(self) { switch(_state) { case FBLPromiseStatePending: { if (_observers == nil) { _observerss = [[NSMutableArray alloc] init]; } FBLPromiseObserver observer = ^(FBLPromiseState state, id __nullable resolution) { dispatch_group_async(FBLPromise.dispatchGroup, queue, ^{ switch(state) { case FBLPromiseStatePending: break; case FBLPromiseStateFulfilled: onFulfill(resolution); break; case FBLPromiseStateRejected: onReject(resolution); break; }}); } [_observers addObject:observer]; break; } } case FBLPromiseStateFulfilled: { dispatch_group_async(FBLPromise.dispatchGroup, queue, ^{ onFulfill(self->_value); }); break; } case FBLPromiseStateRejected: { dispatch_group_async(FBLPromise.dispatchGroup, queue, ^{ onReject(self->_error); }); break; }}} // Solve the chain call problem, Resolver block - (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill chainedReject:(FBLPromiseChainedRejectBlock)chainedReject {  NSParameterAssert(queue); NSParameterAssert(chainedFulfill); NSParameterAssert(chainedReject); FBLPromise *promise = [[FBLPromise alloc] initPending]; [self observe:queue onFulfill:^(id __nullable value) { value = chainedFulfill ? chainedFulfill(value) : value;  if ([value isKindOfClass:[FBLPromise class]]) { [(FBLPromise *)value observe:queue onFulfill:^(id __nullable value) { [promise fulfill:value]; } onReject:^(NSError *error) { [promise reject:error]; }]; } else { [promise fulfill:value];  } } onReject:^(NSError *error) { id value = chainedReject ? chainedReject(value) : value;  if ([value isKindOfClass:[FBLPromise class]]) { [(FBLPromise *)value observe:queue onFulfill:^(id __nullable value) { [promise fulfill:value]; } onReject:^(NSError *error) { [promise reject:error]; }]; } else { [promise fulfill:value]; }}]; return promise; }Copy the code

2. Support ability

Only the features and implementation ideas are introduced here. For detailed implementation, refer to the source code and use methods according to Tests.

1. async
typedef void (^FBLPromiseFulfillBlock)(Value __nullable value);
typedef void (^FBLPromiseRejectBlock)(NSError *error);
typedef void(^FBLPromiseAsyncWorkBlock)(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject);
+ (instancetype)onQueue:(dispatch_queue_t)queue async:(FBLPromiseWorkBlock)work;
Copy the code
  • Creating a pending Promise and executing the Work block asynchronously
  • The dispatch_group_async function is directly called to execute the work block in the queue. The first parameter of the work block is considered compatible with value being the case of the Promise
2. do
typedef id __nullable (^FBLPromiseDoWorkBlock)(void);
+ (instancetype)onQueue:(dispatch_queue_t)queue do:(FBLPromiseDoWorkBlock)work;
Copy the code
  • Creating a pending Promise and executing the Work block asynchronously. Can be seen as a simplified version of Async
  • The implementation is similar to async, except that the work Block is called directly to get the value
3. then
typedef id __nullable (^FBLPromiseThenWorkBlock)(Value __nullable value);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue then:(FBLPromiseThenWorkBlock)work;
Copy the code
  • Then can be regarded as a simplified version of chainOnQueue: chainedReject: chainedReject: Then corresponds to chainedFulfill:. This is a big pity. If the monitored promise state is a big pity, then the corresponding FBLPromiseThenWorkBlock will be invoked, that is, when the promise passes the resolution. If rejected, the block corresponding to THEN is rejected. Then can be understood as a success pity value that is used to capture the previous asynchronous task
  • Implement the chainOnQueue: chainedReject: chainedReject parameter is passed to then work block
4. catch
typedef void (^FBLPromiseCatchWorkBlock)(NSError *error);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue catch:(FBLPromiseCatchWorkBlock)work;
Copy the code
  • Catch is called only when the state of the monitored promise is Rejected.
  • Implement a direct call to chainOnQueue: chainedReject: chainedReject:, chainedReject parameter to catch work block
5. all
+ (FBLPromise<NSArray *> *)onQueue:(dispatch_queue_t)queue all:(NSArray *)promises;
Copy the code
  • Introduce passing in a promise array. All will wait for All the promise states to become fulfilled, and then return an array of values in the order in which the promise array is passed in. If any promise is rejected, then the promise is rejected immediately.
  • Achieve results by async method to generate a promise, then check the incoming array parameter, transform its real promise array, iterate over this array, call [promise observeOnQueue: fulfill: reject] listening state change every promise, This is a big pity until all the promise states become fulfilled or the first promise state becomes rejected, directly reject the result promise.
6. always
typedef void (^FBLPromiseAlwaysWorkBlock)(void);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue always:(FBLPromiseAlwaysWorkBlock)work;
Copy the code
  • Whether the monitored promise status becomes a pity or rejected, always block will always be implemented
  • To realize direct call [the self chainOnQueue: chainedFulfill: chainedReject:], before return calls always work block.
7. any
+ (FBLPromise<NSArray *> *)any:(NSArray *)promises;
Copy the code
  • The introduction will pass in an array of promises, and the Any will wait for all the promise states to become a big pity or rejected. As long as there is a prmise state completed, the promise state will be resolved into a big pity. When all the promise states change to Rejected, the result is that the promise state changes to Rejected, and the error information is the same as that of the promise whose state changes to Rejected.
  • The implementation is very similar to the implementation of all, except that when the promise is listened to, the promise needs to be checked if there is a promise that is fulfilled when the promise is rejected.
8.await
FOUNDATION_EXTERN id __nullable FBLPromiseAwait(FBLPromise *promise, NSError **error);
Copy the code
  • Describes waiting for a promise to resLoved, which blocks the current thread.
  • The implementation uses the semaphore mechanism dispatch_semaphore_t, which uses a lock to wait for the promise to be resLoved. Note: only an Await queue is generated with dispatch_once.
9. delay
- (FBLPromise *)onQueue:(dispatch_queue_t)queue delay:(NSTimeInterval)interval;
Copy the code
  • This topic describes how to create a pending promise. After the delay, the state will become depressing, and the value is the same as self. If self changes to Rejected, the delay will be ignored and the promise will be directly rejected
  • Implement a listener method that calls self and disptch_after in the callback to FULFILL
10. race
+ (instancetype)race:(NSArray *)promised;
Copy the code
  • This is a big pity. If any promise becomes fulfilled, then the promise becomes fulfilled. This is a pity.
  • This implementation uses async to instantiate a result promise, iterate through the array of incoming promises, and listen for each promise. Because of the state feature of promises, after the first promise becomes a pity or Rejected, the result promise will be consistent with it. And then the state doesn’t change.
11. recover
typedef id __nullable (^FBLPromiseRecoverWorkBlock)(NSError *error);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue recover:(FBLPromiseRecoverWorkBlock)recovery;
Copy the code
  • This section describes how to return a new pending Promise when the Receiver changes to Rejected
  • Implementation call [the self chainOnQueue: chainedFulfill: chainedReject:], in chainedReject block in return to recover the work the result of the block.
12. reduce
typedef id __nullable (^FBLPromiseReducerBlock)(Value __nullable partial, id next);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue reduce:(NSArray *)items combine:(FBLPromiseReducerBlock)reducer;
Copy the code
  • This paper introduces the realization of reduce function based on the current promise and items data source. The current promise value is the initial value, and the promise finally returned is the promise of the conversion of the last Item in the array.
  • Implementation call [promise chainOnQueue: chainedFulfill: chainedReject:]
13. retry
typedef id __nullable (^FBLPromiseRetryWorkBlock)(void);
typedef BOOL (^FBLPromiseRetryPredicateBlock)(NSInteger, NSError *);
+ (FBLPromise *)onQueue:(dispatch_queue_t)queue retry:(FBLPromiseRetryWorkBlock)work;
Copy the code
  • This section describes how to create a pending promise. The return value of work is regarded as the value, which becomes depressing. If it becomes rejected, it will be retry.
  • implementation
static void FBLPromiseRetryAttempt(FBLPromise *promise, dispatch_queue_t queue, NSInteger count, NSTimeInterval interval, FBLPromiseRetryPredicateBlock predicate, FBLPromiseRetryWorkBlock work) {
  __auto_type retrier = ^(id __nullable value) {
    if ([value isKindOfClass:[NSError class]]) {
      if(count <= 0 || (predicate && ! predicate(count, value))) { [promise reject:value]; }else{ dispatch_after(dispatch_time(0, (int64_t)(interval * NSEC_PER_SEC)), queue, ^{ FBLPromiseRetryAttempt(promise, queue, count - 1, interval, predicate, work); }); }}else{ [promise fulfill:value]; }}; id value = work();if ([value isKindOfClass:[FBLPromise class]]) {
   [(FBLPromise *)value observeOnQueue:queue fulfill:retrier reject:retrier];
  } else{ retrier(value); }}Copy the code
14. timeout
- (FBLPromise *)onQueue:(dispatch_queue_t)queue timeout:(NSTimeInterval)interval;
Copy the code
  • Describes waiting for a promise until timeout. Return a pending Promise, which is either fulfilled with the same resolution as receiver or rejected with an error of the type timeout
  • Implement monitor receiver PROMISE status, and call dispatch_after reject result PROMISE.
15. validate
typedef BOOL (^FBLPromiseValidateWorkBlock)(Value __nullable value);
- (FBLPromise *)onQueue:(dispatch_queue_t)queue validate:(FBLPromiseValidateWorkBlock)predicate;
Copy the code
  • This will be a pity. If the promise can not be fulfilled, reject the promise
  • This implementation creates a pending promise and listens for the state of the promise. When the promise becomes a pity, the predicate will be called to verify the value. If the promise fails, the result promise will be rejected.

Third, summary

Going back to the original problem, after using Promise, the code might look like this:

[[[[[self getVideoIDWithAssetResourceID:@"asset-resource-id-1234"] then:^id(NSString *value){ return [self decryptVideoWithVideoID:value]; }] then:^id(NSString *value){ return [self requestVideoDataWithParam:value];  }] then:^id(NSString *value) { [self.player setVideoURLString:value]; return value;  }] catch:^(NSError *error) { NSLog(@"play failed!"); }];Copy the code

The structure is much cleaner than the initial approach, and it only uses the THEN feature. Promise also offers all of these features, which can be used in flexible combinations to make writing code much easier. So get out of callback hell and embrace Promise!