Objective-C 之 PromiseKit入门
一、PromiseKit介绍
PromiseKit,优雅的的管理多个异步操作,让你从此远离多层嵌套。
用一个例子作比较:
需求:先注册,后登录,提示登录成功。
// 常规方法:嵌套执行多个任务
[self signUpWithUserName:uid pwd:pwd resultBlock:^(BOOL success, NSString *message) {
if (success) {
[self signInWithUserName:uid pwd:pwd resultBlock:^(BOOL success, NSString *message) {
if (success) {
NSLog(@"登录成功");
}
}];
}
}];
// PromiseKit提供的方法,简洁清晰的展示多任务操作
signUpPromise.then(^{
return signInPromise;
}).then(^{
NSLog(@"登录成功");
}).catch(^(NSError* error){
});
由此可见,用PromiseKit实现的方法更美观,且更易维护。
二、PromiseKit的使用
还是上面那个需求,下面是具体实现步骤:
1. 修改现有的注册任务和登录任务,如下:
// 注册伪代码(登录代码类似)
- (AnyPromise*)promise_signUpWithUserName:(NSString*)uid pwd:(NSString*)pwd {
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
// 网络请求 xxx
if(注册成功) {
adapter(@"注册成功", nil);
}else {
adapter(nil,[self errorWithMessage:"注册失败!"]); // error自己定义
}
}];
}
- (NSError*)errorWithMessage:(NSString*)msg {
return [[NSError alloc] initWithDomain:NSNetServicesErrorDomain code:-101 userInfo:@{NSLocalizedDescriptionKey:msg}];
}
2. 在一个地方管理所有异步操作
[HTTPUtil promise_signUpWithUserName:uid pwd:pwd].then(^{// 注册成功走这里
NSLog(@"注册成功");
return [HTTPUtil promise_signInWithUserName:uid pwd:pwd];// 接下来执行登录操作
}).then(^(NSDate* date){// 登录成功走这里
NSLog(@"登录成功, 时间:%@",date);
}).catch(^(NSError* error){// 注册失败或者登录失败跳到这里,不会执行then方法
NSLog(@"%@",error.userInfo[NSLocalizedDescriptionKey);
});
接下来逐句解读该段代码,并穿插知识点。
2.1 创建任务
AnyPromise
可以看作是任务
,下文以任务
代替。
- (AnyPromise*)promise_signUpWithUserName:(NSString*)uid pwd:(NSString*)pwd {
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
}];
}
任务
的所有初始化方法如下,可在源码查看它们说明,这里不一一解释:
// 常用
+ (instancetype __nonnull)promiseWithAdapterBlock:(void (^ __nonnull)(PMKAdapter __nonnull adapter))block;
+ (instancetype __nonnull)promiseWithResolverBlock:(void (^ __nonnull)(__nonnull PMKResolver))resolveBlock;
+ (instancetype __nonnull)promiseWithValue:(__nullable id)value;
+ (instancetype __nonnull)promiseWithIntegerAdapterBlock:(void (^ __nonnull)(PMKIntegerAdapter __nonnull adapter))block;
+ (instancetype __nonnull)promiseWithBooleanAdapterBlock:(void (^ __nonnull)(PMKBooleanAdapter __nonnull adapter))block;
- (instancetype __nonnull)initWithResolver:(PMKResolver __strong __nonnull * __nonnull)resolver ;
2.2 处理Block
任务
的初始化方法中附有一个Block:PMKAdapter
,其返回的参数表达了该任务
的处理结果是成功还是失败,如:
if(登录成功) {
adapter([NSDate date], nil);// 处理成功
}else {
adapter(nil, error); // 处理失败
}
任务
有2种状态:pending
和resolved
(本人习惯翻译为:等待处理和处理结束),
其中resolved
包括fulfilled
(处理成功)和rejected
(处理失败)。
为什么上段代码中adapter([NSDate date], nil);
代表处理成功,而adapter(nil, error);
代表处理失败呢?
接下来了解PMKAdapter
:
PMKAdapter
可以传递2个参数,第一个是id类型的任何对象,可以是nil,第二个是NSError
对象,也可以是nil。传递不同参数,任务
的状态会不同,如下图所示:
因此可以总结PMKAdapter
的参数:
总结一:如果任意一个参数的类型是
NSError
,任务
处理失败
;
总结二:2个参数都是nil或者第一个是id,第二个是nil,
任务
处理成功
。
处理结果将影响第三步的操作。
下面是所有Block的定义:
typedef void (^PMKAdapter)(id __nullable, NSError * __nullable) ;
typedef void (^PMKResolver)(id __nullable);
typedef void (^PMKIntegerAdapter)(NSInteger, NSError * __nullable) ;
typedef void (^PMKBooleanAdapter)(BOOL, NSError * __nullable) ;
2.3 在一个地方管理所有异步操作
signUpPromise.then(^{// 注册成功走这里
NSLog(@"注册成功");
return signInPromise;// 返回signInPromise:接下来执行登录操作
}).then(^(NSDate* date){// 登录成功走这里
NSLog(@"登录成功, 时间:%@",date);
}).catch(^(NSError* error){// 注册失败或者登录失败跳到这里
NSLog(@"%@",error.userInfo[NSLocalizedDescriptionKey);
});
如果
任务
处理成功
,就会执行紧接其后的then
方法;如果处理失败
,则会跳过后面的所有then
方法,执行catch
方法。
怎样才能使用Block传递过来的参数呢?
第一:如果任务
处理成功,
可以在then
方法里面手动
增加最多3个参数:
.then(^(NSDate* date, ..., ...){
NSLog(@"date: %@",date);
})
第二:如果任务
处理失败
,可以把参数传给NSError
:
adapter(nil,[self errorWithMessage:"登录失败!"]);
然后在catch
方法提取:
.catch(^(NSError* error){
NSLog(@"%@",error.userInfo[NSLocalizedDescriptionKey);
})
三、PromiseKit之AnyPromise
AnyPromise是PromiseKit的关键类,一个AnyPromise对象可以看作是一个任务。
1 属性
@property (nonatomic, readonly) __nullable id value; // 已经处理的Promise的值
@property (nonatomic, readonly) BOOL pending; // 等待处理
@property (nonatomic, readonly) BOOL fulfilled; // 处理成功
@property (nonatomic, readonly) BOOL rejected; // 处理失败
2 连接词
2.1 then
then
:当任务
处理成功
,在主线程执行。
- (AnyPromise * __nonnull (^ __nonnull)(id __nonnull))then;
thenInBackground
:当任务
处理成功
,在默认线程执行。
- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))thenInBackground;
thenOn
:当任务
处理成功
,在指定线程执行。
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, id __nonnull))thenOn;
2.2 catch
catch
:当任务
处理失败
,在主线程运行。
- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))catch;
catchInBackground
:当任务
处理失败
,在global线程
执行。
- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))catchInBackground;
catchOn
:当任务
处理失败
,在指定线程执行。
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, id __nonnull))catchOn;
2.3 ensure
ensure
:当任务
处理结束
,在主线程执行。
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_block_t __nonnull))ensure;
ensureOn
:当任务
处理结束
,在指定线程执行。
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, dispatch_block_t __nonnull))ensureOn;
所有连接词在指定线程执行的用法类似:
.ensureOn(queue, ^{
})
3 进阶用法
3.1 PMKJoin
AnyPromise *__nonnull PMKJoin(NSArray * __nonnull promises);
等待所有任务
处理结束
,执行下一步。(所有任务
处理结束
才会处理处理失败
,如果有就跳转到then
方法)
官方注释:
PMKJoin
waits on all provided promises, then rejects if any of those promises rejects, otherwise it fulfills with values from the provided promises.
大意:
PMKJoin
将会等待所有任务
(promises
参数提供)处理结束
,之后如果有一个任务
处理失败
,PMKJoin
才会处理失败
,否则处理成功
。
例子:吃完饭,喝完汤,才能吃水果。
PMKJoin(@[ricePromise, soupPromise]).then(^(NSArray* messages){// 此时参数是数组类型
return fruitPromise;
}).catch(^(NSError* error){
NSArray* promises = error.userInfo[PMKJoinPromisesKey];// 错误信息中包含了所有任务
for (AnyPromise* promise in promises) {
if (promise.rejected) {
NSLog(@"%@ is rejected",promise);
}
}
});
3.2 PMKWhen
extern AnyPromise * __nonnull PMKWhen(id __nonnull input);
等待所有任务
处理成功
,执行下一步。(一旦有任务
处理失败
,立即跳转到then
方法,且停止执行其他任务)
官方注释:
PMKWhen
rejects as soon as one of the provided promises rejects.
大意:只要其中一个
任务
(input
参数提供)处理失败
,PMKWhen
立即处理失败
。
例子:做饭,拿碗筷,才能吃饭。
PMKWhen(@[cookPromise, setPromise]).then(^(NSArray* messages){// 此时参数是数组类型
return eatPromise;
}).catch(^(NSError* error) {// "处理失败"的任务
NSInteger index = [error.userInfo[PMKFailingPromiseIndexKey] integerValue];
NSLog(@"index:%@ error: %@\n", @(index),error.userInfo[NSLocalizedDescriptionKey]);
});
3.3 PMKRace
extern AnyPromise * __nonnull PMKRace(NSArray * __nonnull promises);
只要处理结束
一个任务
,立即执行then
或者catch
方法,其他任务
继续执行。(只关心第一个处理结束
的任务
,不关心其他任务
)
例子:在众多算法中找出解题速度最快的那个。
PMKRace(@[algPromise1, algPromise2, algPromise3]).then(^(id message){
NSLog(@"%@",message);
}).catch(^(NSError* error){
NSLog(@"%@",error.userInfo[NSLocalizedDescriptionKey]);
});
3.4 PMKAfter
extern AnyPromise * __nonnull PMKAfter(NSTimeInterval duration);
延迟一定时间执行任务
,相当于dispatch_after
。
例子:注册成功后3秒再登录。
signUpPromise.then(^{
// 注册成功
return PMKAfter(3.f).then(^{// 延迟执行signInPromise
return signInPromise;
});
}).then(^{
// 登录成功
}).catch(^(NSError* error){
// handle error
});
3.5 PMKHang
extern id __nullable PMKHang(AnyPromise * __nonnull promise);
阻塞线程,直到任务
处理结束
。这个做法不安全,只在调试时用!!!
例子:阻塞当前线程,直到注册成功或者注册失败才会执行next step。
id value = PMKHang([self promise_signUpWithUserName:uid pwd:pwd]);
NSLog(@"%@ is resolved",value);
// next step
// ...
4. 综合使用
设定任务执行的超时时间
// 限定注册任务的超时时间为2秒
PMKRace(@[PMKWhen([HTTPUtil promise_signUpWithUserName:uid pwd:pwd]), PMKAfter(2.).then(^{
NSLog(@"计时结束~");
})]).then(^(NSString* msg){
NSLog(@"%@",msg);
});
四、PromiseKit的集成(手动)
刚着手把PromiseKit集成到项目的时候,按照网上的教程,无论是CocoaPods还是手动安装都失败,经过摸索,步骤如下:
版本:Xcode 10.1,PromiseKit 6.8.4
-
把PromiseKit项目放在你的项目文件夹,如图:
- 然后把PromiseKit.xcodeproj文件拖到你的项目
- 前往Target-Embbedded Binaries添加PromiseKit.framework,如图:
- 导入头文件,编译
#import <PromiseKit/PromiseKit.h>
此时可能报错:
编译报错解决方法:把Build Active Architecture Only属性改为YES
修改属性这样,就能在模拟器上使用PromiseKit。但是依然不能在真机上运行,可能报错:
运行报错解决方法:修改PromiseKit的可用架构
修改可用架构报错原因:由于在OC中使用了Swift,编译成功后Xcode会生成PromiseKit-Swift.h文件。如果编译失败,则没有此文件,因此报错。
到此,成功集成PromiseKit。
参考:
https://juejin.im/post/5c78eac2f265da2db4143ca2