本文是本系列的第五篇系列报道了优步的移动工程团队如何开发了代号为Carbon的最新版司机应用程序,这是我们拼车业务的核心组件。除了其他新功能外,这款应用还能让我们超过300万的司机合作伙伴找到车费、指路和追踪他们的收入。2017年,我们结合司机合作伙伴的反馈开始设计新应用程序,并于2018年9月开始投入生产。
优步的用户依赖我们的应用程序作为访问我们服务的主要工具。建筑我们新的改进的驱动程序花了很多时间协同设计工作还有很多开发时间。为我们在世界各地的司机合作伙伴快速、无缝地推出这款应用,也进行了大量深思熟虑的规划;积极而顺利的发布体验对于确保司机合作伙伴能够继续依赖我们的平台,以及保持我们业务的完整性至关重要。
对于我们的Android版本的新驱动程序,我们不能冒险让它的发布对我们的用户产生负面影响,所以我们选择了一条不太可能的道路,即在一个包中发布两个应用程序或二进制文件。虽然这种方法并不常见,但我们可以将新应用作为测试版向特定城市的部分司机合作伙伴推出,同时保留其他司机在现有应用上的使用。
当我们发行Android版本的我们2016年的新骑手应用在那里,我们还将两款应用打包出售。
为了让驱动程序支持我们现有和测试版应用程序的组合包,我们必须对应用程序类、启动程序活动、接收器和服务进行更改,以便它们可以在独立或组合模式下运行。我们还需要添加逻辑,以在方便的时间优雅地切换模式,使这些更改对我们的驱动伙伴尽可能透明,让他们继续不受影响的业务。
这一推出策略已经证明了它的价值,因为我们让我们的新应用程序尽可能无缝地提供给超过300万名司机合作伙伴。
二合一应用程序
在一个包中发布两个版本的应用程序的想法是几个不同需求的结果。首先,当我们在2016年开发新的乘客应用程序时,优步平台还没有出现变成了单簧管因此我们使用了两个存储库,一个用于现有的应用程序,另一个用于新版本。通过从零开始开发我们的新应用,我们发现工程师能够快速迭代,在没有任何技术债务的干净空间中设计解决方案,并使用新采用的技术来开发新应用巴克构建系统。这种方法还确保了新应用程序不会意外地泄露到旧应用程序的二进制文件中,这可能会被狂热者或竞争对手反编译,泄露重写计划。
我们在旧应用所在的monorepo上构建了新的驱动程序应用,但直到开发后期才将两个应用合并为一个包。在早期将这两个应用分开,我们可以不断更新旧应用,同时发布新应用的独立测试版。新的测试版应用支持我们的测试版程序,我们将其发布给世界各地的一组选定的驾驶员,收集有价值的反馈。当我们的最终发行临近时,利用一个包含两个应用的包让我们能够更好地控制发行过程。
其次,虽然谷歌Play Store提供了一些工具,可以让开发者轻松地按百分比设置铺开,甚至控制特定市场的铺开,但我们需要对这个版本进行细粒度的控制,因为应用程序需要根据城市级别的不同环境和政策进行调整。除了位置,时间也非常重要,因为我们不想在旅途中启动更新,导致司机失去导航和车费计算等应用功能。我们也表演彻底的A/B测试重新实现或新设计的功能,让我们对产品按照设计运行有信心。让一个应用包含新旧应用,让我们可以构建机制来控制用户、时间和区域的推出。
最后,我们需要保证安全的推出,并对应用程序在各种条件下仍然能够可靠地运行保持高度的信心。通过发布旧版本的应用程序和主要的重写,我们可以调整变量的推出或退回到一个应用程序有证明的稳定记录。
结合两个应用程序
将新旧应用打包在一起,我们称之为Dual Binary,将新应用作为一个Android库,并将其添加为旧应用的应用模块的依赖项。在此之前,我们首先需要将每个Application子类中的所有逻辑下推到一个称为AppDelegate的类中。这允许每个应用程序的应用程序级代码具有最小的表面面积,以便它可以轻松集成到任何需要的Application类中。
公共类DualBinaryApplication扩展应用程序{
AppDelegate;
@Override
公共无效onCreate() {
如果(BuildConfig。IS_DUAL_BINARY && shouldLaunchNewApp(this)) {
//单个二进制返回无操作AppDelegate
appDelegate = newappdelegate .create(this);
} else {
appDelegate = OldAppDelegateFactory.create(this);
}
}
}
出于我们的目的,我们有三个不同的构建:Dual Binary应用程序、单二进制旧应用程序和单二进制新应用程序。当我们想要构建Dual Binary应用程序时,我们设置了一个IS_DUALGradle属性为true,这是在旧应用的build.gradle中读取的。此属性控制新应用程序的代码是否作为编译依赖项添加,以及是否创建和设置BuildConfig。IS_DUAL是通过Android Gradle配置中的buildConfigField实现的。使用新的和旧的AppDelegate类可用时,我们可以将逻辑插入到旧应用程序的Application中子类来控制在Dual Binary应用程序启动时加载哪个AppDelegate。
我们仍然需要单二进制的旧应用程序,这样我们就可以在开发新应用程序的同时使用相同的每周构建计划继续发布它。我们添加了一个无操作模块作为包含NoOp AppDelegate的依赖项,如图1所示,它没有依赖项,并允许我们在双二进制逻辑下编译,因为我们将新应用程序作为单二进制发布。然后可以通过设置IS_DUAL来构建单二进制旧应用程序Gradle属性为false。单一的二进制新应用程序也是必要的,这样工程师就可以在开发阶段快速构建和迭代新产品。创建单个二进制新应用程序需要连接new AppDelegate到NewApplication。
类似于我们如何创建一个地方来为应用程序级代码引入切换逻辑,我们使用一个跳板活动来引入活动级代码。当这个活动启动时,它首先检查Application子类,看哪个AppDelegate被加载了,然后继续将意图转发到新或旧应用程序的主活动和调用完成()使跳板在任何UI显示之前消失。呼唤完成()配置了后退堆栈,这样当用户按下后退按钮时就不会尴尬地返回到旧的应用程序。
当使用跳板活动时,我们需要确保预期的活动意图标志被正确声明。此外,如果有人启动我们的应用程序的主入口点的结果,那么我们需要覆盖活动# getCallingPackage ()而且活动# getCallingActivity ()确保跳板传递正确的信息,以便将结果返回给适当的调用者。
我们需要考虑的其他入口点是接收者和服务。如果该组件没有在新旧应用程序之间共享,则应用程序子类将以编程方式使用PackageManager.setComponentEnabledSetting在加载应用程序委托之前。如果该组件在新旧应用程序之间共享,比如推送通知库模块中的接收器,那么在桥接到新应用程序或旧应用程序处理推送通知之前,该组件需要与Application子类检查哪个AppDelegate被加载了。
在将新应用程序合并到旧应用程序中时,我们必须考虑一些额外的代码更改,以使推出成功且无缝。这些是任何考虑类似于我们的推出方法的工程师可能会问的问题:
- 新应用的构建完成了吗?Gradle有任何配置需要移植到旧的吗?
- 都是相关的AndroidManifest.xml声明和设置是否正确合并?
- 双二进制应用程序是否声明了所有权限和功能的联合?
- 我们是否将任何用户/认证数据从旧应用程序的存储迁移到新应用程序?
- 新旧应用程序是否有同名的XML资源?如果是这样,那么我们可能会在UI中得到不可预测的结果。为了轻松避免这个问题,我们确保使用了资源前缀。
- 如果我们没有monoorepo,也就是我们推出rider应用时的情况,那么我们就需要为新应用的构件制定一个版本控制方案,并确保旧应用的版本与适当的构件兼容。
推出新的应用程序
Dual Binary应用内置了许多机制,以确保可控和安全的推出,如选择加入功能标志、客户端桶、实验终止开关和崩溃恢复。
私有boolean shouldLaunchNewApp(上下文上下文){
rolloutPrefs = new RolloutPreferences(context);
如果(rolloutPrefs.isCrashRecoveryForceOldApp ()
|| rolloutPrefs.isKillSwitchForceOldApp()) {
返回错误;
} else if (rolloutPrefs.isNewAppFeatureEnabled()) {
返回true;
} else {
String deviceId = DeviceUtils.getDeviceId(context);
int rolloutPercentForRegion = getNewAppRolloutPercent(context);
返回clientSideBucket(deviceId, newAppRolloutPercentForRegion);
}
}
我们不能用我们的服务器驱动的特征标记系统直接,因为这将需要初始化AppDelegate,与试图在应用启动时做出的决定相冲突。相反,我们选择采用第二种会话方法,其中活动AppDelegate监听LAUNCH_NEW_APP标志的服务器值,并将结果缓存到典型实验存储之外的SharedPreference中。当Dual Binary应用程序启动时,它读取SharedPreference值并启动适当的AppDelegate。
特性标志方法的缺点是,改变标志的服务器值需要在应用UI中反映任何更新之前第二次启动应用。这意味着我们无法测试新应用对新注册和登录的影响。为了解决这个问题,我们实现了客户端桶,在设备上本地决定特性标志的值。如果LAUNCH_NEW_APPSharedPreference为false,设备将生成一个介于0到100之间的随机数,该随机数将基于该设备的信息保持不变。如果生成的数字低于硬编码的每构建rollout,那么将启动NewAppDelegate。这种策略提供了一种安全的、渐进的推出,仍然支持对注册流进行验证。
如果Dual Binary的新应用模式有问题,或者客户端桶有问题,我们实现了一个额外的FORCE_OLD_APP特性标志。应用程序从服务器接收到的值被缓存到SharedPreferences,就像上面列出的标志一样。
因为我们的新应用程序使用服务器驱动的特性标志,所以在新模式下运行的应用程序仍然能够成功地从服务器接收值是很重要的。为了保护这一点,我们添加了一个叫做崩溃恢复的功能。这个轻量级、最小依赖的系统跟踪信号,比如应用程序启动的数量、来自特性标志服务器的网络响应的数量和应用程序生命周期。如果NewAppDelegate试图加载,但在启动序列中始终没有达到接收特性标志有效负载的足够距离,系统将执行越来越强的一系列恢复操作。在一次失败的启动后,系统将清空存储缓存。在另一个连续失败的发射后,系统清除本地实验标志值(除了少数白名单标志,如LAUNCH_NEW_APP标志)。如果应用程序试图第三次启动NewAppDelegate,并且在合理的时间内未能接收实验,那么Dual Binary应用程序将返回到旧的应用程序模式,直到下一次应用程序更新,确保司机有一个工作的应用程序,这样他们就可以继续接受乘车。
这些不同的机制有助于稳定应用新旧模式下的代码,但Dual Binary控制逻辑是应用启动时执行的第一个代码,因此需要同样稳定。在执行实际的铺开之前,我们通过模拟铺开并使用分析方法来确认适当数量的用户处于Dual Binary应用程序的适当模式中,从而对每种机制进行了测试。这一测试极大地增加了我们对Dual Binary控制逻辑的信心。
经验教训
在启用应用的新模式时,我们遇到了一些问题,这证明了我们的Dual Binary方法在应用开发中的价值。在一次这样的事件中,我们的数据科学家发现,新应用模式的业务指标在一个地区的表现有所下降。以更细粒度的方式调整推出百分比的能力让我们可以在其他地方继续推出,同时工程师纠正了区域问题,这被证明是一个缺失的支付流。如果没有Dual Binary,我们将不得不暂停整个构建过程数周甚至数月,同时进行研究和开发以解决这个问题,从而阻碍了除了必要的bug修复之外的其他功能的发展。雷竞技是骗人的
我们还了解到,在最后一级的推出控制机制上有一个服务器驱动的后退标志是至关重要的。我们过去通过为500个硬编码设备id的列表启用新的驱动程序应用程序来测试客户端桶。然而,由于设备id对单个设备并不是唯一的,所以新应用发布给的用户群比我们预期的要大得多,导致一些地区在我们打算推出新应用之前就访问了它。由于新的应用程序还没有在这些市场上稳定下来,我们通过更改FORCE_OLD_APP将这些区域强制回到旧的应用程序中标志。如果我们不能在这些市场中恢复到旧的应用,我们将不得不削减一个热修复构建来缓解这个问题。
我们的双二进制方法可能比简单地将用户从一个应用程序批量更新到另一个更复杂,但它已经证明了它的价值,通过无缝体验支持我们的司机合作伙伴。Dual Binary让我们在发布新应用时采取谨慎、谨慎的方法,同时提供了一个安全网,以防发布未能按计划进行。
优步司机应用程序系列文章的索引
- 为什么我们决定重写优步的司机应用
- 在rib中构建Uber的新司机应用程序
- 优步新司机应用如何克服网络延迟
- Uber Eats的现金支付规模
- 如何在不危及整个业务的情况下发布应用重写
- 为司机建立可扩展和可靠的地图接口
- 工程优步Beacon:匹配车手和司机在24位RGB颜色
- 为驱动首选项设计一个安全的、可伸缩的、服务器驱动的平台
- 在优步的新司机应用程序中建立实时收入追踪系统
- 活动/服务作为一种依赖:反思Uber新司机应用的Android架构
对开发每天被数百万人使用的移动应用程序感兴趣吗?考虑加入我们的团队安卓或iOS开发人员!






