活动/服务是一种依赖:重新思考优步司机应用程序的Android架构

0
活动/服务是一种依赖:重新思考优步司机应用程序的Android架构

本文是本系列的第十篇,也是最后一篇系列报道了优步的移动工程团队如何开发了代号为Carbon的最新版司机应用程序,这是我们拼车业务的核心组成部分。在其他新功能中,这款应用程序可以让我们超过300万的司机合作伙伴找到票价,获得方向,并跟踪他们的收入。2017年,我们结合司机合作伙伴的反馈,开始设计新的应用程序,并于2018年9月开始投入生产。

许多移动应用程序用于特定的、基于用户交互的任务,例如分享照片、发送消息或浏览信息。另一方面,优步的司机合作伙伴让优步司机应用程序在他们的手机上运行几个小时,查找票价,获得路线指导,等等检查他们的收入.大多数这些功能都需要用户打开应用程序,并将其放在手机的后台。

我们设计了原始的驱动程序应用程序服务符合一般使用的Android应用程序的最佳实践。然而,随着时间的推移,我们发现这种方法并不是最适合需要持续运行的应用程序,这会导致具有重复功能和意外行为的过于复杂的代码。

当我们开始的过程重写驱动程序,我们有机会重新思考这个架构,并缓解我们在之前的应用程序中发现的问题。

传统上,工程师在Android应用程序中开发功能代码活动或将服务作为锚。我们的新解决方案,建立在开源肋骨跨平台移动架构我们之前开发的新的骑手应用程序,其核心思想是活动和服务不必成为应用程序的结构基础。相反,rib让我们为应用程序构建一个体系结构,其中活动和服务不是核心组件的一部分,简化了业务逻辑并简化了代码。

虽然我们的方法不能直接适用于大多数类型的移动应用,但它应该为如何编写应用提供一个新的视角,比如导航服务和基于地理位置的游戏,这些应用通常会运行好几个小时。

我们使用服务的技术问题

在编写我们之前的驱动程序应用程序时,我们遵循了Android开发人员的一个常见模式:创建一个Activity并运行与它的UX相关的逻辑,同时创建服务来运行与UX无关的逻辑。根据驱动伙伴是在线还是离线,我们还需要启动一个前台服务即使前台没有活动,也可以让应用程序保持活跃。

当应用程序在后台或前台时,许多功能都需要可用。例如,当司机在等待下一次旅行时,他们可能会将应用程序放在后台,但我们仍然需要在它到达时显示调度报价。

优步司机应用界面
图1:当应用程序在前台(左)或后台(右)时,我们必须向司机合作伙伴显示调度报价。


我们旧的应用程序结构,如图2所示,使用多个服务来构建后台功能:

优步司机应用程序RIB树
图2:我们之前的驱动程序应用程序的简化结构包括多个使用服务实现的后台状态。


让我们来确定由结构或之前的驱动程序应用程序引起的一些症状:

  • 本来只需要存在于应用程序的一小部分中的特性类存在于应用程序作用域中。因此,我们最终得到了两个不同的提供屏幕组件,一个用于前景,一个用于背景。两者使用的任何共享类都不能假定用户已登录或用户在线,这就要求工程师在整个代码中添加额外的检查,并在内存状态失效时手动清理内存状态。(有关此主题的更多信息,请参阅我们之前的文章,用深度层次重写优步工程的Android Rider应用程序讨论问题由糟糕的范围引起。)
  • 特性代码有时会重复。考虑到需要为相同的特性(例如VOIP支持)编写前台和后台变量,为每个特性的变量独立实现代码似乎是合理的。在许多特性中重复这种模式会增加代码库的复杂性。
  • 提醒通知弹出每次应用程序是后台。由于某些特性在活动和服务内部重复,我们不能同时运行这些服务和活动。这意味着我们需要在每次将应用程序置于前台时关闭一些前台服务,然后在将应用程序置于后台时重新启动它们。重新启动这些服务会重新触发抬头通知的出现,这对驾驶伙伴来说是不必要的分心。
  • 决定应用程序在后台运行多长时间的逻辑分布在多个前台服务上。以这种方式扩展逻辑使得很难推断保持应用程序存活和耗尽电池的条件。

除了上述所有问题之外,使用服务也很困难。例如,在一小部分Android操作系统变体上,服务生命周期方法的调用顺序是错误的。在另一小部分设备上,前台服务如果没有奇怪的变通办法,就无法保持应用程序的持续运行。

我们希望在新版本的驱动程序中从结构上缓解这些问题。

我们的解决方案

