如何在不影响整个业务情况下重构App

如何在不影响整个业务情况下重构App
Carbon图

本文是 Uber的客户端工程师团队讲述了如何开发最新版本司机端系列文章中的第五篇,该系列代号Carbon,是我们共享出行业务的核心。包括其它功能在内,Uber 司机端使得超过 300 万名司机可以查看费用、里程以及收益情况。2017 年我们结合司机的反馈开始对司机端进行重新设计,在 2018 年 9 月份投入使用。

用户所使用的Apps是访问我们服务的主要工具。构建新的和改进的司机端需要大量的 设计工作 和许多开发工作。为司机提供快速、无缝的切换新应用程序,需要深思熟虑的计划;好的用户体验是保证司机可以继续使用我们平台的关键,也是维护业务完整性的关键。

对于新安卓司机端中,我们不敢冒险,担心会影响用户,所以我们采用新的方式,开发两个二进制包。虽然不常见,但是这个可以让我们在特定城市按照比例开放测试版本,其他司机仍使用现存版本。

在2016年发布新乘客端时,我们在安卓版本下采用的这个策略,积累了一定经验。

为了司机端支持包含两个二进制包的组件包,我们需要改变应用类,启动器,接收器,和服务以便它们能在独立和组合模式下运行。我们也需要更加逻辑代码,在不影响司机正常接单的情况下,可以进行 新旧功能顺畅地切换。

这种策略证明了它的价值,因为我们让新应用程序为300多万司机无缝使用新应用程序。

两个应用合二为一

在一个包中开发两套代码的想法是不同需求的结果。首先,在2016年开发新的乘客端时,Uber平台还没有过渡到一个库,所以我们使用两个库。一个用于现在的应用程序,另一个是新的。通过从头开始构建新程序,我们发现如果没有技术债务工程师可以快递迭代,设计解决方案,并且使用新技术Buck构建新程序。这个方法还确保了新应用程序不会泄露程序代码,这些文件可能被一些技术爱好者和竞争对手反编译并泄露我们的重构计划。

我们在旧应用程序的仓库中创建新应用程序,但是直到开发后期才将两个应用程序合并到一个包中。最初将两个程序分开,发布新应用程序测试版本的同时,也持续维护旧代码的程序。新测试程序支持发布给特定区域,收集有效反馈信息。随着正式版本的发布,一个包中包含两套程序可以更好的控制发布过程。

其次,虽然Google Play提供了可以让开发者轻松按照比例设置部署的工具,甚至控制特定市场的工具,我们仍需要对版本进行更细度的控制。因为APP需要根据不同城市级别的不同环境和策略进行调整。除了位置,时间也很重要,因为我们不想在乘客使用中更新程序。除了位置,时间也很重要,因为我们不想在旅途中启动更新,这会导致司机失去应用程序的功能,比如导航和车费计算。我们还对重新实现或新设计的特性执行彻底的A/B测试,以增强我们对产品按设计运行的信心。有一个包含新旧应用程序的应用程序可以让我们构建机制来控制用户、时间和区域的部署。

最后,我们需要确保应用程序在各种条件下仍能可靠运行的安全性,并且抱有高度自信。通过旧版本应用程序和新版本一起发布,我们可以调整或者回滚到可用稳定版本的程序中。

将两个应用结合

将新旧程序打包在一起,成为Dual Binary。这个涉及到将新的程序作为安卓依赖库添加到就的应用程序中。在此之前,我们需要将每个应用程序子类的所有逻辑添加到AppDelegate中。这允许每个应用程序的应用级代码具有更小的内存管理,以便轻松集成到任何需要的应用程序中。

public class DualBinaryApplication extends Application {
**  private AppDelegate appDelegate;**

**  @Override**
**  public void onCreate() {**
**    if (BuildConfig.IS_DUAL_BINARY && shouldLaunchNewApp(this)) {**
**      // single binary returns no-op AppDelegate**
**      appDelegate = NewAppDelegateFactory.create(this);**
**    } else {**
**      appDelegate = OldAppDelegateFactory.create(this);**
**    }**
**  }**
}

为了达到我们的目标,我们有三个不同的构建,the Dual Binary app, single binary old app, and single binary new app.当我们想要构建Dual Binary app时,我们将IS_DUAL Gradle属性设为YES,这个属性在旧的应用build.gradle中可读。这个属性控制了新应用程序的代码是否作为可编译依赖添加,已经通过Android Gradle配置中的buildConfigField确定是否创建和设置BuildConfig.IS_DUAL。因为新程序和旧程序类均可用,我们可以在旧应用中插入逻辑代码,当Dual Binary启动时控制哪个AppDelegate加载。

