RJIterator 1.1.5

RJIterator 1.1.5

renjinkui2719 维护。



  • 作者:
  • renjinkui

生成器和迭代器是 ES6 和 Python 的关键概念,初体验后感受到它们的强大,特别是在异步调用中的运用。RJIterator 是该功能的 OC 实现,可以在 OC/Swift 项目中使用。

1. 异步的运用

异步任务

在 RJIterator 中,一个 RJAsyncClosure 类型的闭包就是一个异步任务,可以被异步调度。

RJAsyncClosure 是 RJIterator 定义的闭包类型:

typedef void (^RJAsyncCallback)(id _Nullable value, id _Nullable error);
typedef void (^RJAsyncClosure)(RJAsyncCallback _Nonnull callback);

同时,RJIterator 兼容 PromiseKit,RJIterator 在运行时会判断,如果一个对象是 AnyPromise 类型,它也是一个异步任务。

异步块

使用 rj_async 声明一个异步块,表示代码块内部将以异步方式调度执行

Objective-C

rj_async(^{
    //异步代码
})
.error(^(id error) {
    //出错处理
})
.finally(^{
    //收尾 不论成功还是出错都会执行
});

Swift

rj_async {
   //...
}
.error {error in
    let error = error as! MyErrorType
    //...
}
.finally {
    //...
}

以登录为例

比如有如下登录场景:登录成功 --> 查询个人信息 --> 下载头像 --> 给头像加特效 --> 进入详情。

为了举例,假设要求每一步必须在上一部完成后才能进行。该功能可以使用异步块如下实现

(1) 定义异步任务
//登录
- (RJAsyncClosure)loginWithAccount:(NSString *)account pwd:(NSString *)pwd {
    return ^(RJAsyncCallback callback){
       //调用http接口
        post(@"/login", account, pwd, ^(id response, error) {
            callback(response.data, error);
        });
    };
}
//拉取信息
- (RJAsyncClosure)queryInfoWithUid:(NSString *)uid token:(NSString *)token{
    return ^(RJAsyncCallback callback){
        get(@"query", uid, token, ^(id response, error) {
            callback(response.data, error);
        });
    };
}
//下载头像
- (RJAsyncClosure)downloadHeadImage:(NSString *)url{
    return ^(RJAsyncCallback callback){
        get(@"file", url, ^(id response, error) {
            callback(response.data, error);
        });
    };
}
//处理头像
- (RJAsyncClosure)makeEffect:(UIImage *)image{
    return ^(RJAsyncCallback callback){
        make(image, ^(id data, error) {
            callback(data, error);
        });
    };
}
(2)以同步方式编写代码
- (void)onLogin:(id)sender {
    [ProgressHud show];
    rj_async(^{
        NSDictionary *login_josn = rj_yield( [self loginWithAccount:@"112233" pwd:@"12345"] );
        NSDictionary *query_json = rj_yield( [self queryInfoWithUid:login_josn[@"uid"] token:login_josn[@"token"]] );
        UIImage *image = rj_yield( [self downloadHeadImage:query_json[@"url"]] );
        NSString *beautiful_image = rj_yield( [self makeEffect:image] );
        NSLog(@"all done");
        //进入详情界面
     })
    .error(^(id error) {
        NSLog(@"error happened");
    })
    .finally(^{
        [ProgressHud dismiss];
    });
}

rj_async块内部完全以同步方式编写,通过将异步任务包装进rj_yield(),rj_async会自动以异步方式调度它们,不会阻塞主流程,从主观感受上,它们就像同步代码一样,功能逻辑也比较清晰。

rj_async块内部运行在主线程,可以直接在块内部进行UI操作。

这里async的含义并不是启动子线程来执行块,而是块内部以异步方式调度。异步意味着不阻塞,异步不一定就是子线程。

RJIterator兼容PromiseKit。如果已有自己的Promise,可以在异步块内直接传给rj_yield(),它会被正确异步调度,但是只支持AnyPromise。如果不是AnyPromise,如果可以转换的话,使用PromiseKit提供的相关方法转换为AnyPromise后再使用。

对比普通回调方式编写代码

如果以普通回调方式,则无论怎样都无法逃出以下模式:

- (void)loginWithAccount:(NSString *)account pwd:(NSString *)pwd callback:(void (^)(id value, id error))callback {
    post(@"/login", account, pwd, ^(id response, error) {
        callback(response.data, error);
    });
}
- (void)queryInfoWithUid:(NSString *)uid token:(NSString *)token  callback:(void (^)(id value, id error))callback{
    get(@"query", uid, token, ^(id response, error) {
        callback(response.data, error);
    });
}
- (void)downloadHeadImage:(NSString *)url callback:(void (^)(id value, id error))callback{
    get(@"file", url, ^(id response, error) {
        callback(response.data, error);
    });
}
- (void)makeEffect:(UIImage *)image callback:(void (^)(id value, id error))callback{
    make(image, ^(id data, error) {
        callback(data, error);
    });
}

- (void)onLogin:(id)sender {
    [ProgressHud show];
    [self loginWithAccount:@"112233" pwd:@"112345" callback:^(id value, id error) {
        if (error) {
            [ProgressHud dismiss];
            NSLog(@"Error happened:%@", error);
        }
        else {
            NSDictionary *json = (NSDictionary *)value;
            [self queryInfoWithUid:json[@"uid"] token:json[@"token"] callback:^(id value, id error) {
                if (error) {
                    [ProgressHud dismiss];
                    NSLog(@"Error happened:%@", error);
                }
                else {
                    NSDictionary *json = (NSDictionary *)value;
                    [self downloadHeadImage:json[@"url"] callback:^(id value, id error) {
                        if (error) {
                            [ProgressHud dismiss];
                            NSLog(@"Error happened:%@", error);
                        }
                        else {
                            UIImage *image = (UIImage *)value;
                            [self makeEffect:image callback:^(id value, id error) {
                                if (error) {
                                    [ProgressHud dismiss];
                                    NSLog(@"Error happened:%@", error);
                                }
                                else {
                                    [ProgressHud dismiss];
                                    UIImage *image = (UIImage *)value;
                                    /*
                                     All done
                                     */
                                }
                            }];
                        }
                    }];
                }
            }];
        }
    }];
}

这时onLogin方法就掉进了传说中的回调地狱。

对比Promise链
[ProgressHud show];

[self loginWithAccount:@"112233" pwd:@"12345"].promise
.then(^(NSDictionary *json) {
    return [self queryInfoWithUid:json[@"uid"] token:json[@"token"]].promise;
})
.then(^(NSDictionary *json) {
    return [self downloadHeadImage:json[@"url"]].promise;
})
.then(^(UIImage *image) {
    return [self makeEffect:image].promise;
})
.then(^(UIImage *image) {
    /*All done*/
})
.catch(^(id error) {
    NSLog(@"error happened");
})
.finally(^{
    [ProgressHud dismiss];
});

2.生成器与迭代器

生成器与迭代器的概念及用法。可以参考ES6教程

http://www.infoq.com/cn/articles/es6-in-depth-generators

http://es6.ruanyifeng.com/#docs/generator

在RJIterator中,满足以下条件的C/Objective-C/Swift方法的闭包即可以作为生成器

(1)返回值为id或void,接受最多8个id参数的OC类方法、实例方法、block;c函数;Swift类方法、实例方法。

(2)返回值为id,接受一个参数的Swift函数、闭包。

生成器不能直接调用,需要通过RJIterator类的初始化方法创建迭代器,再通过迭代器访问生成器。

- (id _Nonnull)initWithFunc:(RJGenetarorFunc _Nonnull)func arg:(id _Nullable)arg;
- (id _Nonnull)initWithTarget:(id _Nonnull)target selector:(SEL _Nonnull)selector, ...;
- (id _Nonnull)initWithBlock:(id _Nonnull)block, ...;
- (id _Nonnull)initWithTarget:(id _Nonnull)target selector:(SEL _Nonnull)selector args:(NSArray *_Nullable)args;
- (id _Nonnull)initWithBlock:(id _Nullable (^ _Nonnull)(id _Nullable))block arg:(id _Nullable)arg;
- (id _Nonnull)initWithStandardBlock:(dispatch_block_t _Nonnull)block;
低配版聊天机器人

假设talk是一个会说话的机器人,按一下它就会回一句。那么talk可以这样实现:

func talk(arg: Any?) -> Any? {
    rj_yield("Hello, How are you?");
    rj_yield("Today is Friday");
    rj_yield("So yestday is Thursday");
    rj_yield("And tomorrow is Saturday");
    rj_yield("Over");
    return "==talk done==";
}

在这个例子中,talk是一个生成器。每次调用都会返回“下一句话”。它会记住上一次说到哪里。调用的方式必须通过迭代器进行,因此首先创建谈的迭代器,然后通过next方法依次获取回答。