我们重新构建Android驱动程序的核心方法可以归结为两个原则:

  1. 服务不需要是应用程序中的结构性组件。我们可以利用粘性前台服务的好处,而无需代码库中的任何特性来确认服务的存在。
  2. 活动是可选的。无论应用程序当前是否有Activity,应用程序中的许多高级状态转换、瞬态状态和屏幕的行为都是类似的。那么,为什么不构建这些特征类的一个版本,并允许它们选择如何改变它们的行为来响应前景或背景呢?

肋骨架构

新的驱动程序应用程序是使用路由器交互器构建器.总之,RIB体系结构允许我们创建模块化的、以业务逻辑为中心的组件。每个RIB都是由Interactor(业务逻辑)、Router(导航)和Builder(依赖)组成的独立组件,这些组件可以作为子组件添加到另一个RIB。(读在rib中构建Uber的新司机应用程序关于如何使用rib重写驱动程序的概述。)

Activity-independent代码

我们编写应用程序的核心层次结构是独立于Activity的存在的。例如,在下面的图3中,不管应用程序是否有一个Activity,当驱动程序在线时就会附加Online RIB。类似地,只要用户选择使用Uber的导航,导航RIB就会运行,而不管是否有一个活动来直观地显示导航方向。

优步司机应用程序和RIB树
图3:这个简化的层次结构显示了无论Activity是否存在都存在的驱动程序作用域。


在其他情况下,一些rib只编写一次,并在活动和非活动用例之间切换时重新实例化。例如,当司机应用程序收到骑手报价时,如果没有活动存在,在线RIB将offer RIB附加到一个窗口,如果活动存在,则将offer屏幕附加到OnlineView。当Activity附加到App根RIB时,Online RIB会观察到发出的View对象流,因此它知道何时在两个变体之间切换。Offer RIB本身独立运行,不受它是附加到窗口还是OnlineView的影响。

单一服务

应用程序中的所有服务代码都写在一个100行的文件中。这个服务的唯一工作是保持应用程序在后台存活,并在操作系统临时杀死应用程序以释放内存时重新启动应用程序。应用程序的其余部分通过增加或减少KeepAliveCount请求此行为。当计数大于0时,应用程序将通过粘性前台服务保持活跃。当计数减少到零以下时,前台服务被杀死。

例如,在图3中,当应用程序启动Online RIB时,KeepAliveCount会增加。

我们可以轻松监控这个单一服务,以确保它不会在生产中被滥用或不会导致电池耗尽。此外,在服务框架中,我们只需要在一个位置解决bug。

妥协

我们的解决方案并非没有代价。除了我们的方法提供的好处之外,它也产生了一些缺点:

  • 这种结构是非常规的,需要严格的一致性。为其他应用开发的一些现有功能需要它们自己的服务。我们需要重构这段代码,使其与服务无关。
  • 在许多情况下,工程师被迫思考他们的功能在后台和前台应该如何工作,而不管这些功能是否出现在后台。
  • 我们通过减少全局状态的数量来减少陈旧的状态问题。然而,这种方法引入了一种新的陈旧数据问题,其中一些肋骨比它们的视图更长寿,可能会从这些肋骨内部的视图中产生内存泄漏。虽然在传统的Android架构中不是常见的做法,但我们使活动和扩展视图成为可选的方法意味着我们需要响应地发出视图以将它们传递给rib。

前进

服务和活动不需要成为应用程序的架构组件。Android社区包含了许多构建应用程序的想法和哲学,这些想法和哲学适用于许多情况。我们的范例符合我们的架构原则,即专注于业务逻辑,并将View作为应用程序的rib结构中的外围组件。这种模式适用于像我们这样经常需要在后台和前台之间切换并保持运行数小时的应用程序。

优步司机应用程序系列的文章索引

  1. 为什么我们决定重写Uber的司机应用程序
  2. 在rib中构建Uber的新司机应用程序
  3. 优步新司机应用如何克服网络延迟
  4. 在Uber Eats扩大现金支付
  5. 如何在不危及整个业务的情况下重新编写应用程序
  6. 为司机建立一个可扩展和可靠的地图接口
  7. 工程优步信标:匹配乘客和司机在24位RGB颜色
  8. 为驱动程序首选项构建一个安全、可伸缩和服务器驱动的平台
  9. 在我们的新司机应用程序中构建实时收入
  10. 活动/服务是一种依赖:重新思考Uber新司机应用程序中的Android架构

对开发每天被数百万人使用的移动应用程序感兴趣?考虑加入我们的团队安卓iOS工程师!

评论