Architecting Android with RxJava

最近,我抽出了幾個晚上的時間,把咖啡和啤酒變成了代碼與文字。

引子

三個月以來,我翻譯了一些關于RxJava的文章,說實話這些翻譯,真的搞得我很頭疼,那么現(xiàn)在是時候回來寫點什么了。

最近,我在看兩本書,《Learning Reactive Programming with Java 8》,《RxJava Essentials》,不過,沒關系,我已經(jīng)買到了電子版,我會在文章結尾附上網(wǎng)盤鏈接和密碼,但我還是希望你將文章繼續(xù)讀下去,因為那是文章結尾的事。

其實關于RxJava的文章和消息遠不止我們能了解到的,但又拜英語所賜,所以它看起來又沒那么多。好在,國內有許多優(yōu)秀的開發(fā)專家hi大頭鬼hi
BlackSwift,程序亦非猿Drakeet扔物線,流火楓林等等在為之做著貢獻,以及簡直不能更優(yōu)秀的文章《給 Android 開發(fā)者的 RxJava 詳解》。

但是,現(xiàn)在,我不得不再次做啰嗦一下,RxJava究竟會改變我們什么。

響應式編程Reactive Programming

什么是響應式編程呢?在Java程序中:

int a = 4;
int b = 5;
int c = a + b;
System.out.println(c); // 9
a = 6;
System.out.println(c);
// 9 again, but if 'c' was tracking the changes of 'a' and 'b',
// it would've been 6 + 5 = 11

當我們改變“a”和“b”的值時,“c”并沒有改變。換句話說,“a”和“b”的改變并沒有響應到“c”。這就是響應式:程序以流的形式,傳遞數(shù)據(jù)的改變

那我,我們又為什么需要響應式呢?

以下翻譯自《Learning Reactive Programming with Java 8》

10-15年前,對于網(wǎng)站開發(fā)來說,最平常的日常工作就是進行維護和縮短響應時間,那么今天,一切程序都應該保證七天二十四小時不間斷運行,并且能夠極快的做出響應;如果你的網(wǎng)站響應慢或者宕機,那么用戶將會對你們真愛一秒變備胎,轉而選擇其他網(wǎng)站服務。當今的慢意味著不可用甚至是有故障的。如今的互聯(lián)網(wǎng)是在和大數(shù)據(jù)打交道,所以我們需要快速的處理數(shù)據(jù)。

過去的幾年中HTTP錯誤已經(jīng)不是什么新鮮事了,但是現(xiàn)在,我們不得不進行容錯機制,還要提供用戶易讀以及合理的消息更新。

在過去,我們寫簡單的桌面應用,但如今我們寫能夠做出快速響應的Web應用。多數(shù)情況下,這些應用要與大量的遠程服務器進行數(shù)據(jù)傳遞。

如果我們想讓自己的軟件保持競爭性,就不得不實現(xiàn)這些新需求,所以,換言之就是我們應該這樣做:

  • 模塊的/動態(tài)的:用這種方式,我們就能夠擁有一個七天二十四小時的系統(tǒng)了,因為這些模塊能夠在不停止整個系統(tǒng)的情況下進行脫機和聯(lián)機。另外,隨著系統(tǒng)的不斷龐大,還能幫助我們更好地組織應用結構,同時還能管理底層代碼。
  • 可擴展的:用這種方式,我們就能夠處理大量的數(shù)據(jù)和用戶請求了。
  • 容錯性:用這種方式,能夠為用戶提供穩(wěn)定的系統(tǒng)。
  • 響應式:這不僅意味著快速,還意味著可用性強。

讓我們思考如何實現(xiàn)它:

  • 如果我們的系統(tǒng)是事件驅動型的,那就把它模塊化。我們可以將系統(tǒng)分成多個彼此之間通過通知進行交互的微服務/組件/模塊。這樣,我們就能夠以通知為代表,響應系統(tǒng)的數(shù)據(jù)流了。
  • 可擴展意味著能夠應對日益增長的數(shù)據(jù),在負載的情況下不會崩潰。
  • 對故障/錯誤做出及時的響應,能夠提高系統(tǒng)的容錯性。
  • 響應意味著對能夠對用戶操作及時的做出反應。