var it: RJIterator;
var r: RJResult;

it = RJIterator.init(withFunc: talk, arg: nil)
r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: Hello How are you?, done:NO

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: Today is Friday, done:NO

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: So yestday is Thursday, done:NO

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: And tomorrow is Saturday, done:NO

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: Over, done:NO

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: ==talk done==, done:YES

r = it.next()
print("value: \(r.value), done:\(r.done)")
//==> value: ==talk done==, done:YES

RJResult是迭代器RJIterator每次next返回的结果值的类型,其中value表示结果数据,done表示迭代是否结束,结束表示生成器内部已经执行了尾部的或者在某些地方返回了。

每次调用next,talk都会从rj_yield处返回,这可以看作是暂时中断talk,等到再次执行next时,talk会从上次中断的地方恢复继续执行。这种“切换”方式类似于协程,但是RJIterator不是一个完整的协程库。协程库大部分目的是提升服务端的性能,因此有效的协程调度非常重要,而RJIterator的核心在于实现yield原语和async块。

新的需求

感觉还不够好,比如想让机器人知道我的名字,以增进我们之间的感情。修改talk

func talk(name: Any?) -> Any? {
    rj_yield("Hello \(name), How are you?");
    rj_yield("Today is Friday");
    rj_yield("So yestday is Thursday");
    rj_yield("And tomorrow is Saturday");
    rj_yield("Over");
    return "==talk done==";
}

并在创建迭代器时传递参数。

it = RJIterator.init(withFunc: talk, arg: "乌卡卡")

这时候第一次调用next,将如下返回

value: Hello 乌卡卡, How are you?, done:NO
更新的需求

在第5次调用next和talk对话时,它回答“Over”,然后再次迭代(第6次)就会结束。但如果还想继续聊天,可以在第6次迭代时,传递命令告诉机器人再次开始。

修改talk

fileprivate func talk(name: Any?) -> Any? {
    var cmd = ""
    repeat {
        rj_yield("Hello \(name ?? ""), How are you?");
        rj_yield("Today is Friday");
        rj_yield("So yestday is Thursday");
        rj_yield("And tomorrow is Saturday");
        cmd = rj_yield("Over") as? String ?? "";
    }while cmd != "Over"
    
    return "==talk done==";
}

第6次调用next时传递值

r = it.next("again")
print("value: \(r.value), done:\(r.done)")
//value: value: Hello 乌卡卡, How are you?, done:NO

如果不传递这个值,talk将按正常流程结束。但是传入“again”后,它将从头开始。也就是说,生成器talk除了具有“中断+返回”的能力外,还拥有多次“传入值”的能力。其中原理是

给next传的值将作为生成器内部上次rj_yield的新返回值,并在生成器“苏醒”的时候赋值给左边的"cmd"。如果next未传递参数,则该返回值就是rj_yield()本身封装的值。通过这个特性,可以基于生成器和迭代器变种出新功能,rj_async块就是基于这个原理。
生成器嵌套

生成器内部可以嵌套调用其他生成器,例如要从1数到9,感觉工作量太大,所以定义了3个生成器,每个负责数3个数,再定义一个总生成器,内部调用这三个小生成器。

func count_1_3(_: Any?) -> Any? {
    rj_yield(1)
    rj_yield(2)
    return 3
}
func count_4_5(_: Any?) -> Any? {
    rj_yield(4)
    rj_yield(5)
    return 6
}
func count_7_9(_: Any?) -> Any? {
    rj_yield(7)
    rj_yield(8)
    return 9
}

func count(_: Any?) -> Any? {
    rj_yield(RJIterator.init(withFunc: count_1_3, arg: nil))
    rj_yield(RJIterator.init(withFunc: count_4_5, arg: nil))
    rj_yield(RJIterator.init(withFunc: count_7_9, arg: nil))
    return nil
}

为count创建迭代器,连续执行next将得到value: 1, 2, 3, 4, 5, 6, 7, 8, 9

可能

苹果文档透露Swift以后的版本可能会新增异步和多任务方面的新特性,因此以后的Swift可能会像JavaScript一样支持async、yield、await等功能。

安装

pod

pod "RJIterator", "~> 1.1.3"

手动

RJIterator基于MRC管理内存,包含一个Swift文件,因此还要手动添加并更改配置,新增Bridge Header,比较麻烦,建议使用pod。