两个apk结构示意图

图1:Dual Binary打包允许我们在新旧应用程序中构建多种配置。

单个binary的旧程序仍然需求,因为我们需要同时开发新应用程序的时候继续使用它。我们添加了一个无操作模块作为一个依赖,这个依赖包含一个无操作AppDelegate,如上图1所示。零依赖编译Dual Binary逻辑时,将发布新应用程序作为一个单独的binary。然后,可以通过将IS_DUAL Gradle属性设置为false来构建单个binary旧应用程序。为了让工程师能够在开发阶段快速构建并迭代新产品,单binary新应用程序也是必要的。创建一个binary新应用程序需要将新的AppDelegate连接到新应用程序。

类似于我们如何创建一个为应用程序级代码引入交换逻辑的内存,我们使用一个跳板活动来引入活动级代码。启动此活动时,它首先与应用程序子类一起检查加载了哪个AppDelegate,然后将意图转发给新应用程序或旧应用程序的主活动,并调用finish()使跳板在显示UI之前消失。finish()调用配置了back堆栈,这样当用户按下back按钮时,不会执行到原来的应用程序中。

在使用springboard活动时,我们需要确保正确地声明了预期的活动意图标志。此外,如果有人启动了应用程序的一个结果的主入口点,那么我们需要覆盖活动#getCallingPackage()和活动#getCallingActivity(),以确保跳板传递了正确的信息,以便将结果返回给适当的调用者。

