RxSwift #03 | Subjects

Observable 是 RxSwift 的基礎(chǔ),但它們本質(zhì)上是只讀(read-only)的。你只能通過(guò)訂閱 observable,來(lái)獲得它們產(chǎn)生的新事件的通知。

舉個(gè)栗子:

Observable.create { observer in
    cache.calculateDiskStorageSize { result in
        switch result {
        case .success(let size):
            observer.onNext(Int64(size))
    case .failure(let error):
            observer.error(error)
      log.error("calculate cache failed with error:\\(error)")
    }
        observer.onCompleted()
    }
    return Disposables.create()
}

從上述代碼中可以看出,Observable 在創(chuàng)建的時(shí)候,就已經(jīng)確定了通過(guò)某種固定的邏輯,去發(fā)出事件,從而產(chǎn)生事件流。

上面這個(gè)例子中的固定邏輯,指的就是計(jì)算 cache 大小。通過(guò) cache 計(jì)算結(jié)果,來(lái)決定發(fā)出 onNext , error 或者 completable 事件。

而在日常開(kāi)發(fā)中,我們通常需要根據(jù)不同的邏輯,來(lái)決定發(fā)出事件。抽象來(lái)說(shuō),就是需要一個(gè)既能作為 Observable 又能作為 Observer 的東西。這種東西稱為 Subject:

let subject = PublishSubject<String>()

subject.on(.next("Is anyone listening?"))

let subscriptionOne = subject
  .subscribe(onNext: { string in
    print(string)
  })

上述代碼創(chuàng)建了一個(gè) PublishSubject, 它的名字很貼切:就像一個(gè)報(bào)紙出版商一樣,它接收信息,然后發(fā)布給訂閱者。

執(zhí)行上述代碼,會(huì)發(fā)現(xiàn)控制臺(tái)中沒(méi)有打印任何東西,這是因?yàn)椋?strong>PublishSubject 只給當(dāng)前的訂閱者發(fā)出事件,如果一個(gè) observer 是在事件發(fā)出之后才訂閱的,那么將不會(huì)收到任何事件。

什么是 Subject?

Subject 既是一個(gè) observable, 又是一個(gè) observer。在上面的例子中,可以看到 subject 既可以接收事件,又可以被訂閱。

Observable 和 Subject 的區(qū)別,除了 Subject 既可以作為 Observable,又可以作為 Observer以外,也可以這么理解:

Observable 已經(jīng)把各種事件都定好了,比如發(fā)送網(wǎng)絡(luò)請(qǐng)求,然后 Observer 在 subscribe 的時(shí)候,就觸發(fā)這個(gè)網(wǎng)絡(luò)請(qǐng)求,然后發(fā)送各種事件。

Subject 則是事件沒(méi)有定好,可以靈活地根據(jù)業(yè)務(wù)需求去進(jìn)行觸發(fā),比如選擇相片,比如發(fā)送網(wǎng)絡(luò)請(qǐng)求,然后發(fā)送各種事件。

在 RxSwift 中,有四種類型的 subject:

  • PublishSubject: 初始時(shí)是空的,只向訂閱者發(fā)出新元素。
  • BehaviorSubject: 有一個(gè)初始值,并將其初始值和最新的元素發(fā)送給新的訂閱者。
  • ReplaySubject: 初始化時(shí)需要有一個(gè)緩沖區(qū)大小,并將維持一個(gè)該大小的緩沖區(qū),緩沖區(qū)內(nèi)的元素都會(huì)發(fā)送給新的訂閱者。
  • AsyncSubject: 只發(fā)出序列中的最后一個(gè) next 事件,并且只在 subject 接收到 completed 事件時(shí)才發(fā)出。這是一種很少使用的 subject。

RxSwift 中還提供了一種叫做 Relay 的概念(在使用是需要 import RxRelay),RxSwift 中提供了兩種 relay:

  • PublishRelay
  • BehaviorRelay

這兩種 relay 包含著對(duì)應(yīng)的 subject, 但只能接收和轉(zhuǎn)發(fā) next 事件,不能添加 completed 或者 error 事件,所以它們對(duì)于非終止序列來(lái)說(shuō)是非常友好的。

Relay 只能 accept 事件,不能發(fā)送 completed 或者 error 等終止事件,因此沒(méi)有結(jié)束的概念。如果要正確釋放 replay, 需要把它添加到 disposeBag 中。

使用 PublishSubject

let subject = PublishSubject<String>()

subject.on(.next("Is anyone listening?"))

let subscriptionOne = subject
  .subscribe(onNext: { string in
    print(string)
  })

let subscriptionTwo = subject
  .subscribe { event in
    print("2)", event.element ?? event)
  }

subject.onNext("3")

/**
output:
3
2) 3
**/

subscriptionOne.dispose()
subject.onNext("4")

/**
output:
2) 4
**/

當(dāng)一個(gè) publish subject 收到一個(gè) completed 或 errror 事件,也就是終止事件,它將向新的訂閱者發(fā)出該終止事件,它將不再發(fā)出 next 事件。

而且,它將向后來(lái)的訂閱者重新發(fā)出其終止事件。(Subject 被終止后,如果還有 observer 去 subscribe 它,那么 subject 會(huì)重復(fù)給這些 observers 發(fā)送終止事件

// 1
subject.onCompleted()

// 2
subject.onNext("5")

// 3
subscriptionTwo.dispose()

let disposeBag = DisposeBag()

// 4
subject
  .subscribe {
    print("3)", $0.element ?? $0)
  }
  .disposed(by: disposeBag)

subject.onNext("?")

/**
output:
2) completed
3) completed
**/

使用 BehaviorSubject

頂部的第一行是 subject。

第二行的第一個(gè)訂閱者在 1 之后但在 2 之前訂閱,所以它在訂閱后立即收到 1,然后在主體發(fā)出 2 和 3 的時(shí)候收到。

同樣地,第二個(gè)訂閱者在 2 之后但在 3 之前訂閱,所以它在訂閱后立即收到 2,然后在 3 被發(fā)出時(shí)收到。

// 1
enum MyError: Error {
  case anError
}

// 2
func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
  print(label, (event.element ?? event.error) ?? event)
}