如果應用是事件驅動型的,那么,它就能夠解耦成多個自包含組件。這能夠幫我們更好的實現(xiàn)擴展性,因為我們總是可以在不停掉或者打斷系統(tǒng)的情況下添加新組建或者移除舊組件。如果錯誤和故障傳遞給正確的組件,把它們當做通知來處理并作出響應,那么應用能變得更具有容錯性和彈性。所以,如果把系統(tǒng)構建成事件驅動型的。我們可以更容易的實現(xiàn)擴展性和容錯性,而且一個具有擴展性,低耦合和防錯的應用能夠快速的響應用戶操作。


Reactive Manifesto文檔定義了我們剛剛提到的四點響應式準則。每一個響應式系統(tǒng)都應該是消息驅動型(事件驅動型)的。這樣它不僅能變得低耦合,而且擴展性和容錯性將更高,這就意味著它可靠和具有響應式。

要注意的是,Reactive Manifesto只是描述了一個響應式系統(tǒng),并不是對響應式編程的定義。當然,你也可以不使用任何響應式類庫或者語言,打造一款彈性可擴展,具有消息驅動的響應式應用。

應用程序中數(shù)據(jù)的變化,以通知的方式傳遞給正確的Handler。所以,使用響應式構造應用是符遵循Manifesto最簡單的方式。

回調地獄

如果你是一個能夠時刻保持頭腦清醒,邏輯清晰和思維縝密的人,是個Callback高手,善用并且能夠用好FutureTask。

那么在Android中你的代碼可能會頻繁的使用async+callbacks,或者service composition+ error handing

那么關于異步回調的邏輯,你會寫成這樣getData(Callback<T>)、這樣Future<T> getData(),還是這樣Future<List<T>> getData(),甚至這樣Future<List<Future<T>>> getData(),嗷!拜托,我簡直不能再舉例下去了,這簡直就是Callback Hell,這樣的程序或許寫起來很舒服,但是如何測試和維護呢。

如果哪天你的程序出了問題而必須馬上修復,但你卻不能馬上趕來或者需要別人協(xié)助(這在很多公司是很常見的),或者當他人在review你的代碼時,那么,是時候拿出這張圖了。

然而使用RxJava的操作符,我們可以避免這些煩人甚至糟糕的回調,讓結構和思路看起來更清晰,通過組合API,只需要約定最終的結果Observable<T>就行了。

并且scheduler的出現(xiàn),不僅解放了線程的切換,讓UI線程與工作線程間的跳轉變得簡單,而且,它的API很豐,也提供了很多使用場景的建議,比如,適用計算任務的Schedulers.computation(?);處理密集IO任務的Schedulers.io(?);以及Schedulers.trampoline(?)能夠有效避免StackOverflowError,所以非常適合函數(shù)的遞歸調用。好了,我不再舉例了,因為官方文檔已經(jīng)給出了很詳細的解釋了,但是值得一提的是,如果使用Schedulers的工廠方法創(chuàng)建的Worker,一旦任務執(zhí)行完畢,都應該調用worker.unsubscribe( )方法,然后轉向之前定義的Scheduler實例上來。

當然RxJava的出現(xiàn)并不僅僅是為了解決回調地獄的。

這是我通過學習和不斷地練習,一路走來很辛苦,總結的一些經(jīng)驗,分享給大家:

1 . error handling

2 . lifecycle changes

3 . caching (roation)

   @Override public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setRetainInstance(true);

      /*.cache()操作符:
        當?shù)谝粋€subscribe訂閱的時候,才會連接原始Observable,緩存事件,重發(fā)給后續(xù)訂閱的subscribe
        值得注意的事,它和使用了.replay()操作符的ConnectableObservable的不同。
        另外,為了避免內存開銷,不建議緩存大量事件*/
      cacheObservable = weatherManager.getWeather().cache();
    }

    @Override public void onViewCreated(View view, Bundle savedInstanceState) {
      super.onViewCreated(view, savedInstanceState);
      cacheObservable.subscribe(/*your subscribe*/);
    }

4 . composing multiple calls

5 . more robust interface than asyncTask

6 . easy to do complex threading