我们还需要考虑两个点,接收者和服务者。如果在新旧应用程序之间没有共享组件,那么在加载应用程序委托之前,应用程序子类将在加载程序delegate前使用[PackageManager.setComponentEnabledSetting]启用/禁用它(https://developer.android.com/reference/android/content/pm/PackageManager.html#setComponentEnabledSetting(android.content.ComponentName,%20int,%20int))。 如果组件是在新应用程序和旧应用程序之间共享的,比如推送通知库模块中的接收器,那么在连接到新应用程序或旧应用程序对推送通知的处理之前,组件需要检查应用程序子类,以查看加载了哪个AppDelegate。

在将新应用程序合并到旧应用程序时,我们必须考虑对额外的代码量,以使发布成功且无缝切换。这些考虑采用类似于我们的发布方法的工程师可能会问的问题:

  • 完成了新应用移植到旧应用中的build.gradle配置吗?
  • 相关的AndroidManifest。xml声明和设置是否正确合并?
  • Dual binary应用程序是否声明了所有权限和特性的联合?
  • 我们是否将任何用户/身份验证数据从旧应用的存储迁移到新应用?
  • 新旧应用程序有相同名称的XML资源吗?如果是这样,那么我们的UI可能有不可预知的结果。为了轻松避免这个问题,我们确保使用了资源前缀。
  • 如果像我们发布rider应用程序时那样,我们没有单个仓库,那么我们需要为新应用程序的工件制定一个版本控制方案,并确保旧应用程序的版本与相应的工件兼容。

推出新应用

Dual Binary应用程序内置了许多机制,以确保可控的和安全的推出,比如可选特性标志、客户端配置、实验终止开关和崩溃恢复。

private boolean shouldLaunchNewApp(Context context) {
** rolloutPrefs = new RolloutPreferences(context);**
** if (rolloutPrefs.isCrashRecoveryForceOldApp()**
**       || rolloutPrefs.isKillSwitchForceOldApp()) {**
**   return false;**
** } else if (rolloutPrefs.isNewAppFeatureEnabled()) {**
**   return true;**
** } else {**
**   String deviceId = DeviceUtils.getDeviceId(context);**
**   int rolloutPercentForRegion = getNewAppRolloutPercent(context);**
**   return clientSideBucket(deviceId, newAppRolloutPercentForRegion);**
** }**
}

我们不能直接使用我们的服务器驱动系统,因为这将需要初始化一个AppDelegate,这与在应用程序启动时做出的决策相冲突。相反,我们选择采用第二种会话方法,其中活动AppDelegate侦听LAUNCH_NEW_APP标志的服务器值,并在实验存储之外的SharedPreference中缓存结果。当Dual Binary应用程序启动时,它读取SharedPreference值并启动相应的AppDelegate。

特性标志方法的缺点是,对标志的服务器值的更改需要再次启动程序,并在应用程序UI中反映任何更新。这意味着我们无法测试新应用程序对新注册和登录的影响。为了解决这个问题,我们实现了客户端嵌套,在设备上本地决定特性标志的值。如果LAUNCH_NEW_APP SharedPreference的值为false,设备将生成0到100之间的随机数,该随机数将根据设备的信息保持不变。如果生成的数字低于硬编码的每个构建的rollout,那么将启动NewAppDelegate。该策略提供了一个安全的、渐进的部署,仍然支持注册流程的验证。

如果Dual Binary新应用程序模式出现问题,或者客户端嵌套出现问题,我们实现了一个额外的FORCE_OLD_APP特性标志。应用程序从服务器接收到的值被缓存到SharedPreferences,就像上面列出的标志一样。

由于我们的新应用程序使用服务器驱动,所以在新模式下运行的应用程序仍然能够成功地从服务器接收数据是非常重要的。为了保护它,我们添加了一个称为崩溃恢复的特性。这是个轻量级的、最小依赖项的系统跟踪信号,例如应用程序的启动数量、来自特征标志服务器的网络响应数量和应用程序的生命周期。如果NewAppDelegate试图加载,但在启动序列中始终没有足够的时间来接收特性标志负载,那么系统将执行一系列越来越强大的恢复操作。在一次失败的启动之后,系统清除了存储缓存。在另一个连续失败的启动之后,系统清除本地实验标志值(除了一些白名单的标志,如启动_NEW_APP标志)。如果应用程序试图启动第三次NewAppDelegate并未能在合理的时间内接收数据,然后双二进制应用程序恢复回原来的应用模式,直到下一个应用更新,确保司机有一个能工作的应用程序,这样他们就可以继续接单。

这些不同的机制有助于在应用程序的新旧模式下稳定运行,但Dual Binary控制逻辑是在应用程序启动时执行的第一个代码,因此需要同样稳定。执行正式的推出之前,我们在真实环境测试了每个机制,通过模拟推广和使用分析来一定比例用户的在适当模式下Dual Binary应用程序。这种测试大大增加我们对Dual Binary的信心。

经验学习

在启用新模式应用程序时,我们遇到了一些问题,这些问题证明了的Dual Binary方法的价值。例如在这样的事件中,我们的数据科学家发现,在一个地区,新的应用程序模式的业务指标有所下降。以更细粒度的方式调整推出百分比的能力,让我们可以在其他地方继续推出,而工程师们则可以解决区域问题,这最终是一个缺失的支付流程。如果没有Dual Binary文件,我们将不得不在研究和开发解决这个问题时暂停工程,可能数周可能数月,在必要的bug修复之后也要阻止其他问题的发生。

我们还了解到,回滚机制非常重要,回退到最后一层使用服务器驱动的后退标志。我们过去通过启用新的驱动程序应用程序来测试客户端嵌套,该应用程序包含500个硬编码设备id。然而,由于设备id不是单一设备所独有的,新应用程序的用户群比我们预期的要大得多,导致一些地区在我们打算推出新应用程序之前就访问了它。由于新应用程序还没有为这些市场稳定下来,我们通过更改服务器上的FORCE_OLD_APP标志,迫使这些地区回到旧应用程序。如果我们不能在这些市场上恢复到旧的应用程序,我们将不得不削减一个热修复构建来缓解这个问题。

Dual Binary方法可能比简单地将用户从一个应用程序批量更新到另一个应用程序要复杂,但它已经证明了司机可以通过无缝体验支持我们的驱动程序是有价值的。这Dual Binary让我们在交付新应用程序时采取一种谨慎、慎重的方法,同时提供了一个安全网,以防发布没有按计划进行。

优步司机app系列文章

  1. Why We Decided to Rewrite Uber’s Driver App
  2. Architecting Uber’s New Driver App in RIBs
  3. How Uber’s New Driver App Overcomes Network Lag
  4. Scaling Cash Payments in Uber Eats
  5. How to Ship an App Rewrite Without Risking Your Entire Business
  6. Building a Scalable and Reliable Map Interface for Drivers
  7. Engineering Uber Beacon: Matching Riders and Drivers in 24-bit RGB Colors

猜你喜欢

转载自juejin.im/post/5c7cea1b51882507ae09dba6
今日推荐