Max 在 Boston 上學,在 San Francisco 工作,是一名軟件工程師及創業者。當他還在高中的時候就在一家創業公司工作了,他非常喜歡使用 iOS、Android 以及 JavaScript 框架來爲用戶編寫軟件。不過他偶爾也會抱怨創業、技術、書籍、電子遊戲等這些雜七雜八的東西。
概述(0:00)
這幾年有很多人在討論着關於 Rx 的相關概念。Rx 通過 Observable<Element>
接口來表達計算型泛型抽象 (generic abstraction of computation) 的概念,而 RxSwift 是
Rx 的 Swift 版本。無疑,這個內容非常龐大,所以我打算用一種稍微簡單點的介紹方式來講解這個框架。
如果有人寫出了下面這麼一段代碼,相信肯定會有很多人感到非常不爽:
Alamofire.request(.POST, "login", parameters: ["username": "max", "password": "insanity"])
.responseJSON(completionHandler: { (firedResponse) -> Void in
Alamofire.request(.GET, "myUserInfo" + firedResponse.result.value)
.responseJSON(completionHandler: { myUserInfoResponse in
Alamofire.request(.GET, "friendList" + myUserInfoResponse.result.value)
.responseJSON(completionHandler: { friendListResponse in
Alamofire.request(.GET, "blockedUsers" + friendListResponse.result.value)
.responseJSON(completionHandler: {
})
})
})
Alamofire.request(.GET, "myUserAcccount" + firedResponse.result.value)
.responseJSON(completionHandler: {
})
})
這段代碼有什麼問題呢?這些都是 Alamofire 網絡請求方法。如果大家沒用過 Alamofire 的話,可以把它看作是 AFNetworking
框架在執行 HTTP
請求操作。在這段代碼中你可以看到各種各樣的嵌套閉包代碼,每多一層嵌套,代碼縮進就會向右移動一層,你知道這段代碼是有問題的,因爲你無法直觀地解釋這段代碼做了什麼。在這段代碼中涉及到了不少網絡服務,而網絡很有可能會訪問失敗。我們需要爲此添加錯誤處理,然而我們卻無法知道該在何處處理這些所有的錯誤異常。Rx
就應運而生,它可以幫助我們解決這個問題。
回到基礎(2:26)
在處理不同事件的時候,無論如何你都會持有一個包含這些事件的集合。舉個例子,我們有這樣一個整數數組 [1, 2, 3, 4, 5, 6]
,如果你喜歡你也可以稱之爲列表。當我需要執行某些操作的時候,最符合 Swift 風格的操作就是使用 filter
方法了。
[1, 2, 3, 4, 5, 6].filter{ $0 % 2 == 0 }
那麼如果我想要給這個數組中的所有元素都乘以 5 然後生成一個新數組呢?
[1, 2, 3, 4, 5, 6].map{ $0 * 5 }
那麼執行加法操作呢?
[1, 2, 3, 4, 5, 6].reduce(0, +)
這些操作非常簡便。我們沒有使用任何 for 循環方法,我們也不會持有並保持那些臨時的中間數。這看起來就非常像 Scala 或者 Haskel 的操作方式。然而,一個正常的應用程序很難只使用數組就可以完成的了。大家都希望使用網絡、下載圖片、網上聊天、添加好友等等。你需要大量使用 IO 操作。IO 意味着你需要讓內存與用戶交互動作、用戶設備、相機、硬盤等等諸如此類的東西進行數據交換。這些操作都是異步的,隨時都可能會發生失敗,並且會發生很多奇怪的問題。
Rx 權利法案(4:09)
我提出了一個關於 Rx 的權利法案,它規定:
我們的開發者擁有像管理迭代集合 (iterable collections) 一樣管理異步事件的權利。
觀察者(4:25)
在 Rx 的世界裏,讓我們用 觀察者 (Observables)
的概念來代替數組。 觀察者
是一個類型安全的事件對象,可以長期對不同種類的數據值進行寫入和讀出。RxSwift
目前處於 Beta 3 的版本,安裝非常簡單。你所需要做的就是導入 RxSwift 即可。
pod 'RxSwift', '~> 2.0.0-beta.3'
import RxSwift
創建 觀察者
也很容易。最簡單的創建方式就是使用 just
,這是一個
RxSwift 內建的函數。你可以將你所想要的變量放到其中,它就會返回一個包含相同類型的 觀察者
變量。
just(1) //Observable<Int>
那麼如果我們想要從數組中一個接一個的推出元素並執行相關操作呢?
[1,2,3,4,5,6].toObservable() //Observable<Int>
這會返回一個 Observable<Int>
對象。
如果你在使用類似於上傳數據到 S3 或者向本地數據庫保存數據之類的 API,你可以這樣寫:
create { (observer: AnyObserver<AuthResponse>) -> Disposable in
return AnonymousDisposable {
}
}
當你調用 create
的時候會返回一個閉包。這個閉包會給予一個 觀察者
參數,這意味着有某個東西正在對其進行觀察。目前你可以忽略 AnonymousDisposable
這個東西。在下面兩行代碼中你將會看到你在何處將
API 代碼轉換爲了一個好用的 觀察者
對象。
下面這段代碼和 Alamofire 的使用方式相同:
create { (observer: AnyObserver<AuthResponse>) -> Disposable in
let request = MyAPI.get(url, ( (result, error) -> {
if let err = error {
observer.onError(err);
}
else if let authResponse = result {
observer.onNext(authResponse);
observer.onComplete();
}
})
return AnonymousDisposable {
request.cancel()
}
}
我可以進行日誌記錄操作,也可以執行一個 GET
請求,隨後我可以得到一個帶有結果和錯誤的回調函數。實際上我無法改變這個 API,因爲這是由另一個客戶端 SDK 所提供的,但是我可以將其轉換爲一個 觀察者
對象。當存在錯誤的時候,我會調用observer.onError()
方法。這意味着只要監聽了這個對象的代碼都會接收到這個錯誤消息。當你得到可用的服務器迴應時,調用 observable.onNext()
方法。接着,如果處理結束的話就調用 onComplete()
。這個時候我們就到了AnonymousDisposable
這裏了。 AnonymousDisposable
是當你想要中止請求的時候被調用的操作。比如說你離開了當前視圖控制器或者應用已經不再需要調用這個請求的時候,就可以使用這個方法了。這對視頻上傳等大文件操作是非常有用的。當你結束所有操作的時候, request.cancel()
可以清除所有的資源。無論是操作完成還是發生錯誤,這個方法都會被調用。
監聽觀察者(8:11)
現在我們知道如何創建觀察者了,那麼就來看看如何對其建立監聽吧!我們以數組爲例,因爲我們可以在很多對象中調用一個名爲 toObservable()
的擴展方法。隨後,就可以編寫監聽函數了:
[1,2,3,4,5,6]
.toObservable()
.subscribeNext {
print($0)
}
這看起來跟枚舉差不多。Subscribe 監聽器事件基於失敗請求、下一步事件以及onCompleted
操作,給你提供了各種各樣的信息。你可以有選擇性的建立相應的監聽:
[1,2,3,4,5,6]
.toObservable()
.subscribe(onNext: { (intValue) -> Void in
// 推出一個整數數據
}, onError: { (error) -> Void in
// 發生錯誤!
}, onCompleted: { () -> Void in
// 沒有更多的信號進行處理了
}) { () -> Void in
// 我們要清除這個監聽器
}
關聯觀察者(9:14)
使用 Rx 的最好例子就是套接字 (socket) 服務了。假設我們有一個網絡套接字服務,它用來監聽股票行情,然後顯示用戶的當前賬戶餘額 UI 界面。由於股票行情對應了不同的事件,根據這些事件來決定用戶是否可以購買股票。如果賬戶餘額過低的時候我們將禁止用戶購買,當股票票價在用戶的承擔範圍內的時候允許用戶購買。
func rx_canBuy() -> Observable<Bool> {
let stockPulse : [Observable<StockPulse>]
let accountBalance : Observable<Double>
return combineLatest(stockPulse, accountBalance,
resultSelector: { (pulse, bal) -> Bool in
return pulse.price < bal
})
}
combineLatest
意味着當某個事件發生的時候,我們就將最近的兩個事件之間建立關聯。Redution 閉包是否會被回調取決於股票票價是否低於餘額。如果被回調,這就意味着用戶可以購買這隻股票。這個操作允許你將兩個觀察者關聯起來,然後列出一個邏輯決定某些操作是否可以進行。這會返回一個 Bool
類型的觀察者。
rx_canBuy()
.subscribeNext { (canBuy) -> Void in
self.buyButton.enabled = canBuy
}
使用 subscribe 方法來操作剛剛我創建的那個會返回 Bool
值的 rx_canBuy
方法。然後你就可以根據 canBuy
的返回值來決定 self.buyButton
的行爲了。
讓我們來舉一個合併的例子。假設我有一個存有我所喜歡的股票的用戶界面應用。我通過 Apple、Google 以及 Johnson 來監聽股票票價。所有這些股票行情都有不同的結果。當股票行情發生變化的時候我需要立刻知道並更新我的用戶界面。
let myFavoriteStocks : [Observable<StockPulse>]
myFavoriteStocks.merge()
.subscribeNext { (stockPulse) -> Void in
print("\(stockPulse.symbol)/
updated to \(stockPulse.price)/")
}
這些參數的類型都是相同的 Observable<StockPulse>
類型。我需要知道它們何時被觸發,我需要做的就是持有一個 觀察者
數組。裏面存放了我需要進行監聽的多個不同種類的股票行情,我可以在一個輸出流中將它們合併然後進行監聽。
與 Rx 觀察者圖表進行交互(18:03)
我使用 Rx 使用了很長時間。很遺憾,我仍然忘記了許許多多的操作,並且需要非常頻繁地回顧參考文檔。這個網站 rxmarbles.com 將會爲我們展示所有這些操作的理論部分。
輕鬆實現後臺進程(19:03)
藉助 RxSwift 還有許多很讚的事情可以做。比如說你有一個視頻上傳操作,由於這個視頻文件太大了,因此你需要在後臺中進行。執行這個操作的最好辦法就是使用observeOn
進行。
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 3
operationQueue.qualityOfService = NSQualityOfService.UserInitiated
let backgroundWorkScheduler
= OperationQueueScheduler(operationQueue: operationQueue)
videoUpload
.observeOn(backgroundWorkScheduler)
.map({ json in
return json["videoUrl"].stringValue
})
.observeOn(MainScheduler.sharedInstance)
.subscribeNext{ url
self.urlLabel.text = url
}
視頻上傳操作需要給我當前完成度百分比的信號信息,這樣我才能知道這個操作是否完成。但是我並不打算在主線程中執行這個操作,因爲我可以在後臺執行。當視頻上傳結束之後,我會得到一個返回的 JSON 數據,它會告知我所上傳的 URL 地址這樣我就可以將其寫入到 UI 標籤當中了。因爲我是在後臺進行監聽的,因此這個操作不會在主線程上進行。我需要通知 UI 進行更新,這樣才能將信息傳遞給主線程。因此我們需要回去執行 observeOn(MainScheduler.SharedInstance)
方法,這個操作將會執行
UI 更新操作。遺憾的是,和 Android 上的 RxJava 框架不同,在 Swift 中如果你在後臺進程中更新 UI 也是可以進行的(但不要這麼做)。在 Android 中這麼做會發生崩潰,而在 Swift 中則不會發生崩潰。
這只是 RxSwift 的皮毛(20:31)
我展示了一些很酷的東西,通過將事件視爲數組,你可以用 RxSwift 讓代碼更簡單、更好用。我知道 MVVM 是一個非常龐大的項目,它可以將視圖控制器從一個完整的個體變成一個個關聯的組織。RxSwift 有一個相似的名爲 RxCocoa 的倉庫,可以有效地解決這個問題。基本上來說,它給所有的 Cocoa 類建立了擴展方法,從而可以讓 UI 視圖可以建立諸如 Rx-Text 或者名字文本框之類的東西。這樣你就可以少寫一點 subscribeNext 方法,從而在多個不同的視圖點中將值和觀察值之間建立關聯。
跨平臺(22:49)
我們生活在一個有多個平臺的世界。Rx 對我來說,最主要的吸引點就是 Rx 可以忽略每一個其他客戶端 API 執行 IO 的方法。如果你在使用 Android 或者 JavaScript,你必須要學習如何異步管理這些不同的 IO 操作事件。Rx 是一個支持多平臺的輔助庫,你可以在多個熱門語言中使用:.NET、JavaScript、Java,這三門是除了 Swift 之外最熱門的三個語言了。你可以使用相同的操作、步驟和心態來編寫代碼。在所有的語言當中,Rx 看起來都是十分相似的。我們以一個日誌功能爲例,首先是 Swift:
func rx_login(username: String, password: String) -> Observable<Any> {
return create({ (observer) -> Disposable in
let postBody = [
"username": username,
"password": password
]
let request = Alamofire.request(.POST, "login", parameters: postBody)
.responseJSON(completionHandler: { (firedResponse) -> Void in
if let value = firedResponse.result.value {
observer.onNext(value)
observer.onCompleted()
} else if let error = firedResponse.result.error {
observer.onError(error)
}
})
return AnonymousDisposable{
request.cancel()
}
})
}
rx_login
函數可以返回一個你所想要的觀察者對象。下面是 Kotlin 版本:
fun rx_login(username: String, password: String): Observable<JSONObject> {
return Observable.create ({ subscriber ->
val body = JSONObject()
body.put("username", username)
body.put("password", password)
val listener = Response.Listener<JSONObject>({ response ->
subscriber.onNext(response);
subscriber.onCompleted()
})
val errListener = Response.ErrorListener { err ->
subscriber.onError(err)
}
val request = JsonObjectRequest(Request.Method.POST, "login", listener, errListener);
this.requestQueue.add(request)
});
}
看起來基本差不多,下面是 TypeScript 版本:
rx_login = (username: string, password: string) : Observable<any> => {
return Observable.create(observer => {
let body = {
username: username,
password: password
};
let request = $.ajax({
method: 'POST',
url: url,
data: body,
error: (err) => {
observer.onError(err);
},
success: (data) => {
observer.onNext(data);
observer.onCompleted();
}
});
return () => {
request.abort()
}
});
}
不仔細看這些代碼形式還真差不多。你可以對所有這類事件隨意編寫測試用例。你可以不必寫所有的客戶端代碼或 UI 代碼,因爲它們都需要自身平臺的支持,但是基於服務的類可以很容易地提取出相同的使用接收方式。幾乎相同的原理無處不在。
問與答(24:42)
問:您對 RxSwift 和 ReactiveCocoa 都有什麼看法嗎?
Max:我使用了三年的 Objective-C。ReactiveCocoa 是我早期的試用目標。當我換用 Swift 進行開發的時候,我安裝了 ReactiveCocoa 的早期版本,使用起來讓我感到很不愉快。我發現通過 Google 搜索我就可以很快地上手 RxSwift。對我個人而言,RxSwift 和 ReactiveCocoa 我都用。人們總會說這兩個框架之間有着這樣那樣的差別,但是我從來沒有聽人說過 RxSwift 毀了某人的童年,也沒聽說過 ReactiveCocoa 讓某人妻離子散。沒有人曾經跟我談論過這些差異。因此使用哪一個是你的自由。RxSwift 是 ReactiveX Github 倉庫下的一個項目,因此如果如果對你來說閱讀代碼和學習很輕鬆的話,那麼就直接使用 RxSwift 吧!如果你的公司只是關於 iOS 以及 Android 的話,那麼就使用 ReactiveCocoa 就好了,就不要再考慮別的了。但是如果你需要準備三個平臺的話,比如說有一個和 JS 應用一起使用的電子應用,最好是能夠將這個應用放到 Spotify 上面,然後給三個平臺分別複製服務然後創建一個監視器。你可以在一晚上完成這個任務。
問:關於自動完成功能速度慢的問題您有沒有建議或者解決方案呢?
Max:我打字速度是很快的,比自動完成功能要快得多了。我大多數時候只在敲下點的時候才查看自動完成列表。如果你輸入的速度過快,那麼你很可能就無法得到自動完成提示。這是此時 Xcode 面臨的實際問題。我在使用自動完成的時候並沒有碰到問題,而通常我使用自動完成的方法是 flatMap
、 merge
和 combineLatest
。
問:您提到了跨平臺。它能在 Linux 上運行嗎?
Max:我之所以提到跨平臺的特性,是因爲它本質上是一個擁有 API 特性的庫。之所以這麼說是因爲你可以使用 Java 或者 TypeScript 來實現輔助庫的功能,這個庫本質上是獨立運行的。
問:我注意到這個框架導入的是 Foundation 框架,我好奇的是,如果要擺脫這個基礎庫的依賴的話,或者替換這個基礎庫的時候,會不會非常難?
Max:我不清楚。我會具體問下別人這個問題,之後再來回復你。
問:如何對 RxSwift 進行調試呢?
Max:有一個帶有閉包的調試函數的。RxSwift 有一個類似庫是專門處理閉包的。這個庫不會在正式產品中被使用。你實際使用之後你會發現它的功能真的十分好用。它會自行創建閉包調用,因此你無需對這個異步線程進行管理。因此你可以在主線程上面對堆棧進行跟蹤。
問:我在想爲什麼您覺得專門挑選幾個特殊的例子來介紹 RxSwift 是一個好主意呢?或者說爲什麼不全用上 RxSwift 或者 Rearctive 來創建一個完整的應用程序呢?
Max:很多對 RxSwift 進行貢獻的人都說,你應當從一開始就在項目中使用 RxSwift。但是這很不現實,因此我決定有選擇性地進行介紹。我不認爲在場的絕大多數觀衆朋友們都能夠有立馬開始新項目的機會。你們大家很多時候是在處理着各種各樣有五年或者六年曆史的代碼庫,因此有選擇性地進行介紹,決定是否使用就看大家是否喜歡了。
問:我已經用了很久的 ReactiveCocoa 了,其中一件 ReactiveCocoa 所要做的事就是他們想將所有事件都轉爲信號進行處理,這樣你就可以將他們集中在一起。在 Objective-C 中他們使用 Selector 來實現此功能。你是否清楚在 RxSwift 中,它是如何使用 Swift 來處理委託回調方法的呢?
Max:是的,如果你看一下在 RxCocoa 中這一部分代碼的話,它們會重新激活 (reactifying) 你的 UITableViewDelegates
和 UICollectionViewDelegates
。它們會創建一個微妙的代理,這樣你就可以通過閉包來開始接收事件,然後將其轉換爲 觀察者
,以便創建屬於你自己的 觀察者
集,接着在委託層中接收信號。如果你查看
RxCocoa 庫的話你會發現這個操作做得也是非常完美的。