RxAndroid使用初探;簡潔、優雅、高效

安卓開發高階技術分享發表於2019-01-16

引言

RxAndroid是一個開發庫、是一種程式碼風格、也是一種思維方式。

正如標題所言,RxAndroid的特點是簡潔、優雅、高效,它的優點是多執行緒切換簡單、資料變換容易、程式碼簡潔可讀性好、第三方支援豐富易於開發;缺點是學習成本較高、出錯難以排查。

 

用途與優勢

·起源

RxAndroid源於RxJava——"a library for composing asynchronous and event-based programs using observable sequences for the Java VM“,意為“一個在Java VM 上使用可觀測的序列來組成非同步的、基於事件的程式的庫”。

·用途

RxAndoid在我的粗略理解中,是一個實現非同步操作的庫,具有簡潔的鏈式程式碼,提供強大的資料變換。

詳細解析 RxAndroid 的使用方式中,有這樣一個例子:

一個典型的Android圖文列表

這個介面是一個典型的Android圖文列表,取線上資料,並顯示為圖文列表,業務流程是這樣的:

1.輸入多個人名

2.根據人名,線上查詢對應的資訊

3.獲取返回資訊,顯示為圖文列表

我們在常規開發中,一般要做到這樣幾件事:

•用非同步執行緒線上查詢資料

•用foreach查詢每個使用者(沒有批量查詢的介面)

•訪問網路,查詢每個人名對應的github資訊(返回Json字串)

•型別轉換(Json->Java Object)

•在主執行緒中接收資料

•每收到一條資料,就用Adapter向列表新增一條,並重新整理介面

實現這樣的需求,可以說功能不多,程式碼不少,少說也得N個類+幾百行程式碼吧,加班到23:59:59應該就能搞定了…工作量主要在非同步網路操作、型別轉換和介面重新整理上。

不過,RxAndroid可以做到,用區區一小段兒程式碼就實現這個需求:

除了MainActivity、layout檔案和Adapter類之外,主要的業務邏輯都在這一段兒程式碼中實現了。感興趣的朋友可以在詳細解析 RxAndroid 的使用方式中找到這段兒程式碼。

剛才誰說要幾百行程式碼來著...

·優勢

我看到這段程式碼,想到的第一件事就是非同步好簡單、程式碼好簡潔,一個簡單、一個簡潔,這就意味著工作效率。

知識結構

前面說過,RxAndroid學習成本比較高,其中一個原因是出現的概念和知識點比較多,我個人傾向於把它們分為三部分:基本概念、執行緒控制和變換操作。

基本概念

先看一下剛才那段兒RxAndroid程式碼,裡面有這麼幾個元素:

 

圖中出現的基本概念,包括觀察者、被觀察者和訂閱、另外還有一個圖中沒有出現的事件。

執行緒控制和變換操作我們先略過,在後面再看。

我們理解一個觀察者、被觀察者、訂閱和事件的關係:

 

被觀察者、觀察者、訂閱和事件

這顯然是一個觀察者模式,其中有四個基本概念:Observable (可觀察者,即被觀察者)、 Observer (觀察者)、 subscribe (訂閱)、事件。Observable 和Observer 通過 subscribe() 方法實現訂閱關係,從而 Observable 可以在需要的時候發出事件來通知 Observer。

與傳統觀察者模式不同, RxJava 的事件回撥方法除了普通事件 onNext() (相當於 onClick() / onEvent())之外,還定義了兩個特殊的事件:onCompleted() 和 onError(),作為事件佇列的結束。

·基本概念—被觀察者Observable

 

 

被觀察者有這麼幾個特點:

•每一段Rx程式碼都從被觀察者開始

•被觀察者向觀察者丟擲onNext,onCompleted,onError事件,onNext可以連續多個,但onCompleted/onError只能出現一次,且出現後就結束事件佇列

•用一些操作符可以方便地生成Observable物件,例如這段程式碼等價於Observable.just("Hello","Hi","Aloha");

·基本概念—觀察者Observer及其變體

 

 

觀察者有這麼幾個特點:

