RxSwift教程(二)

从“以时间为索引的常量队列”开始 - Observable

第一个要介绍的,就是我们在之前的例子中提到的“以时间为索引的常量队列”。在RxSwift里,这种概念叫做Observable。在ReactiveX对Observable的说明中,我们可以看到一张这样的图:

Snip20180716_2.png

其中,最上面的一排,就是一个Observable。从左到右,表示时间由远及近的流动过程。上面的每一个形状,就表示在“某个时间点发生的事件”,而最右边的竖线则表示事件成功结束。因此,我们之前过滤用户输入的例子,也可以表示成这样:


Snip20180716_3.png

看到这里,你可能已经有点儿着急了,你说的这些我都明白,代码呢?该怎么写呢?先别急,我们再来看一个概念:Operator。

理解operators

在ReactiveX官网可以看到,operators分为两大类:一类用于创建Observable;


Snip20180716_4.png

这些不同的创建方法可以针对不同的事件流类型生成Observable。另一类是接受Observable作为参数,并返回一个新的Observable;


Snip20180716_5.png

它们有点儿类似我们对集合类型使用的各种变形方法,用于在序列中找到我们希望处理的事件,稍后,我们就会看到一个更具体的例子。

创建一个事件序列

接下来,我们先看一些简单的创建Observable的方法。用任何一种之前我们提过的方法创建一个包含RxSwift的项目

import RxSwift

Observable.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
// Or

Observable.from(["1", "2", "3", "4", "5", "6", "7", "8", "9"])

这里,我们就用了两个operator:

  • of:用固定数量的元素生成一个Observable
  • from:用一个Sequence类型的对象创建一个Observable;

但这两个operator返回的结果是一样的,都是一个包含字符1-9的Observable:


Snip20180716_6.png

对事件序列进行处理

定义好事件序列之后,我们就可以处理过滤偶数的需求了。之前我们提到过,对序列的加工是通过另外一类operator完成的。思路,和我们之前过滤数组是一样的,先把Observable中的元素都变成整数:

_ = Observable.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
        .map { Int($0) }

这里,map就是一个可以对Observable中的元素变形的operator,它返回一个新的Observable对象。因此,我们可以把多个这种加工Observable的方法串联起来

例如,进一步在变形后的Observable中,找出所有偶数:

_ = Observable.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
    .map { Int($0) }
    .filter { $0 != nil && $0! % 2 == 0 }

这样,filter这个operator返回的,就是一个只包含偶数的Observable了。
尽管上面map和filter这两个operator和集合中的map和filter方法非常类似,但它们执行的逻辑却截然不同。
调用集合类型的map和filter方法,表达的是同步执行的概念,在调用方法的同时,集合就被立即加工处理了。

但我们创建的Observable,表达的是异步操作。Observable中的每一个元素,都可以理解为一个异步发生的事件。因此,当我们对Observable调用map和filter方法时,只表示我们要对事件序列中的元素进行处理的逻辑,而并不会立即对Observable中的元素进行处理。

为了验证这个结论,我们可以在筛选偶数的时候打印个消息:

_ = Observable.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
    .map { Int($0) }
    .filter {
        if let item = $0, item % 2 == 0 {
            print("Even: \(item)")
            return true
        }

        return false
    }

然后,执行swift build编译,当我们执行编译结果的时候,不会在控制台上打印任何消息。也就是说,我们没有实际执行任何的筛选逻辑。

如何“订阅”事件?

那么,真正的筛选是什么时候发生的呢?答案是,真的有人对这个事件感兴趣的时候。也就是说,有人“订阅”这个Observable中的事件的时候,像这样:

var evenNumberObservable =
    Observable.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
        .map { Int($0) }
        .filter {
            if let item = $0, item % 2 == 0 {
                print("Even: \(item)")
                return true
            }

            return false
        }

evenNumberObservable.subscribe { event in
    print("Event: \(event)")
    }

这表示的,就是我们“从头至尾”关注了evenNumberObservable这个序列中的所有事件。重新编译执行一下,就可以看到筛选的过程和结果了,我们关注到了这个筛选事件中的所有偶数:


Snip20180716_8.png

那么,除了“全程关注”的人之外,还有一类是“半路来的人”,对于这些人,就只能看到他们关注到evenNumberObservable之后,发生的事件了,我们可以用下面的代码,来理解这个场景:

evenNumberObservable.skip(2).subscribe { event in
    print("Event: \(event)")
}

这里,我们用了另外一个operator skip来模拟“半路关注”的情况。skip(2)可以让订阅者“错过”前2次发生的事件。此时,对这个订阅者而言,他就完全不知道之前还过滤出了2个偶数,他看到的结果就是这样的:


Snip20180716_9.png

把这两次订阅放在一个图里,就是这样:


Snip20180716_10.png

通过这两个例子,我们要表达的最重要的一个思想,就是Observable中的每一个元素都表示一个“异步发生的事件”这样的概念,operator对Observable的加工是在订阅的时候发生的。这种只有在订阅的时候才emit事件的Observable,有一个专有的名字,叫做Cold Observable。

言外之意,就是也有一种Observable是只要创建了就会自动emit事件的,它们叫做Hot Observable。在后面的内容中,我们会看到这类事件队列的用法。

