什么是Promise

借用PromiseKit文档中的定义:

A promise represents the future value of an asynchronous task.

如果去Google一下Promise模式关键字,会发现基本都是和JavaScript相关的,在JavaScript中,Promise模式应用的非常多,因为JavaScript语言自身的特点(函数是一等对象),以及运行环境(浏览器,Node.js)决定了在JavaScript里异步操作运用的非常多,所以很多JavaScript开发者也想了很多办法更好的管理这些异步操作,Promise模式是其中最简洁有效的一种。

在iOS开发中,同样会面临和JavaScript运行环境类似的问题。作为客户端开发,我们必须要保证用户界面流畅不卡顿,所以要尽量避免有长时间运行的操作阻塞主线程。那使用的方式就是把耗时比较长的比如I/O操作、网络请求、数据库查询之类的操作放到后台线程里去做,等操作完成之后再通知主线程更新UI。这些放到后台里任务就构成了一系列异步操作。最传统的处理异步任务的方式是使用回调(Callback)。回调本身没什么问题,但是使用回调的异步操作代码相对来说更难看了。

编造一个真实场景的例子,假设我们要处理一个用户登录的流程:

  1. 首先向服务器发起网络请求验证用户的账号密码。
  2. 验证成功之后查询本地数据库里有没有用户的相关信息:
    a. 如果没有,向服务器发起网络请求拉取用户信息.
    b. 如果有,把本地用户数据的时间戳发送给服务器检查需不需要更新数据,如果需要的话同样也做一次数据拉取。
  3. 拉取用户信息成功之后,把拉取到用户信息写回到数据库。

整个流程中还需要考虑每一步的错误处理。

如果不考虑主线程阻塞的问题,就可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)doSync { 
BOOL loginSuccess = [_client loginWithUsername:@"aUser" andPassword:@"123456"];
if (!loginSuccess) {
NSLog(@"登录验证失败!");
}
id userData = [_db queryWithUsername:@"aUser"];
BOOL needFetch = NO;
if (userData) {
NSNumber * timestamp = [userData timestamp];
needFetch = [_client checkTimestamp:timestamp];
}
if (needFetch) {
id userData = [_client fetchUserData];
[_db writeUserData:userData];
}
NSLog(@"操作成功");
}

整个流程写下来非常清晰。但是真实情况下,为了避免阻塞线程,我们都会把网络操作、I/O操作包括数据库操作等等写成异步接口,这种情况下我们再看下代码要怎么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)doAsync {
[_client loginWithUsername:@"aUser" andPassword:@"123456" result:^(BOOL success){
if (success) {
[_db queryWithUsername:@"aUser" result:^(id userData){
if (userData) {
NSNumber * timestamp = [userData timestamp];
[_client checkTimestamp:timestamp result:^(BOOL needFetch){
if (needFetch) {
[_client fetchUserDataWithResult:^(id userData){
[_db writeUserData:userData result:^{
NSLog(@"操作成功!");
}]
}];
} else {
NSLog(@"操作成功!");
}
}];
}
}];
} else {
NSLog(@"登录失败!");
}
}];
}

最后再看一下用了Promise模式的异步接口,代码是什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)doPromiseAsync {
PMKPromise *loginPromise = [_client loginWithUsername:@"aUser" andPassword:@"123456"];
PMKPromise * queryPromise = loginPromise.then(^{
return [_db queryWithUsername:@"aUser"];
});
PMKPromise * checkPromise = queryPromise.then(^(id userData){
if (userData) {
NSNumber * timestamp = [userData timestamp];
return [_client checkTimestamp:timestamp];
} else {
return YES;
}
});
PMKPromise * needFetchPromise = checkPromise.then(^(BOOL needFetch){
if (!needFetch) {
return;
}
PMKPromise * fetchPromise = [_client fetchUserData];
PMKPromise * writePromise = fetchPromise.then(^(id userData){
return [_db writeUserData:userData];
});
});
needFetchPromise.then(^{
NSLog(@"操作成功!");
}).catch(^(NSError *err){
NSLog(@"操作失败:%@", err);
});
}

使用Promise构建的异步操作的代码结构变得和同步接口很相似了,调用异步接口直接通过返回值返回一个Promise对象,这个对象‘保证‘会把相应异步操作会在将来到来的结果的值,通过then方法交给后续的调用。Promise支持链式调用,所以上面的例子中,我们可以省略中间变量,写成下面这种更简洁的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)doPromiseAsync {
[_client loginWithUsername:@"aUser" andPassword:@"123456"].then(^{
return [_db queryWithUsername:@"aUser"];
}).then(^(id userData){
if (userData) {
NSNumber * timestamp = [userData timestamp];
return [_client checkTimestamp:timestamp];
} else {
return YES;
}
}).then(^(BOOL needFetch){
if (!needFetch) {
return;
}
return [_client fetchUserData].then(^(id userData){
return [_db writeUserData:userData];
});
}).then(^{
NSLog(@"操作成功!");
}).catch(^(NSError *err){
NSLog(@"操作失败:%@", err);
});
}

简而言之,使用Promise模式写异步操作,避免了层层嵌套的回调,使得代码结构和逻辑更直接更清晰。像使用同步接口一样,代码的书写顺序就是代码的执行顺序。

PromiseKit的基本使用

直接引用PromiseKit首页的说明:

PromiseKit is not just a promises implementation, it is also a collection of helper functions that make the typical asynchronous patterns we use as iOS developers delightful too.

首先看一下Promise对象PMKPromise的主要接口:

then

then用来传入异步操作完成之后对结果的操作的Block。例如:

1
2
3
4
UIAlertView *alertView = [UIAlertView …];
[alertView promise].then(^(NSNumber *dismissedIndex){
NSLog(@"index %@ pressed.", dismissedIndex);
});

