RxSwift by Examples 分成 4 部分。以下是 PART 4 的學(xué)習(xí)筆記和翻譯整理。原文在這里触幼。
當(dāng)我們談?wù)?Rx 的時(shí)候,常常歸結(jié)為連接數(shù)據(jù)源與 UI究飞。
在這個(gè)系列教程的此前部分置谦,除了 UI 綁定堂鲤,我們還講到了獲取數(shù)據(jù)。當(dāng)從服務(wù)端獲取到數(shù)據(jù)的時(shí)候媒峡,常常要解析它瘟栖。如果數(shù)據(jù)量很大,map 的任務(wù)就要消耗內(nèi)存和時(shí)間谅阿,尤其是當(dāng)操作在主線程中進(jìn)行時(shí)半哟,會(huì)阻塞 UI,導(dǎo)致給最終產(chǎn)品造成糟糕的用戶體驗(yàn)签餐。
在 part 3 我們 map 了對(duì)象寓涨。在一些操作器中使用 MainScheduler.instance,因?yàn)闉榱舜_保我們的數(shù)據(jù)在主線程中氯檐。事實(shí)上戒良,這是一個(gè) Scheduler,不是一個(gè) Thread男摧,那么為什么我們要討論線程呢蔬墩?而且我們只是認(rèn)識(shí)到不應(yīng)該在主線程 map 對(duì)象译打,但是似乎我們最后的案例這樣做了耗拓。為什么?
你可以在 part 1 中找到答案奏司。
調(diào)度者 Scheduler
開(kāi)始我們先討論一點(diǎn)關(guān)于 scheduler 的理論知識(shí)乔询。當(dāng)我們用 Rx 做操作時(shí),理論上所有操作都在一個(gè)線程上韵洋。只要你沒(méi)有手動(dòng)改變線程竿刁,當(dāng)前線程的入口也就是所執(zhí)行的線程。
scheduler 并不真的是線程搪缨,但如它的名字所表示的一樣食拜,它們調(diào)度所得到的任務(wù)。有兩種 scheduler:串行和并行(serial and concurrent)副编。以下列出的是已經(jīng)內(nèi)置的 scheduler:
- CurrentThreadScheduler(串行) - 安排在當(dāng)前線程负甸,也是默認(rèn) scheduler
- MainScheduler(串行) - 安排在主線程
- SerialDispatchQueueScheduler(串行) - 安排在指定隊(duì)列(dispatch_queue_t)
- ConcurrentDispatchQueueScheduler(并行) - 安排在指定隊(duì)列(dispatch_queue_t)
- OperationQueueScheduler(并行) - 安排在指定隊(duì)列(NSOperationQueue)
有意思的是鳄厌,當(dāng)你傳遞一個(gè)并行隊(duì)列到一個(gè) 串行的 scheduler句旱, RxSwift 會(huì)將它轉(zhuǎn)換成串行隊(duì)列。相反地趁怔,傳遞串行隊(duì)列到并行 scheduler 不會(huì)有任何問(wèn)題队腐,不過(guò)如果可以最好避免這樣做蚕捉。
你還可以實(shí)現(xiàn)你自己的 scheduler,這個(gè)文檔會(huì)對(duì)你有幫助如果需要這樣做的話柴淘。
observeOn() & subscribeOn()
這兩個(gè)方法是多線程的核心迫淹。從它們的命名應(yīng)該能看出來(lái)它們是做什么的秘通。事實(shí)上,很少人理解兩者之間的區(qū)別以及使用時(shí)的具體行為敛熬。
暫時(shí)忘掉這兩個(gè)詞充易。假設(shè)我們接到 Emily 的電話,她曾請(qǐng)我們幫她找看她的貓咪 Ethan 在她度假的時(shí)候≥┬停現(xiàn)在她回來(lái)了盹靴,我們需要把 Ethan 還回去。我們要駕車數(shù)小時(shí)才能到她家瑞妇,最好為此做點(diǎn)準(zhǔn)備稿静。
通常我們開(kāi)車去 Emily 家默認(rèn)會(huì)走公路。不過(guò)今天我們想為我們的生活做一點(diǎn)改變辕狰,我們選擇雙車道的高速路改备。天氣很不錯(cuò),開(kāi)了一小時(shí)后我們停下來(lái)呼吸新鮮空氣蔓倍。突然一個(gè)念頭浮起——這樣的天氣里在公路上開(kāi)車會(huì)很棒悬钳。因?yàn)樾迈r空氣對(duì)我們總是有壞的影響。于是我們決定回到老公路上偶翅。伴隨著好聽(tīng)的音樂(lè)默勾,可愛(ài)的貓和美妙的天氣,開(kāi)了一小時(shí)后我們終于見(jiàn)到 Emily聚谁,把貓交給她母剥,每個(gè)人都很高興。我們已經(jīng)學(xué)習(xí)了 observeOn() 和 subscribeOn()形导。
我們是一個(gè) Observable环疼,Ethan 是我們產(chǎn)生的信號(hào) Signal,路線是一個(gè) Scheduler朵耕,Emyly 是一個(gè) Observer炫隶。Emily 訂閱了我們,她相信她會(huì)得到一個(gè)信號(hào)(貓)阎曹。當(dāng)開(kāi)車去 Emily 家時(shí)我們也有一個(gè)默認(rèn)路線伪阶。不過(guò)開(kāi)始的時(shí)候我們走了另一條路線(scheduler)。當(dāng)你開(kāi)始旅程的時(shí)候不用默認(rèn)路線而選擇不同的路芬膝,你使用了 subscribeOn() 方法望门。如果你使用了 subscribeOn(),你不能確定旅程結(jié)束的時(shí)候(Emily 用了 subscribeNext())所在的路與開(kāi)始的路一致锰霜。你只能確保你從哪兒開(kāi)始筹误。
第二個(gè)方法,observeOn() 可以改變路線癣缅。不過(guò)它不限制旅途的起點(diǎn)厨剪,在旅途中任何時(shí)候你可以使用 observeOn() 切換路線哄酝。作為對(duì)比,subscribeOn() 只在起點(diǎn)的時(shí)候切換路線 - 這就是不同之處祷膳。然而大部分時(shí)候你將使用 observeOn()陶衅。
回到貓的傳遞,在 RxSwift 中用偽代碼表達(dá)我們的傳遞是這樣:
catObservable // 1
.breatheFreshAir() // 2
.observeOn(MainRouteScheduler.instance) // 3
.subscribeOn(TwoLaneFreewayScheduler.instance) // 4
.subscribeNext { cat in // 5
if cat is Ethan {
hug(cat)
}
}
.addDisposableTo(disposeBag)
分步講解:
- 我們訂閱了 observable 貓直晨,它發(fā)送貓信號(hào)
- 在訂閱之前我們?cè)谕粋€(gè) scheduler(Rx 的默認(rèn)行為)搀军。
- 切換 sheduler 至 MainRouteScheduler。在這之下的所有操作都將安排到 MainRouteScheduler(如果之后不改變的話)勇皇。
- 我們?cè)?TwoLaneFreewayScheduler 啟動(dòng)罩句,所以 breatheFreshAir() 將被安排在 TwoLaneFreewayScheduler。然后用 observeOn() 改變 sheduler敛摘。
- subscribeNext() 被安排到 MainRouteScheduler门烂。如果在這行之前我們沒(méi)有添加 observeOn(),它會(huì)被安排到 TwoLaneFreewayScheduler兄淫。
總結(jié):subscribeOn() 指向整個(gè)鏈條的開(kāi)始點(diǎn)屯远,observeOn() 指向下一個(gè)去向。(看原文的圖捕虽,不轉(zhuǎn)了)慨丐。
示例
創(chuàng)建一個(gè) project。用 Cocoapods 安裝依賴庫(kù)薯鳍。
Podfine
platform :ios, '8.0'
use_frameworks!
target 'RxAlamofireExample' do
pod 'RxAlamofire/RxCocoa'
pod 'ObjectMapper'
end
列一下任務(wù)大綱:
- 創(chuàng)建 UI咖气。UISearchBar 和 UITableView挨措。
- 觀察 search bar挖滤,每一次它的值改變,轉(zhuǎn)換成 repo 的 array浅役。這里需要 model 做網(wǎng)絡(luò)請(qǐng)求斩松。
- 用新數(shù)據(jù)去更新 table view。思考關(guān)于 scheduler觉既,考慮不要拖累到 UI惧盹。
第1步 - Controller 和 UI
創(chuàng)建 RepositoriesViewController.swift
import UIKit
import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift
class RepositoriesViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
func setupRx() {
}
}
創(chuàng)建可觀察的 search bar 的 rx.text 屬性。不過(guò)這一次加個(gè)過(guò)濾器:我們不想要空值瞪讼。
class RepositoriesViewController: UIViewController {
...
var rx_searchBarText: Observable<String> {
return searchBar
.rx.text
.filter { $0.characters.count > 0 } // notice the filter new line
.throttle(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
}
...
}
我們剛剛添加了一個(gè)變量到 RepositoryViewController【現(xiàn)在我們需要連接轉(zhuǎn)換過(guò)的 Observable<[Repository]> 并傳給 UITableView。
第2步 - Network model and mapping objects
首先設(shè)置 map 對(duì)象符欠。這一次我們用不一樣的 mapper嫡霞。創(chuàng)建 Repository.swift
import ObjectMapper
class Repository: Mappable {
var identifier: Int!
var language: String!
var url: String!
var name: String!
required init?(_ map: Map) { }
func mapping(map: Map) {
identifier <- map["id"]
language <- map["language"]
url <- map["url"]
name <- map["name"]
}
}
我們有一個(gè) controller,還有一個(gè) Repository 對(duì)象的 model∠J粒現(xiàn)在實(shí)現(xiàn) network model诊沪。
我們將初始化這個(gè) model 為 Observable<String>养筒,實(shí)現(xiàn)方法返回 Observable<[Repository]>。然后連接到 RepositoriesViewController 的 view端姚。初步實(shí)現(xiàn) RepositoryNetworkModel.swift:
import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift
struct RepositoryNetworkModel {
private var repositoryName: Observable<String>
private func fetchRepositories() -> Driver<[Repository]> {
...
}
}
為什么返回的不是 Observable<[Repository]> 而是 Driver<[Repository]>晕粪?
今天我們要討論的是 Scheduler。當(dāng)你想綁定數(shù)據(jù)到 UI渐裸,總是想用 MainScheduler 來(lái)做這件事巫湘。基本上這是一個(gè) Driver 的角色昏鹃。Driver 是一個(gè) Variable剩膘,它說(shuō):好的伙計(jì),我就在主線程上所以別猶豫了綁定我吧盆顾。這個(gè)方法使我們的綁定不易出錯(cuò)怠褐,實(shí)現(xiàn)安全的連接。
我們從 flatMapLatest() 開(kāi)始您宪,把 Observable<String> 轉(zhuǎn)換成 Observable<[Repositories]>:
struct RepositoryNetworkModel {
...
private func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.flatMapLatest { text in
return RxAlamofire
.requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.map { (response, json) -> [Repository] in
if let repos = Mapper<Repository>().mapArray(json) {
return repos
} else {
return []
}
}
}
...
}
看起來(lái)不太一樣奈懒,這兒有另一個(gè) map() 添加到 flatMapLatest() 中。但事實(shí)上沒(méi)有你需要擔(dān)心的宪巨。在 flatMapLatest() 中做了常規(guī)的網(wǎng)絡(luò)請(qǐng)求磷杏,如果出現(xiàn) error 則用 Observable.never() 中斷傳輸管道。然后把從 Alamofire 得到的響應(yīng) map 成 Observable<[Repository]>捏卓。我們也可以鏈?zhǔn)?flatMapLatest() 的在 catchError() 之后极祸,不過(guò)我們需要它在 flatMapLatest() 外部,這只是個(gè)偏好問(wèn)題怠晴。
上門這個(gè)代碼無(wú)法編譯遥金,因?yàn)槲覀兎祷亓?Observable,然而我們希望返回 Driver蒜田。所以我們需要更深入稿械。如何轉(zhuǎn)換 Observable<[Repository]> 成 Driver<[Repository]>?非常簡(jiǎn)單冲粤。之需要用 asDriver() 操作器就可以把任何 Observable 可以轉(zhuǎn)換成 Driver美莫。在這個(gè)示例中,我們將使用 .asDriver(onErrorJustReturn: [])梯捕,它的意思是:如果鏈條中有任何錯(cuò)誤(很可能沒(méi)有厢呵,因?yàn)槲覀冊(cè)诖酥稗D(zhuǎn)換它了),返回空數(shù)組傀顾。這是代碼:
struct RepositoryNetworkModel {
...
private func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.flatMapLatest { text in
return RxAlamofire
.requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.map { (response, json) -> [Repository] in
if let repos = Mapper<Repository>().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
...
}
看襟铭,我們甚至沒(méi)有用到 observeOn() 或 subscribeOn(),但已經(jīng)兩次切換了 scheduler。第一次用 throttle()蝌矛,現(xiàn)在用 asDriver()(它確保我們?cè)?MainScheduler) - 這只是個(gè)開(kāi)始〉琅現(xiàn)在代碼可以執(zhí)行了。最后我們要做的事情是連接 RepositoryNetworkModel 中的 repositories 到 view controller入撒。不過(guò)在此之前先用其他東西替換這個(gè)方法隆豹,因?yàn)檫@樣我們每次用它的時(shí)候會(huì)創(chuàng)建新的管道。取而代之茅逮,我更喜歡屬性璃赡。但不是一個(gè)計(jì)算屬性,因?yàn)榻Y(jié)果將和一個(gè)方法一樣献雅。我們將創(chuàng)建一個(gè) lazy var碉考,它將被約束到獲取 repositories 的方法。這種方式避免多次創(chuàng)建序列挺身。我們還需要隱藏不是屬性的氣體東西侯谁,確保任何使用這個(gè) model 的人可以得到正確的 Driver 屬性。這個(gè)方案的唯一麻煩是我們不得不在 init 中明確類型章钾。最終 RepositoryNetworkModel.swift 如下:
struct RepositoryNetworkModel {
lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
private var repositoryName: Observable<String>
init(withNameObservable nameObservable: Observable<String>) {
self.repositoryName = nameObservable
}
private func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.flatMapLatest { text in
return RxAlamofire
.requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.map { (response, json) -> [Repository] in
if let repos = Mapper<Repository>().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
}
很好∏郊現(xiàn)在我們只要連接數(shù)據(jù)到 view controller。我們要綁定 Driver 到 table view贱傀,不用 bindTo(之前用過(guò))惨撇,而用 drive() 操作器,語(yǔ)法和其他跟 bindTo 一樣府寒。為了綁定數(shù)據(jù)到 table view魁衙,我們還做了另一個(gè)訂閱,每次 repositories 的 count 等于 0 時(shí)株搔,顯示一個(gè) alert剖淀。
RepositoriesViewController.swift:
class RepositoriesViewController: UIViewController {
@IBOutlet weak var tableViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
let disposeBag = DisposeBag()
var repositoryNetworkModel: RepositoryNetworkModel!
var rx_searchBarText: Observable<String> {
return searchBar
.rx_text
.filter { $0.characters.count > 0 }
.throttle(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
}
override func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
func setupRx() {
repositoryNetworkModel = RepositoryNetworkModel(withNameObservable: rx_searchBarText)
repositoryNetworkModel
.rx_repositories
.drive(tableView.rx_itemsWithCellFactory) { (tv, i, repository) in
let cell = tv.dequeueReusableCellWithIdentifier("repositoryCell", forIndexPath: NSIndexPath(forRow: i, inSection: 0))
cell.textLabel?.text = repository.name
return cell
}
.addDisposableTo(disposeBag)
repositoryNetworkModel
.rx_repositories
.driveNext { repositories in
if repositories.count == 0 {
let alert = UIAlertController(title: ":(", message: "No repositories for this user.", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
if self.navigationController?.visibleViewController?.isMemberOfClass(UIAlertController.self) != true {
self.presentViewController(alert, animated: true, completion: nil)
}
}
}
.addDisposableTo(disposeBag)
}
}
這段代碼唯一的新事物是 driveNext() 操作器。不過(guò)你可以認(rèn)為它只是一個(gè) Driver 的 subscribeNext邪狞。
第3步 - 多線程優(yōu)化
你可能認(rèn)為事實(shí)上所有事情都在 MainScheduler 中完成祷蝌。為什么?因?yàn)槲覀兊逆湕l從 searchBar.rx.text 中開(kāi)始帆卓,并保證這個(gè)是在 MainScheduler 中。因?yàn)樗衅渌露荚诋?dāng)前 scheduler 我們的 UI 線程可能會(huì)不堪重負(fù)米丘。如何避免這種情況剑令?在 request 和 map 之前切換到背景線程,僅在更新 UI 的時(shí)候回主線程:
RepositoryNetworkModel.swift
struct RepositoryNetworkModel {
...
private func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMapLatest { text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.map { (response, json) -> [Repository] in // again back to .Background, map objects
if let repos = Mapper<Repository>().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
...
}
為什么兩次一樣的方法使用 observeOn()拄查?因?yàn)槲覀儾⒉淮_切地知道 requestJSON 是否將在與它啟動(dòng)時(shí)的同一個(gè)線程返回?cái)?shù)據(jù)吁津,為了確定它在背景線程 map。
現(xiàn)在我們的 map 是在背景線程了,結(jié)果傳遞給 UI 線程碍脏。還可不可以做得更多一些梭依?我們希望用戶知道網(wǎng)絡(luò)請(qǐng)求正在進(jìn)行。為了達(dá)到這個(gè)目的典尾,我們將使用 UIApplication.sharedApplication().networkActivityIndicatorVisible 屬性役拴,顯示旋轉(zhuǎn)的菊花。不過(guò)現(xiàn)在我們必須小心對(duì)待線程钾埂,因?yàn)槲覀兿朐?request 和 mapping 操作的中間更新 UI河闰。我們將使用一個(gè)優(yōu)雅的方法叫做 doOn(),它可以做任何你置頂?shù)氖录ū热?.Next, .Error 等)褥紫。我們想在 flatMapLatest():之前顯示句話姜性,doOn是可以做到的。我們只需要在動(dòng)作執(zhí)行前切換到 MainScheduler髓考。
完整的獲取 repo 的代碼如下:
RepositoryNetworkModel.swift:
struct RepositoryNetworkModel {
lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
private var repositoryName: Observable<String>
init(withNameObservable nameObservable: Observable<String>) {
self.repositoryName = nameObservable
}
private func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.subscribeOn(MainScheduler.instance) // Make sure we are on MainScheduler
.doOn(onNext: { response in
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
})
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMapLatest { text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.map { (response, json) -> [Repository] in // again back to .Background, map objects
if let repos = Mapper<Repository>().mapArray(json) {
return repos
} else {
return []
}
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOn(onNext: { response in
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
}
現(xiàn)在你知道為什么當(dāng)解析的時(shí)候不需要關(guān)心線程問(wèn)題:Moya-ModelMapper 的 extension 為我們切換了 scheduler部念。