subscribe也是一个operator

在刚才的例子里,还有一点值得我们注意的就是在结尾,会有一个Event: completed。这就是在我们之前Observable示意图中的竖线,表示这个Observable事件流成功结束了。

实际上,我们使用的subscribe,也是一个operator,用于把事件的订阅者(Observer)和事件的产生者(Observable)关联起来。而Observer和Observable之间,有着下面的约定:

  • 当Observable正常发送事件时,会调用Observer提供的onNext方法,这个过程习惯上叫做emissions
  • 当Observable成功结束时,会调用Observer提供的onCompleted方法;因此,在最后一次调用onNext之后,就会调用onCompleted;
  • 当Observable发生错误时,就会调用Observer提供的onError方法,并且,一旦发生错误,就不会再继续发送其它事件了。对于调用onComplete和onNext的过程,习惯上叫做notifications;

在RxSwift里,还有一个约定,叫做onDisposed,指的是Observable使用的资源被回收的时候,会调用Observer提供的onDisposed方法。那么,究竟什么是dispose呢?为什么我们需要它?这都要从Observable的类型说起

理解Observable dispose

一直以来,我们使用的Observable都是有限序列,对evenNumberObservable来说,当它emit了所有的数字之后,就自动结束了,此时,它占用的资源就会被回收,这很好理解。

但事情并不总是如此,有些事件队列是无限的,例如像下面这样:

_ = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
    .subscribe(onNext: { print("Subscribed: \($0)") })
    
dispatchMain()

我们用interval在主线程定义了一个每隔1秒发送一个整数的事件序列。为了让它可以一直emit事件,最后,我们调用了dispatchMain让程序保持不退出。
然后编译执行


Snip20180716_11.png

可以想到,如果我们不强制退出,这会是一个一直执行下去的事件序列。它在程序退出之前,会一直占用着系统资源,在绝大多数时候,这不是我们想要的结果。对于这种无限事件序列,如果我们要在不用的时候回收掉它的资源,就得这样

例如,假设5秒后,这个事件序列就不再需要了

public func delay(_ delay: Double,
    closure: @escaping (Void) -> Void) {

    DispatchQueue.main.asyncAfter(
        deadline: .now() + delay) {
            closure()
        }
}

let disposable =
    Observable<Int>.interval(1, scheduler: MainScheduler.instance)
        .subscribe(
            onNext: { print("Subscribed: \($0)") },
            onDisposed: { print("The queue was disposed.") })

delay(5) {
    disposable.dispose()
}

在上面的代码里:

  • 首先,我们定义了一个helper function delay,它可以在特定时间之后,执行我们指定的closure
  • 其次,我们把subscribe的返回值保存在了一个叫做disposable的变量里,我们可以把它理解为是一个订阅对象,我们可以通过这个对象,来取消订阅。同时,我们还指定了onDisposed参数,用来在事件序列被回收时,获得通知;
  • 最后,当我们使用disposable.dispose()的时候,就表示我们要“退订”这个计时事件了。退订后,原来的事件序列就不再继续emit事件了,它占用的资源也会被回收;

编译执行一下,就会看到执行5次后就停止了

但通常,这样单独对subscribe的返回值调用dispose()方法并不是一个好的编码习惯。RxSwift提供了一个类似RAII的机制,叫做DisposeBag,我们可以把所有的订阅对象放在一个DisposeBag里,当DisposeBag对象被销毁的时候,它里面“装”的所有订阅对象就会自动取消订阅,对应的事件序列的资源也就被自动回收了。为了理解这个用法,我们可以把之前的代码改成这样:

var bag = DisposeBag()

Observable<Int>.interval(1, scheduler: MainScheduler.instance)
    .subscribe(
        onNext: { print("Subscribed: \($0)") },
        onDisposed: { print("The queue was disposed.") })
    .disposed(by: bag)

delay(5) {
    bag = DisposeBag()
}

这次,我们直接串联了subscribe的返回值,调用disposed(by:)方法,把返回的订阅对象直接“装”在了bag里。并且,在delay的closure参数里,我们通过让bag等于一个新的DisposeBag对象,模拟了DisposeBag对象被销毁的场景。编译执行一下,就能看到和之前同样的结果了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • 转一篇文章 原地址:http://gank.io/post/560e15be2dca930e00da1083 前言...
    jack_hong阅读 912评论 0 2
  • 我从去年开始使用 RxJava ,到现在一年多了。今年加入了 Flipboard 后,看到 Flipboard 的...
    Jason_andy阅读 5,464评论 7 62
  • 在正文开始之前的最后,放上 GitHub 链接和引入依赖的 gradle 代码: Github: https://...
    松江野人阅读 5,889评论 0 1
  • 串行队列 VS 并行队列 GCD 中的队列是用来放置需要执行的任务的,任务的取出遵循队列的先进先出的原则。GCD ...
    iOS扫地僧阅读 1,930评论 0 2
  • 过年的时候,我和妈妈去了外婆家。妈妈有些晕车,从南方到北方,一路上不知道吐了多少次,她抱怨道:...
    流浪猫_阅读 386评论 1 3