Block的返回值可以是任意对象类型,值类型,PMKPromise对象类型,或者NSError类型。then的返回值也是PMKPromise类型。为了方便描述,假设原PMKPromise对象是AAthen方法返回的PMKPromise对象是B

  • 如果Block的返回值是对象类型或者值类型,那么B也会进一步向后传递这个值或者对象给then的Block。
  • 如果Block的返回值也是PMKPromise对象,称作C,那么会首先执行C对应的异步操作,再把将C的Block的返回值传递给B
  • 如果Block的返回值类型是NSError,那么将触发错误处理流程。这部分在错误处理的章节会做详细说明。

举例说明:

  1. Block返回值是值/对象类型:

    1
    2
    3
    4
    5
    6
    [alertView promise].then(^(NSNumber *dismissedIndex){
    NSLog(@"index %@ pressed.", dismissedIndex);
    return self.contacts[dismissedIndex.integerValue];
    }).then(^(id contact){
    NSLog(@"contact name: %@ pressed.", [contact name]);
    });
  2. Block返回值类型也是PMKPromise类型:

    1
    2
    3
    4
    5
    6
    [alertView promise].then(^(NSNumber *dismissedIndex){
    NSLog(@"index %@ pressed.", dismissedIndex);
    return [self.client promiseQueryCotactAtIndex:dismissedIndex];
    }).then(^(id contact){
    NSLog(@"contact name: %@ pressed.", [contact name]);
    });

catch

如果调用链上某个Block返回值类型是NSError类型,NSError对象会直接跳过调用链上的then方法向下传递,直到遇到catch方法进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
[alertView promise].then(^id(NSNumber *dismissedIndex){
NSLog(@"index %@ pressed.", dismissedIndex);
if (dismissedIndex < self.contacts.count){
return self.contacts[dismissedIndex.integerValue];
} else {
return [NSError errorWithDomain:NSDemoErrorDomain code:123 userInfo:nil];
}
}).then(^(id contact){
NSLog(@"contact name: %@ pressed.", [contact name]);
}).catch(^(NSError *err){
NSLog(@"error occurred with code: %@", @(err.code));
});

catch方法除了可以进行错误处理,甚至还可以进行错误恢复。如果catch方法有非NSError的返回值,那就暗示着错误已经被处理了,所以后续的调用链会继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[alertView promise].then(^id(NSNumber *dismissedIndex){
NSLog(@"index %@ pressed.", dismissedIndex);
if (dismissedIndex < self.contacts.count){
return self.contacts[dismissedIndex.integerValue].name;
} else {
return [NSError errorWithDomain:NSDemoErrorDomain code:123 userInfo:nil];
}
}).catch(^(NSError *err){
if (err.code == 123) {
return @"Unknown Name";
} else {
// return err;
@throw err;
}
}).then(^(NSString * contactName){
NSLog(@"contact name: %@ pressed.", contactName);
}).catch(^(NSError *err){
NSLog(@"fatal error: %@", @(err.code));
});

finally

finallythencatch的补充,无论调用链最终执行then还是catchfinally的代码块始终都会得到执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[alertView promise].then(^id(NSNumber *dismissedIndex){
NSLog(@"index %@ pressed.", dismissedIndex);
if (dismissedIndex < self.contacts.count){
return self.contacts[dismissedIndex.integerValue];
} else {
return [NSError errorWithDomain:NSDemoErrorDomain code:123 userInfo:nil];
}
}).then(^(id contact){
NSLog(@"contact name: %@ pressed.", [contact name]);
}).catch(^(NSError *err){
NSLog(@"error occurred with code: %@", @(err.code));
}).finally(^{
NSLog(@"operation completed.");
});

了解了PMKPromise的主要对象,我们看一下怎么封装我们自己的Promise模式的异步API:

1
2
3
4
5
6
7
8
- (PMKPromise *)users {
return [PMKPromise promiseWithResolverBlock:^(PMKResolver resolve) {
AVQuery *query = [AVUser query];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
resolve(error ?: objects);
}];
}];
}

PMKPromise对象的promiseWithResolverBlock方法会传入的resolve参数,我们可以在异步操作完成的时候,讲异步操作的结果传给resolve。如果传入的是非NSError对象,那么就触发后续的then,否则触发后续的catch

PromiseKit进阶

Dispatch Queues

现在我们来讨论一下PromiseKitGCD。之前我们看到的PMKPromisethen方法默认都是执行在主线程上的,但是我们可以很容易的通过thenOn方法把这些代码的执行dispatch到其他线程,例如:

1
2
3
4
5
id url = @"http://www.baidu.com/test.png";
id q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[NSURLConnection GET:url].thenOn(q, ^(UIImage *image){
assert(![NSThread isMainThread]);
});

PromiseKit方法还提供了thenInBackground的便利函数,把后续的执行dispatchGCDDISPATCH_QUEUE_PRIORITY_DEFAULT队列。

when

之前我们例子里通过then去串行执行异步操作。很多时候我们还需要并行执行异步操作,等所有异步操作都执行完毕以后再继续后续操作。这时候就用到when方法,举例说明:

1
2
3
4
5
6
7
8
9
id search1 = [[[MKLocalSearch alloc] initWithRequest:rq1] promise];
id search2 = [[[MKLocalSearch alloc] initWithRequest:rq2] promise];
// PMKWhen(@[a, b]) 等价于 [PMKPromise when:@[a, b]]
PMKWhen(@[search1, search2]).then(^(NSArray *results){
id resultOfSearch1 = results[0];
id resultOfSearch2 = results[1];
}).catch(^(NSError *error){
// called if either search fails
});

至此,PromiseKit的常用的方法基本就是这些,其他更多的用法,可以参考PromiseKit文档