•被觀察者丟擲事件,觀察者響應事件

•觀察者決定如何響應onNext/onCompleted/onError事件

訂閱(.subscribe(...))的邏輯和寫法是有問題的,從邏輯上是觀察者訂閱被觀察者,但寫法卻好像是被觀察者訂閱了觀察者,這是為了不打斷Rx的鏈式程式碼格式,而且,我個人覺得寫成.subscribeby(...)更恰當

觀察者Observer有多種變體的寫法:

.subscribe(new Observer…)

.subscribe(new Subscriber…)

.subscribe(new onNextAction…,new onErrorAction…,new onCompleteAction…)

.subscribe(new onNextAction…,new onErrorAction…)

.subscribe(new onNextAction…)

其中,Observer是原始寫法;

 

Subscriber繼承了Observer,是推薦寫法;

 

也可以直接傳對三個事件的響應;

 

或者只響應兩個事件、或者只響應一個事件。

 

ActionN和FuncN

我們注意到,上圖的程式碼中出現了ActionN和FuncN的程式碼形式,這都是RxJava的介面:

ActionN,Action0/Action1/…ActionN是RxJava的介面,可傳入0~N個引數,沒有返回值;

FuncN,從Func0/Func1/…FuncN是RxJava的介面,可傳入0~N個引數,有返回值。

到這裡,其實我們就已經能讀懂大部分的Rx程式碼了,包括最前面那段兒。

·基本概念—訂閱、取消訂閱、延遲

RxAndroid在訂閱和取消訂閱時也有豐富的處理函式:

•doOnSubscribe,在subscribe()呼叫後&事件傳送前,執行,可以指定執行緒

observable.doOnSubscribe(…)

•doOnUnsubscribe,取消訂閱時的監聽

observable.doOnUnsubscribe(…)

•doOnCompleted,正常結束時的監聽

observable.doOnCompleted(…)

•doOnError,出錯時的監聽

observable.doOnError(…)

•doOnTerminate,即將被停止(正常/異常)監聽

observable.doOnTerminate(…)

•finallyDo,結束(正常/異常)監聽

observable.finallyDo(…)

•delay,延遲一段兒時間再發射(已運算)

observable.from(items).delay(5,TimeUnit.SECONDS).observeOn(Schedulers.newThread()).subscribeOn(Schedulers.newThread());

•delaySubscription,延遲onNext事件

observable.delaySubscription(2,TimeUnit.SECONDS)

•timeInterval,定製發射事件

observable.interval(1,TimeUnit.SECONDS).take(5).timeInterval();

•要及時釋放訂閱,否則可能造成記憶體洩漏

·基本概念—事件

就是刷一下事件這個概念的存在感,本節無內容...

·執行緒控制

RxAndroid的一個突出優勢是多執行緒控制非常簡單。

·執行緒控制—跨執行緒

和以往程式碼繁瑣的多執行緒控制不同,RxAndroid可以輕鬆地用一行程式碼來切換執行緒

 

兩個函式:

•subscribeOn()指定事件產生的執行緒(為前面的程式碼指定執行緒)

•observeOn()指定事件消費的執行緒(為後面的程式碼指定執行緒)

�多鍾執行緒:

•Schedulers.immediate,預設,當前執行緒,立即執行

•Schedulers.trampoline,當前執行緒,要等其他任務先執行

•Schedulers.newThread,啟用一個新執行緒

•Schedulers.io,擅長讀寫檔案、資料庫、網路資訊,一個無數量上限的執行緒池

•Schedulers.computation,擅長cpu密集型計算,固定執行緒池,不要做io操作,會浪費cpu

•AndroidSchedulers.mainThread,Android主執行緒

·執行緒控制—如何操作

我們舉個栗子,說明如何靈活地切換執行緒

 

需要注意的是:

•subscribeOn只能定義一次,除非是在定義doOnSubscribe

•observeOn可以定義多次,決定後續程式碼所在的執行緒

·執行緒控制—安全性

只要涉及到多執行緒,就一定要考慮安全性問題,這方面,我個人主要考慮3種方法

使用Rx快取:

 

