RxSwift by Examples #4 – Multithreading

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)

分步讲解:

  1. 我们订阅了 observable 猫,它发送猫信号
  2. 在订阅之前我们在同一个 scheduler(Rx 的默认行为)。
  3. 切换 sheduler 至 MainRouteScheduler。在这之下的所有操作都将安排到 MainRouteScheduler(如果之后不改变的话)。
  4. 我们在 TwoLaneFreewayScheduler 启动,所以 breatheFreshAir() 将被安排在 TwoLaneFreewayScheduler。然后用 observeOn() 改变 sheduler。
  5. subscribeNext() 被安排到 MainRouteScheduler。如果在这行之前我们没有添加 observeOn(),它会被安排到 TwoLaneFreewayScheduler。

总结:subscribeOn() 指向整个链条的开始点,observeOn() 指向下一个去向。(看原文的图,不转了)。

示例

创建一个 project。用 Cocoapods 安装依赖库。

Podfine

platform :ios, '8.0'
use_frameworks!
 
target 'RxAlamofireExample' do
 
pod 'RxAlamofire/RxCocoa'
pod 'ObjectMapper'
 
end

列一下任务大纲:

  1. 创建 UI。UISearchBar 和 UITableView。
  2. 观察 search bar,每一次它的值改变,转换成 repo 的 array。这里需要 model 做网络请求。
  3. 用新数据去更新 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,实现方法返回 Observable<[Repository]>。然后连接到 RepositoriesViewController 的 view。初步实现 RepositoryNetworkModel.swift:

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 转换成 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().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。

你可能感兴趣的:(RxSwift by Examples #4 – Multithreading)