// 3
example(of: "BehaviorSubject") {
  // 4
  let subject = BehaviorSubject(value: "Initial value")
  let disposeBag = DisposeBag()

    subject
      .subscribe {
        print(label: "1)", event: $0)
      }
      .disposed(by: disposeBag

    /**
    1) Initial value
    **/

    subject.onNext("X")

    /**
    1) X
    **/

    // 1
    subject.onError(MyError.anError)

    // 2
    subject
      .subscribe {
        print(label: "2)", event: $0)
      }
      .disposed(by: disposeBag)

    /**
    1) anError
    2) anError
    **/
}

BehaviorSubject 向新的訂閱者重放他們的最新值。這使得它們很適合用來(lái)模擬各種狀態(tài)的轉(zhuǎn)移,比如“請(qǐng)求正在加載中”→“請(qǐng)求完成”。

那如果想要顯示比上一個(gè)值還更多的內(nèi)容呢,比如在搜索框上,需要顯示最近使用的五個(gè)搜索值,這個(gè)時(shí)候就要用到 ReplaySubject 了。

使用 ReplaySubject

ReplaySubject 將暫時(shí)緩存、或緩沖他們發(fā)出的最新元素,直到達(dá)到你選擇的指定大小。然后,他們將向新的訂閱者重新發(fā)出該緩沖區(qū)內(nèi)的元素。

下面的大理石圖描述了一個(gè)緩沖區(qū)大小為2的重放主體:

第一個(gè)訂閱者(中間一行)已經(jīng)訂閱了 replay subject(最上面一行),所以它在元素被發(fā)射出來(lái)的時(shí)候得到了元素。第二個(gè)訂閱者(底線)在 2 之后訂閱了,所以它得到了 1 和 2 的重放。

請(qǐng)記住,當(dāng)使用一個(gè) replay subject 時(shí),這個(gè)緩沖區(qū)是在內(nèi)存中保存的,所以很有可能會(huì)導(dǎo)致太高的內(nèi)存占用。比如你為某種類型的 replay subject 設(shè)置一個(gè)大的緩沖區(qū)大小,而這種類型的實(shí)例都會(huì)占用大量的內(nèi)存,比如圖像。

另一件需要注意的事情是創(chuàng)建一個(gè)數(shù)組類型的 replay subject。每個(gè)發(fā)射的元素將是一個(gè)數(shù)組,所以緩沖區(qū)的大小將緩沖那么多數(shù)組。如果不小心的話,也很容易在這里產(chǎn)生內(nèi)存壓力。

example(of: "ReplaySubject") {
  // 1
  let subject = ReplaySubject<String>.create(bufferSize: 2)
  let disposeBag = DisposeBag()

  // 2
  subject.onNext("1")
  subject.onNext("2")
  subject.onNext("3")

  // 3
  subject
    .subscribe {
      print(label: "1)", event: $0)
    }
    .disposed(by: disposeBag)

  subject
    .subscribe {
      print(label: "2)", event: $0)
    }
    .disposed(by: disposeBag)

/**
--- Example of: ReplaySubject ---
1) 2
1) 3
2) 2
2) 3
**/

    subject.onNext("4")
    subject.onError(MyError.anError)

    subject
      .subscribe {
        print(label: "3)", event: $0)
      }
      .disposed(by: disposeBag)

/**
前兩個(gè)訂閱者將正常接收當(dāng)前元素,因?yàn)楫?dāng)新元素被添加到主題時(shí),他們已經(jīng)被訂閱了,而新的第三個(gè)訂閱者將得到最后兩個(gè)緩沖的元素重放給它。
雖然最后訂閱流中發(fā)出了一個(gè) error 事件,但是緩沖區(qū)還在內(nèi)存中,所以它還會(huì)把緩沖區(qū)之前的元素發(fā)給訂閱者。

1) 4
2) 4
1) anError
2) anError
3) 3
3) 4
3) anError
**/
    }

subject.dispose()

// 因?yàn)?subject 在前面已經(jīng)發(fā)出了 error 事件,所以它被終止并且釋放了,這里再調(diào)用 dispose 會(huì)報(bào)錯(cuò)
// 3) Object `RxSwift...ReplayMany<Swift.String>` was already disposed.

使用 Relay

在前面的介紹中,我們知道:Relay 實(shí)際上是對(duì)應(yīng) Subject 的一層封裝——PublishRelay 是 PublishSubject 的封裝,BehaviorRelay 是 BehaviorSubject 的封裝。它和 Subject 不一樣的地方在于:它只能通過(guò) accept(_:) 方法接收并發(fā)出事件,它不能使用 onNext(_:) 發(fā)出事件,也不能使用 onCompleted() 或者 onError(_:) 去終止訂閱流,因此,Relay 保證了永遠(yuǎn)不會(huì)終止。

let relay = PublishRelay<String>()  
let disposeBag = DisposeBag()
relay.accept("Knock knock, anyone home?")
relay
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)

relay.accept("1")

// output: 1

relay.accept(MyError.anError)
relay.onCompleted()
// compile error

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,555評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,900評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,629評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,976評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評(píng)論 3 448
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,139評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,411評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,641評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,820評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,233評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,567評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,362評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,604評(píng)論 2 380