被觀察者可以使用Rx快取,對同一個被觀察者物件執行訂閱或取消訂閱時,這個被快取的Observable不會去重複查詢資料,它會按自己的節奏去繼續執行網路請求,你需要在生命週期之外去做儲存。

使用RxLifecycle:

與Activity或Fragment繫結生命週期

 

在指定生命週期事件中解除訂閱

 

使用CompositeSubscription去收集所有Rx的Subscriptions,統一取消訂閱;

 

·變換操作

除了前面講的多執行緒操作簡單,Rx還有一個深受廣大群眾喜愛的特點,就是強大的資料變換能力。

所謂變換,就是將事件序列中的物件或整個序列進行加工處理,轉換成不同的事件或事件序列

一個簡單的例子:

 

實際上,變換有兩大類,一類是針對事件/資料的lift變換,另一類是針對Observable本身的compose變換:

 

·變換操作—lift變換

我們先看三個典型的lift變換函式:

 

flatmap函式,可以把把一組String轉換為一組User資料物件。

 

throttlefirst函式,在連續點選時,在兩秒內只響應第一個點選事件。

 

merge函式,可以把兩組資料合併為一組資料。

上述3個函式,代表三種lift變換操作符:

轉換操作符:能實現一對一轉換、一對多轉換、依次將輸出作為輸入、分組輸出 等

常用函式包括 map flatMap concatMap flatMapIterable switchMap scan groupBy ...

過濾操作符:能實現去重、取前N個、取後N個、取第N個、從第N個取、自定義過濾 等

常用函式包括 fileter take takeLast takeUntil distinct distinctUntilChanged skip skipLast ...

組合操作符:能實現資料合併、資料運算、資料排列、組合配對 等

常用函式包括 merge zip join combineLatest and/when/then switch startSwitch...

·變換操作—compose變換

compose變換不針對事件和序列,針對的是Observable本身,可以理解為,compose是定義一組變換模板,讓多個Observable執行同樣的變換

 

一個典型的例子是,用compose函式實現與RxActivity生命週期的繫結:

 

注意,這裡繫結的是RxActivity的生命週期,Activity本身是沒有這個功能的。

·回顧一下lift和compose變換的區別

 

lift變換,針對事件項和事件序列

 

lift的功能在於變換資料,定義如何響應事件:

·變換、如String->Bitmap;

·過濾、如只處理前三條;

·合併、如把兩組資料合併為一組

compose變換,針對Observable

 

compose的功能在於定義被觀察者的行為:

定義一組變換模板,讓多個Observable實現同一組變換

如:設定Observable的生命週期

變換是RxJava中非常重要,也比較複雜的一部分內容,這裡只是做了簡單的歸納和介紹,建議仔細閱讀給 Android 開發者的 RxJava 詳解RxJava系列,對變換建立更深入更全面的瞭解。

·回顧知識結構

基本概念裡,我們主要了解了基於事件訂閱的開發風格

執行緒控制裡,我們瞭解到如何簡潔高效地操作非同步執行緒,決定每一段兒程式碼執行的執行緒

�變換操作裡,我們瞭解到RxAndroid如何使用強大的資料查詢/變換/使用

相關開發庫

�在使用RxAndroid的開發中,可選擇很多Rx特性的開發庫

Lambda表示式

Java8的特性,可以進一步簡化程式碼,但暫無Android Studio支援,需要retrolambda外掛,有相容性問題,可讀性和可維護性差,風險高,目前不建議使用

RxBinding

處理控制元件的UI事件,提供一些高階事件,普通如點選、長按、勾選,高階如防抖(throttleFirst,N秒內只允許點選一次)、等待(debounce,一定時間內沒有操作就觸發事件)

RxLifecycle

設定生命週期,如自動跟隨Activity或Fragment的生命週期去建立/銷燬,不用自己寫執行緒,但要使用Rx的Activity和Fragment

Retrofit

最匹配RxAndroid的網路訪問框架,支援Rx的鏈式呼叫,可直接返回資料物件,自定義特性也非常強大

SqlBrite

