[譯]使用MVI打造響應式APP(五):輕而易舉地Debug

卻把清梅嗅發表於2019-03-17

原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART5 - DEBUGGING WITH EASE
作者:Hannes Dorfmann
譯者:卻把清梅嗅

前文我們探討了Model-View-Intent (MVI)架構模式及其相關特性,在 第一篇文章 中,我們談到了 單項資料流的重要性應用狀態應該被業務邏輯驅動。本文我們將展示這種架構模式會怎樣回報開發者,它可以讓開發者在開發過程中更輕而易舉進行debug。

遇到過這樣的情況嘛?你得到了一個崩潰的報告,但是你無法復現這個BUG。聽起來似曾相識?我也是!在花了很多時間檢視堆疊跟蹤和專案的原始碼後,最終我選擇了放棄——關閉了這個issue,並提交了一個類似 無法復現 或者 某個Android生產商的某種特定的機型導致的特殊錯誤 的備註。

以我們的購物App舉例來說,在Home介面,使用者以某種方式進行下拉重新整理,但不知道為什麼,崩潰報告告訴我,當使用者執行下拉重新整理獲取最新資料的操作時,應用丟擲了一個NullPointerException

因此,作為開發人員,您啟動App並嘗試在Home介面進行下拉重新整理,但App並沒有崩潰, 它按照預期正常地執行。然後您開始仔細檢查自己的程式碼,但是就是找不到哪裡會導致NullPointerException的發生。你開啟了debug模式,一行一行逐步執行該介面相關的程式碼,但App仍然正常的執行—— 到底怎麼樣才能讓它在下拉重新整理時崩潰?

問題的根本在於你不能在App崩潰發生之前復現狀態,如果遇到崩潰的使用者可以在崩潰報告中提供他App的狀態(在崩潰發生之前)以及堆疊跟蹤,那不是很棒嗎?

通過 單向資料流Model-View-Intent ,這簡直輕而易舉。

使用者執行所有Intent介面對Model進行渲染時,我們很方便地能夠將它們進行列印,讓我們通過在HomePresenter中新增Log來為Home介面執行這樣的操作(具體程式碼請參考 第三節,該小節我們針對狀態摺疊器進行了探討)。

在以下程式碼片段中,我們使用Crashlytics(譯者注:一種崩潰報告工具),使用其它的崩潰報告工具也是一樣的:

class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {

  private final HomeViewState initialState; // Show loading indicator

  public HomePresenter(HomeViewState initialState){
    this.initialState = initialState;
  }

  @Override protected void bindIntents() {

    Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load first page"))
          .flatmap(...); // 載入資料的業務邏輯

    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
          .flatmap(...); // 載入資料的業務邏輯

    Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load next page"))
          .flatmap(...); // 載入資料的業務邏輯

    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
    Observable<HomeViewState> stateObservable = allIntents
          .scan(initialState, this::viewStateReducer) // 對狀態進行摺疊
          .doOnNext(newViewState -> Crashlytics.log( "State: "+gson.toJson(newViewState) ));

    subscribeViewState(stateObservable, HomeView::render); // 展示新的狀態
  }

  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
    ...
  }
}
複製程式碼

通過RxJava.doOnNext() 操作符,我們可以很輕鬆將每個intent和每個intentresult——也就是即將渲染在view層上的狀態進行列印。

我們將view的狀態序列化為json字串,現在,我們的崩潰報告變成了這樣:

[譯]使用MVI打造響應式APP(五):輕而易舉地Debug

現在來看看這些日誌,我們不僅能看到崩潰發生之前的最後一個狀態,而且還能看到使用者達到這個狀態所經歷的完整歷史記錄——為了保證可讀性,我將data欄位內的內容替換為了[...]:

  • 1.使用者啟動了App,通過載入首頁資料的intent,這樣loadingFirstPage的值為true,使得載入指示器展示了出來,同時資料也被載入完畢(data[…])。
  • 2.接下來使用者滾動列表,並達到了列表的底部,這觸發了載入下一頁資料的intent,並開始載入更多的資料(分頁),這也導致了loadingNextPage狀態的改變,它的值變成了true
  • 3.一旦分頁資料被載入成功,loadingNextPage狀態改變成了false,使用者再次重複操作達到了列表的底部,並又一次出發了觸發了載入下一頁資料的intent
  • 4.接下來使用者開始嘗試下拉重新整理的intent,這導致loadingPullToRefresh狀態變更為了true,然後,App突然發生了崩潰—— 這之後就沒有更多日誌了。

這些資訊如何幫助我們解決這個bug呢?顯然,我們知道使用者觸發了哪些操作,因此我們完全可以手動復現這個崩潰。此外,因為我們將App的狀態用json進行表現,因此我們可以簡單地使用最後一個狀態,反序列化json並將此狀態作為我們的初始狀態來修復該錯誤:

String json ="  {\"data\":[...],\"loadingFirstPage\":false,\"loadingNextPage\":false,\"loadingPullToRefresh\":false} ";
HomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);
HomePresenter homePresenter = new HomePresenter(stateBeforeCrash);
複製程式碼

接下來我們開啟了Debug除錯工具,並嘗試觸發下拉重新整理的intent,事實證明,如果使用者向下滾動頁面2次,則沒有更多資料可用,並且我們的App並沒有進行相應的處理,因此後續的下拉重新整理操作導致了崩潰。

結語

一個應用狀態隨時隨地 可快照App可以使我們開發人員的生活更加輕鬆。我們不僅能夠輕鬆的 復現崩潰,而且可以將狀態進行序列化來 編寫回歸測試,並且這幾乎沒有什麼成本。

請記住,這些便利只有在App的狀態遵循 單項資料流不可變純函式 的原則的情況下才能享受到(即被業務邏輯驅動),Model-View-Intent讓我們偏向了這種思想流派,而這個架構模式中有一個非常棒並且有效的額外的效果,那就是本文所提到的構建了一個 可快照App

可快照 的應用有什麼缺陷呢?顯然我們正在將App的狀態序列化(比如通過Gson).這增加了一些額外的計算資源的負荷,平均來算的話,狀態第一次被Gson序列化大約需要30毫秒,因為Gson必須使用反射來掃描類,以確定必須序列化的欄位。

Nexus 4上,狀態的連續序列化平均需要大約6毫秒。由於序列化在.doOnNext()中執行,雖然這通常在後臺執行緒上執行,但的確是這樣:我的App使用者必須比其它應用的使用者多等待6毫秒,才能在螢幕上看到新的狀態。

我的觀點是,這對於使用者來說也許並不明顯,但是對狀態進行 快照 的一個問題是,在崩潰時,崩潰報告工具從使用者裝置上傳到其伺服器的資料量要大得多—— 如果使用者通過wifi連線,這無關痛癢,但如果使用者處於行動網路下則可能會有一定的爭議。

最後,將狀態附加在崩潰報告中時,您可能會洩漏使用者的一些敏感的資料。針對這個問題,一個方案是不序列化敏感資料,但這可能導致連線到崩潰報告的狀態不完整(因此這些報告可能幾乎無用),另外一個方案則是將敏感資料進行加密——但這可能需要一些額外的CPU佔用。

總結一下:我個人認為這樣 可快照App有很多優點,但是,你可能需要做出一些權衡。也許您開始為內部版本或beta版本啟用App快照,以衡量它其產生的作用。


系列目錄

《使用MVI打造響應式APP》原文

《使用MVI打造響應式APP》譯文

《使用MVI打造響應式APP》實戰


關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章