RxSwift by Examples 分成 4 部分。以下是 PART 4 的学习笔记和翻译整理。原文在这里。
当我们谈论 Rx 的时候,常常归结为连接数据源与 UI。
在这个系列教程的此前部分,除了 UI 绑定,我们还讲到了获取数据。当从服务端获取到数据的时候,常常要解析它。如果数据量很大,map 的任务就要消耗内存和时间,尤其是当操作在主线程中进行时,会阻塞 UI,导致给最终产品造成糟糕的用户体验。
在 part 3 我们 map 了对象。在一些操作器中使用 MainScheduler.instance,因为为了确保我们的数据在主线程中。事实上,这是一个 Scheduler,不是一个 Thread,那么为什么我们要讨论线程呢?而且我们只是认识到不应该在主线程 map 对象,但是似乎我们最后的案例这样做了。为什么?
你可以在 part 1 中找到答案。
调度者 Scheduler
开始我们先讨论一点关于 scheduler 的理论知识。当我们用 Rx 做操作时,理论上所有操作都在一个线程上。只要你没有手动改变线程,当前线程的入口也就是所执行的线程。
scheduler 并不真的是线程,但如它的名字所表示的一样,它们调度所得到的任务。有两种 scheduler:串行和并行(serial and concurrent)。以下列出的是已经内置的 scheduler:
- CurrentThreadScheduler(串行) - 安排在当前线程,也是默认 scheduler
- MainScheduler(串行) - 安排在主线程
- SerialDispatchQueueScheduler(串行) - 安排在指定队列(dispatch_queue_t)
- ConcurrentDispatchQueueScheduler(并行) - 安排在指定队列(dispatch_queue_t)
- OperationQueueScheduler(并行) - 安排在指定队列(NSOperationQueue)
有意思的是,当你传递一个并行队列到一个 串行的 scheduler, RxSwift 会将它转换成串行队列。相反地,传递串行队列到并行 scheduler 不会有任何问题,不过如果可以最好避免这样做。
你还可以实现你自己的 scheduler,这个文档会对你有帮助如果需要这样做的话。
observeOn() & subscribeOn()
这两个方法是多线程的核心。从它们的命名应该能看出来它们是做什么的。事实上,很少人理解两者之间的区别以及使用时的具体行为。
暂时忘掉这两个词。假设我们接到 Emily 的电话,她曾请我们帮她找看她的猫咪 Ethan 在她度假的时候。现在她回来了,我们需要把 Ethan 还回去。我们要驾车数小时才能到她家,最好为此做点准备。
通常我们开车去 Emily 家默认会走公路。不过今天我们想为我们的生活做一点改变,我们选择双车道的高速路。天气很不错,开了一小时后我们停下来呼吸新鲜空气。突然一个念头浮起——这样的天气里在公路上开车会很棒。因为新鲜空气对我们总是有坏的影响。于是我们决定回到老公路上。伴随着好听的音乐,可爱的猫和美妙的天气,开了一小时后我们终于见到 Emily,把猫交给她,每个人都很高兴。我们已经学习了 observeOn() 和 subscribeOn()。
我们是一个 Observable,Ethan 是我们产生的信号 Signal,路线是一个 Scheduler,Emyly 是一个 Observer。Emily 订阅了我们,她相信她会得到一个信号(猫)。当开车去 Emily 家时我们也有一个默认路线。不过开始的时候我们走了另一条路线(scheduler)。当你开始旅程的时候不用默认路线而选择不同的路,你使用了 subscribeOn() 方法。如果你使用了 subscribeOn(),你不能确定旅程结束的时候(Emily 用了 subscribeNext())所在的路与开始的路一致。你只能确保你从哪儿开始。
第二个方法,observeOn() 可以改变路线。不过它不限制旅途的起点,在旅途中任何时候你可以使用 observeOn() 切换路线。作为对比,subscribeOn() 只在起点的时候切换路线 - 这就是不同之处。然而大部分时候你将使用 observeOn()。
回到猫的传递,在 RxSwift 中用伪代码表达我们的传递是这样:
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 猫,它发送猫信号
- 在订阅之前我们在同一个 scheduler(Rx 的默认行为)。
- 切换 sheduler 至 MainRouteScheduler。在这之下的所有操作都将安排到 MainRouteScheduler(如果之后不改变的话)。
- 我们在 TwoLaneFreewayScheduler 启动,所以 breatheFreshAir() 将被安排在 TwoLaneFreewayScheduler。然后用 observeOn() 改变 sheduler。
- subscribeNext() 被安排到 MainRouteScheduler。如果在这行之前我们没有添加 observeOn(),它会被安排到 TwoLaneFreewayScheduler。
总结:subscribeOn() 指向整个链条的开始点,observeOn() 指向下一个去向。(看原文的图,不转了)。
示例
创建一个 project。用 Cocoapods 安装依赖库。
Podfine
platform :ios, '8.0'
use_frameworks!
target 'RxAlamofireExample' do
pod 'RxAlamofire/RxCocoa'
pod 'ObjectMapper'
end
列一下任务大纲:
- 创建 UI。UISearchBar 和 UITableView。
- 观察 search bar,每一次它的值改变,转换成 repo 的 array。这里需要 model 做网络请求。
- 用新数据去更新 table view。思考关于 scheduler,考虑不要拖累到 UI。
第1步 - Controller 和 UI
创建 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() {
}
}
创建可观察的 search bar 的 rx.text 属性。不过这一次加个过滤器:我们不想要空值。
class RepositoriesViewController: UIViewController {
...
var rx_searchBarText: Observable {
return searchBar
.rx.text
.filter { $0.characters.count > 0 } // notice the filter new line
.throttle(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
}
...
}
我们刚刚添加了一个变量到 RepositoryViewController。现在我们需要连接转换过的 Observable<[Repository]> 并传给 UITableView。
第2步 - Network model and mapping objects
首先设置 map 对象。这一次我们用不一样的 mapper。创建 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"]
}
}
我们有一个 controller,还有一个 Repository 对象的 model。现在实现 network model。
我们将初始化这个 model 为 Observable
import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift
struct RepositoryNetworkModel {
private var repositoryName: Observable
private func fetchRepositories() -> Driver<[Repository]> {
...
}
}
为什么返回的不是 Observable<[Repository]> 而是 Driver<[Repository]>?
今天我们要讨论的是 Scheduler。当你想绑定数据到 UI,总是想用 MainScheduler 来做这件事。基本上这是一个 Driver 的角色。Driver 是一个 Variable,它说:好的伙计,我就在主线程上所以别犹豫了绑定我吧。这个方法使我们的绑定不易出错,实现安全的连接。
我们从 flatMapLatest() 开始,把 Observable
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().mapArray(json) {
return repos
} else {
return []
}
}
}
...
}
看起来不太一样,这儿有另一个 map() 添加到 flatMapLatest() 中。但事实上没有你需要担心的。在 flatMapLatest() 中做了常规的网络请求,如果出现 error 则用 Observable.never() 中断传输管道。然后把从 Alamofire 得到的响应 map 成 Observable<[Repository]>。我们也可以链式 flatMapLatest() 的在 catchError() 之后,不过我们需要它在 flatMapLatest() 外部,这只是个偏好问题。
上门这个代码无法编译,因为我们返回了 Observable,然而我们希望返回 Driver。所以我们需要更深入。如何转换 Observable<[Repository]> 成 Driver<[Repository]>?非常简单。之需要用 asDriver() 操作器就可以把任何 Observable 可以转换成 Driver。在这个示例中,我们将使用 .asDriver(onErrorJustReturn: []),它的意思是:如果链条中有任何错误(很可能没有,因为我们在此之前转换它了),返回空数组。这是代码:
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().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
...
}
看,我们甚至没有用到 observeOn() 或 subscribeOn(),但已经两次切换了 scheduler。第一次用 throttle(),现在用 asDriver()(它确保我们在 MainScheduler) - 这只是个开始。现在代码可以执行了。最后我们要做的事情是连接 RepositoryNetworkModel 中的 repositories 到 view controller。不过在此之前先用其他东西替换这个方法,因为这样我们每次用它的时候会创建新的管道。取而代之,我更喜欢属性。但不是一个计算属性,因为结果将和一个方法一样。我们将创建一个 lazy var,它将被约束到获取 repositories 的方法。这种方式避免多次创建序列。我们还需要隐藏不是属性的气体东西,确保任何使用这个 model 的人可以得到正确的 Driver 属性。这个方案的唯一麻烦是我们不得不在 init 中明确类型。最终 RepositoryNetworkModel.swift 如下:
struct RepositoryNetworkModel {
lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
private var repositoryName: Observable
init(withNameObservable nameObservable: Observable) {
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().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
}
很好。现在我们只要连接数据到 view controller。我们要绑定 Driver 到 table view,不用 bindTo(之前用过),而用 drive() 操作器,语法和其他跟 bindTo 一样。为了绑定数据到 table view,我们还做了另一个订阅,每次 repositories 的 count 等于 0 时,显示一个 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 {
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() 操作器。不过你可以认为它只是一个 Driver 的 subscribeNext。
第3步 - 多线程优化
你可能认为事实上所有事情都在 MainScheduler 中完成。为什么?因为我们的链条从 searchBar.rx.text 中开始,并保证这个是在 MainScheduler 中。因为所有其他事都在当前 scheduler 我们的 UI 线程可能会不堪重负。如何避免这种情况?在 request 和 map 之前切换到背景线程,仅在更新 UI 的时候回主线程:
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().mapArray(json) {
return repos
} else {
return []
}
}
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
...
}
为什么两次一样的方法使用 observeOn()?因为我们并不确切地知道 requestJSON 是否将在与它启动时的同一个线程返回数据,为了确定它在背景线程 map。
现在我们的 map 是在背景线程了,结果传递给 UI 线程。还可不可以做得更多一些?我们希望用户知道网络请求正在进行。为了达到这个目的,我们将使用 UIApplication.sharedApplication().networkActivityIndicatorVisible 属性,显示旋转的菊花。不过现在我们必须小心对待线程,因为我们想在 request 和 mapping 操作的中间更新 UI。我们将使用一个优雅的方法叫做 doOn(),它可以做任何你置顶的事件(比如 .Next, .Error 等)。我们想在 flatMapLatest():之前显示句话,doOn是可以做到的。我们只需要在动作执行前切换到 MainScheduler。
完整的获取 repo 的代码如下:
RepositoryNetworkModel.swift:
struct RepositoryNetworkModel {
lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
private var repositoryName: Observable
init(withNameObservable nameObservable: Observable) {
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().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
}
}
现在你知道为什么当解析的时候不需要关心线程问题:Moya-ModelMapper 的 extension 为我们切换了 scheduler。