幫你用Rx的方式完成資料查詢,但它不是一個資料庫框架

·相關開發庫—Labmbda表示式

 

Lambda與Rx融合得更好,能使程式碼更簡潔(可是…再等等吧)

·相關開發庫—RxBinding

 

實質是用Observable去包裝UI事件,然後就可以用RxJava的各種轉換功能去擴充套件UI事件

�·相關開發庫—RxLifecycle

 

通過compose操作符呼叫,完成Observable和當前的UI元件繫結,實現生命週期同步。當UI元件生命週期結束時,就自動取消對Observable訂閱。

bindToLifecycle的生命週期與呼叫的位置有關:

 

��·相關開發庫—Retrofit

一段兒常規的Retrofit程式碼,包括介面檔案、初始化和呼叫:

 

在這個例子中,Retrofit拼url的過程如下:

•定義基礎url:"https://api.github.com/"

•定義@GET的引數:"users/{user}"

•定義函式的入參:@Path("user") String username

•基礎url+@GET中的url:"https://api.github.com/users/{user}“(如果@GET的引數是另一個url如http://www.google.com/users/{user},就可以替換掉原來的基礎url)

•根據引數@Path的傳值,更換user的值:"https://api.github.com/users/jake“

Tips: 基礎url不能寫成"https://api.github.com"(必須以“/”結尾)

上述程式碼是void函式,用callback返回資料,Retrofit還可以直接返回Observable物件:

 

上述例子均為GET方式,Retrofit還可以向伺服器提交POST資料:

 

因為POST提交Json需要加訊息頭"Content-type","application/json“,因為Retrofit預設使用的是okhttp這個網路框架,所以需要設定okhttp的攔截器Intercepter,攔截http請求進行監控,重寫或重試:

 

下面大概列舉一下Retrofit各種網路操作的寫法:

 

需要注意的是,Retrofit使用okhttp實現網路訪問,而okhttp在Android主執行緒執行時會報錯,所以要注意網路操作所在的執行緒。

建議閱讀Retrofit基本使用教程,詳細瞭解一下Retrofit這個網路框架

��·相關開發庫—SqlBrite

 

如上圖所示,SqlBrite使用Rx的訂閱模式來查詢 ,而不是直接執行查詢,SqlBrite不是一個資料庫框架,他依然需要讓你自己寫sql語句來建立資料庫查詢,就是查詢的等操作,採用了RX的方式來完成,這樣的好處,就是我們在展示列表的時候,增刪改查後,Sqlbrite都會自動的呼叫查詢,來改變你傳入列表的資料。

例項分析

·例項分析-改造目標

我手頭有一個專案,準備用RxAndroid進行改造,並評估改造效果,我希望做到以下幾點:

•改造一個自定義控制元件,使用Rx的Interval來替換執行緒,使用RxLifeCycle把取消訂閱託管給UI元件

•把原來的多層資料訪問及處理改用Rx的鏈式方式實現

•用Retrofit實現Rx風格的網路訪問

•保留MVP模式

•簡化程式碼,增強程式碼可讀性

·例項分析-環境準備

新增gradle引用:

compile'io.reactivex:rxandroid:1.1.0' //

RxAndroid

compile 'io.reactivex:rxjava:1.1.0' //推薦同時載入RxJava

compile'com.squareup.retrofit:retrofit:2.0.0-beta2' // Retrofit網路處理

compile'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2' // Retrofit的rx解析庫

compile'com.squareup.retrofit:converter-gson:2.0.0-beta2' // Retrofit的gson庫

compile 'com.trello:rxlifecycle:0.4.0' //RxLifecycle管理Rx的生命週期

compile'com.trello:rxlifecycle-components:0.4.0' // RxLifecycle元件

// RxBinding

compile'com.jakewharton.rxbinding:rxbinding:0.3.0'

compile'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.3.0'

compile'com.jakewharton.rxbinding:rxbinding-design:0.3.0'

·例項分析-一個自定義控制元件

我有一個自定義的播放控制元件,功能如下:

 

準備使用Rx的Interval來替換執行緒,使用RxLifeCycle把取消訂閱託管給UI元件

