是时候学习 RxSwift 了

eleven0220 7年前
   <p>相信在过去的一段时间里,对 RxSwift 多少有过接触或耳闻,或者已经积累了不少实战经验。此文主要针对那些在门口徘徊,想进又拍踩坑的同学。</p>    <h3><strong>为什么要学习 RxSwift</strong></h3>    <p>当决定做一件事情时,至少要知道为什么。RxSwift 官网举了 几个例子 ,比如可以统一处理 Delegate , KVO , Notification ,可以绑定 UI,方便网络请求的处理等等。但这些更多的是描述可以用 RxSwift 来做什么,跟为什么要使用 RxSwift 还是会有点不同。</p>    <p>我们先来分析下 GUI 编程的本质,我喜欢把它抽象为视图和数据的结合。其中视图负责两件事:展示和交互,展示什么由数据决定。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2c3de02080e757272dd303a5aef23401.jpg"></p>    <p>其中单向数据流可以通过之前介绍的 ReSwift 完成。看起来好像没 RxSwift 什么事情,其实不然,RxSwift 可以在 UniDirectional Data Flow 的各个阶段都发挥作用,从而让 Data 的处理和流动更加简洁和清晰。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3bc3b39e4c878e40168e8823a5d0f98f.jpg"></p>    <ol>     <li>通过对 RxCocoa 的各种回调进行统一处理,方便了「Interact」的处理。</li>     <li>通过对 Observable 的 transform 和 composite,方便了 Action 的生成(比如使用 throttle 来压缩 Action )。</li>     <li>通过对网络请求以及其他异步数据的获取进行 Observable 封装,方便了异步数据的处理。</li>     <li>通过 RxCocoa 的 binding,方便了数据的渲染。</li>    </ol>    <p>所以 ReSwift 规范了数据流,RxSwift 为数据的处理提供了方便,这两个类库的结合,可以产生清晰的架构和易维护的代码。</p>    <p>当然,前提是对它们有足够的了解,尤其是 RxSwift,也就是我们今天的主角。</p>    <h3><strong>什么是 RxSwift</strong></h3>    <p>在 GUI 编程中,我认为比较复杂的有三个部分:</p>    <ol>     <li>非原生 UI 效果的实现(比如产品经理们经常冒出来的各种想法)。</li>     <li>大量状态的维护。</li>     <li>异步数据的处理。</li>    </ol>    <p>1)不在这次的讨论范畴(这里的学问也很多,比如流畅性和性能)。2) 可以通过单向数据流来解决(结合 Immutable Data)。3) 可以通过 RxSwift 来解决。那么 RxSwift 是如何处理异步数据的呢?</p>    <p>在说 RxSwift 之前,先来说下 Rx, ReactiveX 是一种编程模型,最初由微软开发,结合了观察者模式、迭代器模式和函数式编程的精华,来更方便地处理异步数据流。其中最重要的一个概念是 Observable 。</p>    <p>举个简单的例子,当别人在跟你说话时,你就是那个观察者,别人就是那个 Observable ,它有几个特点:</p>    <ol>     <li>可能会不断地跟你说话。( onNext: )</li>     <li>可能会说错话。( onError: )</li>     <li>结束会说话。( onCompleted )</li>    </ol>    <p>你在听到对方说的话后,也可以有几种反应:</p>    <ol>     <li>根据说的话,做相应的事,比如对方让你借本书给他。( subscribe )</li>     <li>把对方说的话,加工下再传达给其他人,比如对方说小张好像不太舒服,你传达给其他人时就变成了小张失恋了。( map: )</li>     <li>参考其他人说的话再做处理,比如 A 说某家店很好吃,B 说某家店一般般,你需要结合两个人的意见再做定夺。( zip: )</li>    </ol>    <p>所以,从生活中也能看到 Rx 的影子。「有些事情急不得,你得等它自己熟」,异步,其实就是跟时间打交道,不同的时间,拿到的数据也会不一样。</p>    <p><img src="https://simg.open-open.com/show/edf5008ea6a8794477f43eafc1ec1ffb.jpg"></p>    <p>这里的核心是当数据有变化时,能够立刻知晓,并且通过组合和转换后,可以即时作出响应。有点像塔防,先在路上的各个节点埋好武器,然后等着小怪兽们过来。</p>    <h3><strong>RxSwift Workflow</strong></h3>    <p>大致分为这么几个阶段:先把 Native Object 变成 Observable,再通过 Observable 内置的各种强大的转换和组合能力变成新的 Observable,最后消费新的 Observable 的数据。</p>    <p><img src="https://simg.open-open.com/show/3bba7af0f605b3635a80f69a481ef9ba.jpg"></p>    <p><strong>Native Object -> Observable</strong></p>    <p><strong>.rx extension</strong></p>    <p>假设需要处理点击事件,正常的做法是给 Tap Gesture 添加一个 Target-Action,然后在那里实现具体的逻辑,这样的问题在于需要重新取名字,而且丢失了上下文。RxSwift (确切说是 RxCocoa) 给系统的诸多原生控件(包括像 URLSession )提供了 rx 扩展,所以点击的处理就变成了这样:</p>    <pre>  <code class="language-swift">let tapBackground = UITapGestureRecognizer()    tapBackground.rx.event      .subscribe(onNext: { [weak self] _ in          self?.view.endEditing(true)      })      .addDisposableTo(disposeBag)        view.addGestureRecognizer(tapBackground)</code></pre>    <p>是不是简洁了很多。</p>    <p><strong>Observable.create</strong></p>    <p>通过这个方法,可以将 Native 的 object 包装成 Observable ,比如对网络请求的封装:</p>    <pre>  <code class="language-swift">public func response(_ request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {   return Observable.create { observer in    let task = self.dataTaskWithRequest(request) { (data, response, error) in     observer.on(.next(data, httpResponse))     observer.on(.completed)    }      task.resume()      return Disposables.create {     task.cancel()    }   }  }</code></pre>    <p>出于代码的简洁,略去了对 error 的处理,使用姿势类似</p>    <pre>  <code class="language-swift">let disposeBag = DisposeBag()    response(aRequest)    .subscribe(onNext: { data in      print(data)    })    .addDisposableTo(disposeBag)</code></pre>    <p>这里有两个注意点:</p>    <ol>     <li>Observerable 返回的是一个 Disposable ,表示「可扔掉」的,扔哪里呢,就扔到刚刚创建的袋子里,这样当袋子被回收( dealloc )时,会顺便执行一下 Disposable.dispose() ,之前创建 Disposable 时申请的资源就会被一并释放掉。</li>     <li>如果有多个 subscriber 来 subscribe response(aRequest) 那么会创建多个请求,从代码也可以看得出来,来一个 observer 就创建一个 task,然后执行。这很有可能不是我们想要的,如何让多个 subscriber 共享一个结果,这个后面会提到。</li>    </ol>    <p><strong>Variable()</strong></p>    <p>Variable(value) 可以把 value 变成一个 Observable ,不过前提是使用新的赋值方式 aVariable.value = newValue ,来看个 Demo</p>    <pre>  <code class="language-swift">let magicNumber = 42    let magicNumberVariable = Variable(magicNumber)  magicNumberVariable.asObservable().subscribe(onNext: {      print("magic number is \($0)")  })    magicNumberVariable.value = 73    // output  //   // magic number is 42  // magic number is 73</code></pre>    <p>起初看到时,觉得还蛮神奇的,跟进去看了下,发现是通过 subject 来做的,大意是把 value 存到一个内部变量 _value 里,当调用 value 方法时,先更新 _value 值,然后调用内部的 _subject.on(.next(newValue)) 方法告知 subscriber。</p>    <p><strong>Subject</strong></p>    <p>Subject 简单来说是一个可以主动发射数据的 Observable ,多了 onNext(value) , onError(error) , ‘onCompleted’ 方法,可谓全能型选手。</p>    <pre>  <code class="language-swift">let disposeBag = DisposeBag()  let subject = PublishSubject<String>()        subject.addObserver("1").addDisposableTo(disposeBag)  subject.onNext(":dog:")  subject.onNext(":cat:")        subject.addObserver("2").addDisposableTo(disposeBag)  subject.onNext(":a:")  subject.onNext(":b:")</code></pre>    <p>记得在 RAC 时代,subject 是一个不太推荐使用的功能,因为过于强大了,容易失控。RxSwift 里倒是没有太提及,但还是少用为佳。</p>    <p>Observable -> New Observable</p>    <p>Observable 的强大不仅在于它能实时更新 value,还在于它能被修改/过滤/组合等,这样就能随心所欲地构造自己想要的数据,还不用担心数据发生变化了却不知道的情况。</p>    <p>Combine</p>    <p>Combine 就是把多个 Observable 组合起来使用,比如 zip (小提示:如果对这些函数不太敏感,可以 <a href="/misc/goto?guid=4959654058854309011" rel="nofollow,noindex">实际操作下</a> ,体会会更深些)</p>    <p>zip 对应现实中的例子就是拉链,拉链需要两个元素这样才能拉上去,这里也一样,只有当两个 Observable 都有了新的值时,subscribe 才会被触发。</p>    <pre>  <code class="language-swift">let stringSubject = PublishSubject<String>()  let intSubject = PublishSubject<Int>()    Observable.zip(stringSubject, intSubject) { stringElement, intElement in   "\(stringElement) \(intElement)"   }   .subscribe(onNext: { print($0) })   .addDisposableTo(disposeBag)    stringSubject.onNext(":a:")  stringSubject.onNext(":b:")    intSubject.onNext(1)  intSubject.onNext(2)    // output  //  // :a: 1  // :b: 2</code></pre>    <p>如果这里 intSubject 始终没有执行 onNext ,那么将不会有输出,就像拉链掉了一边的链子就拉不上了。</p>    <p>除了 zip ,还有其他的 combine 的姿势,比如 combineLatest / switchLatest 等。</p>    <p><strong>Transform</strong></p>    <p>这是最常见的操作了,对一个 Observable 的数值做一些小改动,然后产出新的值,依旧是一个 Observable 。</p>    <pre>  <code class="language-swift">let disposeBag = DisposeBag()  Observable.of(1, 2, 3)      .map { $0 * $0 }      .subscribe(onNext: { print($0) })      .addDisposableTo(disposeBag)</code></pre>    <p>这是大致的实现(摘自官网)</p>    <pre>  <code class="language-swift">extension ObservableType {      func myMap<R>(transform: E -> R) -> Observable<R> {          return Observable.create { observer in              let subscription = self.subscribe { e in                      switch e {                      case .next(let value):                          let result = transform(value)                          observer.on(.next(result))                      case .error(let error):                          observer.on(.error(error))                      case .completed:                          observer.on(.completed)                      }                  }                return subscription          }      }  }</code></pre>    <p>接受一个 transform 闭包,然后返回一个 Observable ,因为接下来使用者将会对 myMap 的结果进行 subscribe,所以需要在 create 内部 subscribe 一下,不然最开始的那个 Observable 就是个 Cold Observable ,一个 Cold Observable 是不会产生新的数据的。</p>    <p><strong>Filter</strong></p>    <p>Filter 的作用是对 Observable 传过来的数据进行过滤,只有符合条件的才有资格被 subscribe。写法上跟 map 差不多,就不赘述了。</p>    <p><strong>Connect</strong></p>    <p>这是挺有意思的一块,在之前介绍 Observable.create 时有提到过,一个 Observable 被多次 subscribe 就会被多次触发,如果一个网络请求只想被触发一次,同时支持多个 subscriber,就可以使用 publish + connect 的组合。</p>    <p>当一个 Observable 使用了 publish() 方法后,正常的 subscribe 就不会触发它了,除非 connect() 方法被调用。而且每次 subscribe 不会导致 Observable 重新针对 observer 处理一遍。看一下这张图</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/451702ec0e5252fb912347e5bfd673bf.png"></p>    <p>有两块需要注意:</p>    <ol>     <li>connect() 之前的两次 subscribe 并没有产生新的 value。</li>     <li>connect() 之后 subscribe 的,只是等待新的 value,同时新的 value 还会分发给之前的 subscriber。</li>     <li>即使所有的 subscription 被 dispose , Observable 依旧处于 hot 状态,就好像还以为有人关心新的值一样。(这可能不是想要的结果)</li>    </ol>    <p>针对第 3 点,可以使用 refcount() 来代替 connect() ,前者会在没有 subscriber 时自动「冷」下来,不会再产生新的值。(Demo 取自 <a href="/misc/goto?guid=4959728832902024737" rel="nofollow,noindex">这里</a> )</p>    <pre>  <code class="language-swift">let myObservable = Observable<Int>.interval(1, scheduler: MainScheduler.instance).publish().refCount() // 1)    let mySubscription = myObservable.subscribe(onNext: {      print("Next: \($0)")  })    delay(3) {      print("Disposing at 3 seconds")      mySubscription.dispose()  }    delay(6) {      print("Subscribing again at 6 seconds")      myObservable.subscribe(onNext: {          print("Next: \($0)")      })  }</code></pre>    <p>输出</p>    <pre>  <code class="language-swift">Starting at 0 seconds  Next: 0  Next: 1  Next: 2  Disposing at 3 seconds  Subscribing again at 6 seconds  Next: 0  Next: 1</code></pre>    <p>可以看到,3 秒后 subscription dispose,此时没有任何 subscriber 还关心 Observable ,因此就重置了,所以 6 秒后又回到了初始状态(如果变成 connect 方法的话,会发现 6 秒后会输出 Next: 6 / Next: 7 )</p>    <p>那如果后加入的 subscriber 想要之前的数据怎么办?可以对原始的 Observable 设置 replay(n) ,表示最多返回 n 个元素给后加入的 subscriber。</p>    <h3><strong>Tips</strong></h3>    <p>上面介绍的是最基本的概念。顺便提一下比较常见的几个问题:</p>    <p><strong>如何处理 Scheduler?</strong></p>    <p>默认代码都是在当前线程中执行的,如果要手动切换线程,可以使用 subsribeOn 和 observeOn 两种方式,一般来说后者用得会多一些,那这两者有什么区别呢?</p>    <p>subscribeOn 跟位置无关,也就是无论在链式调用的什么地方, Observable 和 subscription 都会受影响;而 observeOn 则仅对之后的调用产生影响,看个 Demo:</p>    <pre>  <code class="language-swift">var observable = Observable<Int>.create { (observer: AnyObserver<Int>) -> Disposable in      print("observable thread: \(Thread.current)")      observer.onNext(1)      observer.onCompleted()      return Disposables.create()  }    let disposeBag = DisposeBag()    observable      .map({ (e) -> Int in          print("map1 thread: \(Thread.current)")          return e + 1      })      .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInteractive)) // 1      .map({ (e) -> Int in          print("map2 thread: \(Thread.current)")          return e + 2      })      .subscribe(onNext:{ (e) -> Void in          print("subscribe thread: \(Thread.current)")      })      .addDisposableTo(disposeBag)</code></pre>    <p>如果 1) 是 observeOn ,那么输出如下</p>    <pre>  <code class="language-swift">observable thread: <NSThread: 0x7f901cc0d510>{number = 1, name = main}  map1 thread: <NSThread: 0x7f901cc0d510>{number = 1, name = main}  map2 thread: <NSThread: 0x7f901ce15560>{number = 3, name = (null)}  subscribe thread: <NSThread: 0x7f901ce15560>{number = 3, name = (null)}</code></pre>    <p>可以看到 observable thread 和 map1 thread 依旧保持当前线程,但 observeOn 之后就变成了另一个线程。</p>    <p>如果 1) 是 subscribeOn ,那么会输出</p>    <pre>  <code class="language-swift">observable thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}  map1 thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}  map2 thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}  subscribe thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}</code></pre>    <p>可以看到全都变成了 subscribeOn 指定的 Queue。所以 subscribeOn 的感染力很强,连 Observable 都能影响到。</p>    <p><strong>Cold Observable 和 Hot Observable</strong></p>    <p>Cold 相当于 InActive,就像西部世界里,未被激活的机器人一样;Hot 就是处于工作状态的机器人。</p>    <p><strong>Subscription 为什么要 Dispose?</strong></p>    <p>因为有了 Subscriber 所以 Observable 被激活,然后内部就会使用各种变量来保存资源,如果不 dispose 的话,这些资源就会一直被 keep,很容易造成内存泄漏。</p>    <p>同时手动 dispose 又嫌麻烦,所以就有了 DisposeBag ,当这个 Bag 被回收时,Bag 里面的 subscription 会自动被 dispose,相当于从 MRC 变成了 ARC。</p>    <h3><strong>小结</strong></h3>    <p>RxSwift 如果概念上整理清楚了,会发现其实并不难,多从 Observable 的角度去思考问题,多想着转换和组合,慢慢就会从命令式编程转到声明式编程,对于抽象能力和代码的可读性都会有提升。</p>    <p> </p>    <p>来自:http://limboy.me/tech/2016/12/11/time-to-learn-rxswift.html</p>    <p> </p>