Uber RIBs框架源码分析

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也提供了很多基础库以及插件供开发者提高效率,改天有时间再详细分析一下它提供的插件以及基础库。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容