改造前後的程式碼如下:

用Rx的Interval代替原有的執行緒操作

 

用RxLifeCycle來實現與RxActivity的生命週期同步

 

改造後的效果:

•工作量大為減少

•不需要複雜的自定義執行緒

•程式碼變簡潔易讀,沒有了迷之縮排,業務邏輯呈鏈式

•提高了可維護性

·例項分析-多層資料訪問

原有的資料訪問框架參考的google開源的mvp,結構如下:

 

改造前的具體程式碼如下:

 

改造後的程式碼如下:

 

主要變化有:

•不使用callback,改為返回Observable,可以與UI層的Rx無縫對接

•網路訪問直接走Retrofit,程式碼更簡潔

•執行緒管理更精細,網路操作在io執行緒上,運算操作在computation執行緒上

•主要邏輯在同一段鏈式程式碼中,提高了程式碼可讀性

•RxJava有強大的操作符,可以進一步簡化程式碼

上述程式碼雖然減少了類檔案的數量,也簡化了程式碼,但結構上還是略顯繁瑣,可以� 進一步簡化,同時保留cache--local--net的三級資料訪問結構:

 

 

使用concat+first變換操作符,按照快取->資料庫->網路的順序依次查詢資料

concat()操作符可以持有多個Observable物件,並將它們按順序串聯成佇列

first()操作符可以從多個資料來源中,只檢索和傳送第一個返回的結果,並停止後續的佇列

這兩個操作符組合使用,可以實現逐級訪問三級快取,並更新和儲存資料的功能。

·例項分析-在MVP中使用RxAndroid

我原來的專案結構參考了google的mvp專案,大量的callback呼叫其實與RxAndroid的鏈式呼叫是不相容的,但經過對mvp和RxAndroid各自特點的比較後,我還是決定把他們融合在一起使用。

原因:

•mvp可以較好的剝離和複用業務邏輯,但是付出的代價是程式碼量和檔案數量的增加

•Rx的優勢在於非同步和簡潔,非同步不是mvp的強項,簡潔正好可以解決mvp程式碼龐大的問題

•Rx+mvp可以各取所長,不過mvp一般是通過回撥來處理非同步,在程式碼編寫上需要轉換風格

•在框架上要使用mvp的分層分工思想,而在具體的邏輯實現上,使用Rx的鏈式程式碼去做資料的查詢、處理、顯示

在具體操作上,使用了兩種修改形式:

一種部分修改是,修改Presenter中的重新整理函式,從回撥改為RxAndroid:

 

另一種修改是,放棄複雜的MVP檔案結構,全部在Fragment中實現:

 

採用只保留Fragment的方式,看起來比起mvp方式是個倒退,但其實這反而是最合適的做法,事實上,某些業務場景下,使用mvp本來就是過度設計了,我在這個問題上的考慮如下:

•首先考慮業務情況,複用的是Fragment,而不是Presenter,那就只保留Fragment

•Presenter層的非同步執行緒可以在Fragment中用RxLifecycle替代時,也不必須在Presenter中實現非同步執行緒

•資料層只做網路資料訪問,並做資料轉換,完全可以用Retrofit替代

•生命週期全部託管至RxFragment,能減少一部分工作量並消除這部分風險

事情證明,這樣改造後,檔案減少、程式碼減少、可讀性好、可維護性高

我個人對這個專案的改造持肯定態度,我相信深入學習RxAndroid後,還有進一步優化的空間

幾點建議

•Rx涉及的陌生概念比較多,前期上手有一定障礙,要多動手去試

•Rx的功能非常多,這裡只講了冰山一角,建議先系統地瞭解一下RxJava

•要用事件訂閱的思路做開發,和基於回撥的思路有很多區別,要慢慢體會

•Rx的鏈式程式碼比較特殊,和其他形式的程式碼不能混搭,所以同一個業務模組要同時使用Rx,否則工作難以銜接

•還是因為程式碼形式不能混搭,舊專案要改Rx的話,也會遇到需要一口氣打通整條邏輯鏈路的問題



 

相關文章