7 . functional nature is more expressive

    /*一個數(shù)組,每個元素乘以2,然后篩選小于10的元素,放入集合中*/
    Integer[] integers = { 0, 1, 2, 3, 4, 5 };

    /*一般寫法,看上去并不是那么的“函數(shù)”*/
    Integer[] doubles = new Integer[integers.length];
    for (int i = 0; i < integers.length; i++) {
      doubles[i] = integers[i] * 2;
    }
    List<Integer> integerList = new ArrayList<>(doubles.length);
    for (Integer integer : doubles) {
      if (integer < 10) integerList.add(integer);
    }


    /*Observable寫法,一切都好多了*/
    List<Integer> funactionalList = Observable.from(integers).map(new Func1<Integer, Integer>() {
      @Override public Integer call(Integer integer) {
        return integer * 2;
      }
    }).filter(new Func1<Integer, Boolean>() {
      @Override public Boolean call(Integer integer) {
        return integer < 10;
      }
    }).toList().toBlocking().first();

8 . async unit testing

9 . fluent API

10 . easy debugging

//值得一提的是,關于@RxLogSubscriber要放在繼承自Subscriber的類上
@RxLogSubscriber class MySubscriber extends Subscriber<Void> {
        @Override public void onCompleted() {
        }
        @Override public void onError(Throwable e) {
        }

        @Override public void onNext(Void aVoid) {
        }
      }

//而不是實現(xiàn)Observer接口的類上
@RxLogSubscriber class MySubscriber implements Observer<Void> {
        @Override public void onCompleted() {
        }
        @Override public void onError(Throwable e) {
        }

        @Override public void onNext(Void aVoid) {
        }
      }

當然,隨著學習的深入,你會發(fā)現(xiàn),收益不止如此。

在響應式編程中,應該牢記以下兩點:

  • everything is a stream(一切皆流)

  • don't break the chain(不要打斷鏈式結構)

談談Backpressure

Android這種嵌入式系統(tǒng),尤其是生產(chǎn)者-消費者(producer-consumer)模式中,一定要小心Backpressure(背壓,反壓)的出現(xiàn)。一個寬泛的解釋就是:事件產(chǎn)生的速度比消費快。一旦發(fā)生overproducing,當你的鏈式結構不能承受數(shù)據(jù)壓力的時候,就會拋出MissingBackpressureException異常。

在Android中最容易出現(xiàn)的Backpressure就是連續(xù)快速點擊跳轉界面、數(shù)據(jù)庫查詢、文件掃面、鍵盤輸入,甚至聯(lián)網(wǎng)等操作都有可能造成Backpressure,可能有些情況并不會導致程序崩潰,但是會造成一些我們不想見到的小麻煩。那么一起來看看如何用RxJava解決Backpressure,OK,讓我們的程序變得健壯起來吧。

groupBy操作符

在寫這篇文章的時候,剛好看到一段代碼,看來有必要說一說這個操作符了。

.groupBy( ),分組操作符,雖然目前這個項目中沒有用到,但是我還是蠻喜歡它的,而且我看到很多人在使用,將原始Observable根據(jù)不同的key分組成多個GroupedObservable,由原始Observable發(fā)射(原始Observable的泛型將變成這樣Observable<GroupedObservable<K, T>>),每一個GroupedObservable既是事件本身也是一個獨立的Observable,每一個GroupedObservable發(fā)射一組原始Observable的事件子集。

引用自:GroupBy中文翻譯

注意:groupBy將原始Observable分解為一個發(fā)射多個GroupedObservable的Observable,一旦有訂閱,每個GroupedObservable就開始緩存數(shù)據(jù)。因此,如果你忽略這些GroupedObservable中的任何一個,這個緩存可能形成一個潛在的內存泄露。因此,如果你不想觀察,也不要忽略GroupedObservable。你應該使用像take(0)這樣會丟棄自己的緩存的操作符。
如果你取消訂閱一個GroupedObservable,那個Observable將會終止。如果之后原始的Observable又發(fā)射了一個與這個Observable的Key匹配的數(shù)據(jù),groupBy將會為這個Key創(chuàng)建一個新的GroupedObservable。

那么問題恰恰出在.take(n)操作符上。


只返回前面指定的n項數(shù)據(jù),然后發(fā)送完成通知,忽略后面的事件。

那么看一下這個例子:

