Uber RIBs框架原始碼分析

黑島様發表於2018-03-07

Uber最近開源了他們的移動端框架RIBs,RIBs是一個跨平臺框架,支援著很多Uber的移動應用。RIBs這個名字,取自Router、Interactor、Builder的縮寫。

早在2016年,Uber就在Engineering the Architecture Behind Uber’s New Rider App一文中介紹了他們重構Uber app所採用的架構和技術,從原始碼我們能看出,RIBs就是VIPER模式的一個實現,並在VIPER的基礎上做了不少改進。

閱讀本文前需要了解VIPER模式,如之前不瞭解,可谷歌一下。

文章構成

文章將會分成三部分,第一部分介紹RIBs框架的基本組成。第二部分闡述框架需要解決的問題,以及RIBs怎麼解決這些問題。第三部簡述RIBs的特點。

1.RIBs的基本構成
2.主要問題的解決
  • RIBs如何處理生命週期
  • RIBs如何解決Android生命週期引起的RxJava記憶體洩漏
  • 元件間如何通訊
  • 如何處理元件間的解耦
3.RIBs的特點
  • Router樹
  • 單Activity應用
  • 易於單元測試

RIBs的基本構成

RIBs的元件主要由Router、Interactor、Builder、Presenter、View組成,按Uber的設計,Presenter和View不是必須的,應對UI無關的業務場景。除了Builder,其它幾個都是VIPER模式有的元件。

image

我們可以很容易地用他們提供的外掛生成初始程式碼,下圖是用IntellJ外掛生成的模板程式碼示例。

image

Router

RIBs的路由,和別的VIPER設計相同的是,都用於頁面的跳轉。

不同的是: 1.RIBs的Router維護了一個子模組的Router列表,同時負責把子模組的View新增到檢視樹上。 2.Router不和Presenter通訊,而是和Interactor通訊,從上面的架構圖能看出來。

image

Router類依賴Interactor,架構圖裡的Interactor會呼叫Router,來實現跳轉。而Router也會呼叫Interactor,但場景不多,有以下兩個:

1.handleBackPress,處理實體鍵的回退事件 2.向子模組傳遞savedInstanceState

Interactor

RIBs的互動器用於獲取資料,從伺服器或者從資料庫中,和別的VIPER大同小異。它依賴Presenter和Router,從架構圖中也能看出,Interactor會把資料Model傳給Presenter,Presenter再跟View互動,顯示到View上。而Presenter會處理View的點選呼叫,呼叫Interactor獲取資料或處理邏輯。

image

Builder

RIBs的Builder是VIPER設計模式裡沒有的東西,用於初始化Interactor、Router等元件,並且定義依賴關係。

image

可以看出,Builder依賴View、Router,在build方法中建立Interactor。各元件如何組合起來,如何初始化一直是個問題,這部分程式碼寫在Activity裡明顯會造成冗餘。在View、Router、Interactor其中一個裡負責建立也不符合它們的職責,用一個Builder類來負責建立符合邏輯。

View和Presenter

這兩部分的設計也很有意思。一般在MVP裡,我們會把Activity當做View,會有一個IView的介面,以及一個IPresenter的介面。如果按照面向介面的原則,VIPER框架可能有4個介面,如下圖所示:

image

這同時也帶來一個介面過多的問題,造成介面方法冗餘,例如Interactor呼叫Presenter,Presenter接著呼叫View,這三個介面內會有三個表達含義相似的方法,如Interactor內requestLogin(),Presenter裡updateLoginStatus(),View裡會有一個showLoginSuccess()。儘管是不同職責,未免過於累贅。

RIBs的Router、Interactor、View都無需定義介面,直接繼承基類。Presenter是唯一需要定義的介面,在Interactor內定義Presenter介面,View實現Presenter介面,然後通過Rxbinding繫結控制元件,Presenter單向呼叫View。

image

幾個主要問題的解決

1.RIBs如何處理生命週期

如果採用MVP模式,我們需要在Presenter裡有各種生命週期的方法,如果採用MVVM,我們需要在ViewModel裡面處理生命週期。VIPER則需要在Interactor裡處理生命週期。簡單來說,就是把Activity或者Fragment的生命週期回撥,對映到Interactor裡的相關方法。