Observable.just(0, 1, 2, 3, 4, 5).groupBy(new Func1<Integer, Boolean>() {
          @Override public Boolean call(Integer integer) {
            return integer % 2 == 0;
          }
        }).flatMap(new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
          @Override
          public Observable<Integer> call(GroupedObservable<Boolean, Integer> groupedObservable) {

            return groupedObservable.getKey() ? groupedObservable.take(1) : groupedObservable;
          }
        }).subscribe(new Action1<Integer>() {
          @Override public void call(Integer i) {
            System.out.println(i);
          }
        });

輸出結果:

0
1
2
3
4
5

然而在1.0.0-RC5之前的版本中,在GroupedObservable上使用.take(n)操作符將會在發(fā)送完n個事件后,對GroupedObservable進行unsubscribe。并且GroupedObservable內部將會記錄這個unsubscribed狀態(tài),然后忽略后面的事件。所以輸出結果將是這樣的:

0
1
3
5

而在這之后的版本,使用.take(n)操作符,雖然也會發(fā)生unsubscribe,但是當原始Observable再次發(fā)送一個滿足key的事件后,將會重新創(chuàng)建一個GroupedObservable,然后發(fā)送這個GroupedObservable,不會發(fā)生之前那樣的,忽略后續(xù)事件的現(xiàn)象。

當然,不要忘記,對不感興趣的GroupedObservable使用.take(0),來避免泄露。

所以,我的建議是,在使用RxJava之前看看官方文檔或者change log。

關于RxWeather

我盡量減少對這個工程的文字描述。因為代碼才是最好的老師。

通過對Android技術棧,1#架構譯文)和Android架構演化之路譯文)的解讀和學習,按照架構和思路進行了實現(xiàn),并且加入了RxBus。

關于REST API,我選擇了和風天氣,而放棄了Openweathermap的理由如下:

  1. Openweathermap免費用戶所在的服務器不穩(wěn)定。

  2. 付費方面,和風天氣更經(jīng)濟實惠。

但是和風天氣目前并不支持同時查詢多個地區(qū)的天氣預報,也不支持根據(jù)經(jīng)緯度查詢天氣預報。但是以后的事情誰又能說的準呢?

由于應用并不支持動態(tài)的上拉加載。所以,所有的列表展示結果,取決于city.txt文件。

我從Openweathermap給出的資源(下載city.list.json)中,整理需要的城市Json字符串,整合了經(jīng)緯度,以備不時之需。

找到了一個通過Location查詢所在地的API。

就這樣基本實現(xiàn)了列表展示頁ListActivity的功能:

  1. 根據(jù)Loaction查詢所在地城市名稱,然后查詢當?shù)靥鞖狻?/p>

  2. 讀取domain->assets->city.txt,然后依次查詢每個城市的天氣,所以,這個文件不建議放入太多json。

  3. 整合1和2的并發(fā)請求結果,顯示界面。

詳情頁DetailActivity通過RxBus發(fā)送黏性事件接收列表頁傳遞過來的數(shù)據(jù),然后進行展示。這里會有七天內的天氣以及穿衣建議。由于我么并沒有找到一個正確的算法,所以當進入詳情頁后,旋轉屏幕之后的退出動畫會有所不同。這個類涉及的代碼大部分都是動畫(注意Hardware Layer的使用)以及對屏幕旋轉的處理,所以代碼看起有點多。ForkView使用了一個簡單的自定義Behavior。

搜索界面SearchActivity,輸入的關鍵字請不要以市、區(qū)結尾,例如,北京而不是北京市,因為API不支持,我也沒辦法 :( 。

啟動頁

我認為,出彩的引導頁是對細節(jié)的重視,但是我實在不能忍受,在啟動頁等太久。注意:不要混淆這兩種場景。

所以,我在看了正確使用啟動頁之后,決定采取這種方式實現(xiàn)SplashActivity。而且不建議使用大圖,一個icon足以。

Code

所有代碼都可以從Github上獲得。

片尾Tips:

文章開頭提到的資料,需要pdf或者kindle版本的請自行選擇下載,不得用于商業(yè)用途。

Learning Reactive Programming with Java 8 - pdf版,提取密碼:2d88。

Learning Reactive Programming with Java 8 - kindle版,提取密碼:5nec。

RxJava Essentials - pdf版,提取密碼:z3r8。

RxJava Essentials - kindle版,提取密碼:l67e。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容