有很多方法能達到這個目的,最原始的一種,是在Activity裡依賴Interactor,在每一個生命週期方法內,呼叫Interactor的相關方法。

@Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    interactor.onCreate();
  }

  @Override
  protected void onResume() {
    super.onResume();
    interactor.onResume();

  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    interactor.onDestroy();
  }
複製程式碼

另一種方法是使用Google提供的LifeCycle元件,在Interactor基類裡註解方法,然後通過getLifecycle().addObserver(Interactor)新增監聽。

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    @CallSuper
    public void onCreate() {
        mCompositeDisposable = new CompositeDisposable();
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    @CallSuper
    public void onStart() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    @CallSuper
    public void onResume() {

    }
複製程式碼

Uber採用的是第一種,在RibActivity基類裡獲取到router,在生命週期回撥裡dispatch到各個元件。

2.RIBs如何解決Android生命週期引起的RxJava記憶體洩漏

另一個跟生命週期息息相關的問題,就是如何解決RxJava可能會導致的記憶體洩漏問題。

一般我們會用RxLifecycle這個庫,RxLifecycle需要我們拿到RxActivity的引用,但在Interactor裡引用Activity不是好的實踐。沒有Android的Context引用的話,我們可以把Interactor當做一個純Java類進行單元測試,效率會比較高。另外RxLifecycle的作者也在Why Not RxLifecycle?一文中闡述了RxLifecycle存在的問題,並建議我們不要使用。

一個簡潔清晰的處理是用CompositeDisposable把RxJava請求存起來,在Interactor生命週期結束時統一釋放。

Uber的工程師可能覺得這麼做不優雅,開發了一個AutoDispose來處理這個問題。

//AutoDispose庫的使用
myObservable
    .doStuff()
    .as(autoDisposable(this))   // 一行程式碼解決記憶體溢位問題
    .subscribe(s -> ...);
複製程式碼

AutoDispose庫的原理和RxLifecycle大同小異,但在RxLifecycle的基礎上做了改進,例如它不需要傳遞一個RxActivity上下文,取而代之的是一個LifecycleScopeProvider介面。下面是Interactor裡的相關程式碼,這段邏輯其實就是AutoDispose庫的使用,不多做解釋了。

public abstract class Interactor<P, R extends Router>
    implements LifecycleScopeProvider<InteractorEvent> {

  private static final Function<InteractorEvent, InteractorEvent> LIFECYCLE_MAP_FUNCTION =
      new Function<InteractorEvent, InteractorEvent>() {
        @Override
        public InteractorEvent apply(InteractorEvent interactorEvent) {
          switch (interactorEvent) {
            case ACTIVE:
              return INACTIVE;
            default:
              throw new LifecycleEndedException();
          }
        }
      };

  private final BehaviorRelay<InteractorEvent> behaviorRelay = BehaviorRelay.create();
  private final Relay<InteractorEvent> lifecycleRelay = behaviorRelay.toSerialized();

  /** @return an observable of this controller's lifecycle events. */
  @Override
  public Observable<InteractorEvent> lifecycle() {
    return lifecycleRelay.hide();
  }

  @Override
  public Function<InteractorEvent, InteractorEvent> correspondingEvents() {
    return LIFECYCLE_MAP_FUNCTION;
  }

  @Override
  public InteractorEvent peekLifecycle() {
    return behaviorRelay.getValue();
  }
複製程式碼

3.元件間如何通訊

一般無論MVVM模式還是VIPER模式,我們都需要處理父元件與子元件的通訊問題,子元件間的平行呼叫問題。

同樣有很多種方法可以解決,RIBs的通訊圖示

riblet_comms.png
我們著重看一下Interactor的呼叫,從圖中看出,父子元件的通訊是通過介面以及Observable streams的方式。

/**
   * 在子元件定義介面
   */
  interface LoggedOutPresenter {

    Observable<Pair<String, String>> playerNames();
  }

/**
   * 在父元件實現介面,並注入到子元件中供子元件呼叫
   */
class LoggedOutListener implements LoggedOutInteractor.Listener {

    @Override
    public void requestLogin(UserName playerOne, UserName playerTwo) {
      // Switch to logged in. Let’s just ignore userName for now.
      getRouter().detachLoggedOut();
      getRouter().attachLoggedIn(playerOne, playerTwo);
    }
  }
複製程式碼

對於父元件呼叫子元件,Uber更推薦Observable streams的方式,父元件將observable data stream暴露給子元件的Interactor,當資料變化時,子元件做出響應。

4.如何處理元件間的解耦

RIBs在Builder處理View、Router、Interactor的依賴問題。以下是教學程式碼的一個例子

@dagger.Module
  public abstract static class Module {
    //提供子元件跟父元件通訊的介面例項
    @RootScope
    @Provides
    static LoggedOutInteractor.Listener loggedOutListener(RootInteractor rootInteractor) {
      return rootInteractor.new LoggedOutListener();
    }

    //提供Presenter例項
    @RootScope
    @Binds
    abstract RootInteractor.RootPresenter presenter(RootView view);

     //提供Router例項
    @RootScope
    @Provides
    static RootRouter router(Component component, RootView view, RootInteractor interactor) {
      return new RootRouter(
          view,
          interactor,
          component,
          new LoggedOutBuilder(component),
          new LoggedInBuilder(component));
    }
  }

  @RootScope
  @dagger.Component(modules = Module.class, dependencies = ParentComponent.class)
  interface Component extends
      InteractorBaseComponent<RootInteractor>,
      LoggedOutBuilder.ParentComponent,
      LoggedInBuilder.ParentComponent,
      BuilderComponent {

    @dagger.Component.Builder
    interface Builder {

      @BindsInstance
      Builder interactor(RootInteractor interactor);

      @BindsInstance
      Builder view(RootView view);

      Builder parentComponent(ParentComponent component);

      Component build();
    }
  }

  interface BuilderComponent {

    RootRouter rootRouter();
  }

  @Scope
  @Retention(CLASS)
  @interface RootScope {

  }
複製程式碼

Builder出了用於初始化各個元件外,還負責依賴注入,子Interactor的介面例項就是在Builder生成的。

3.RIBs的特點

  • 業務邏輯驅動app,而不是View驅動
  • 整個應用只有一個Activity
  • 易於單元測試

RIBs的Router基類裡維護了一個儲存子Router的List,由於維護了Router樹,在根Router裡我們能一層層往下找到任何一個子元件的Router。也是因為有了Router樹,單Activity成為可能。

  private final List<Router> children = new CopyOnWriteArrayList<>();

  //dispatch 子元件
  protected void dispatchDetach() {
    checkForMainThread();

    getInteractor().dispatchDetach();
    willDetach();

    for (Router child : children) {
      detachChild(child);
    }
  }
複製程式碼

RIBs文件中解釋了單Activity的原因,多Acitivity會導致在全域性中有更多的狀態,程式碼會不穩健。具體的情景可能還得探討,Android使用Activity作為頁面確實會導致一些問題,Activity並不像Router樹有著清晰的層次和邏輯結構。

It contains a single RootActivity and a RootRib. All future code will be written nested under RootRib. RIB apps should avoid containing more than one activity since using multiple activities forces more state to exist inside a global scope. This reduces your ability to depend on invariants and increases the chances you'll accidentally break other code when making changes.

至於單元測試,由於RIBs各元件的職責非常清晰,對Router和Interactor進行單元測試全覆蓋是非常容易的事。

總結

RIBs框架的程式碼量非常小,類不多,是一個短小精悍的框架。作為VIPER模式的一個具體實現,從設計上能看出Uber的工程師深思熟慮,並且用符合邏輯的方式去解決了開發中遇到的問題。寫一個VIPER框架並不難,用優美的方式去解決問題才是難點,Uber的工程師在這方面做得非常好。同時RIBs也提供了很多基礎庫以及外掛供開發者提高效率,改天有時間再詳細分析一下它提供的外掛以及基礎庫。

相關文章