如何在不影響整個業務狀況下重構App

如何在不影響整個業務狀況下重構App
Carbon圖

本文是 Uber的客戶端工程師團隊講述瞭如何開發最新版本司機端系列文章中的第五篇,該系列代號Carbon,是咱們共享出行業務的核心。包括其它功能在內,Uber 司機端使得超過 300 萬名司機能夠查看費用、里程以及收益狀況。2017 年咱們結合司機的反饋開始對司機端進行從新設計,在 2018 年 9 月份投入使用。html

用戶所使用的Apps是訪問咱們服務的主要工具。構建新的和改進的司機端須要大量的 設計工做 和許多開發工做。爲司機提供快速、無縫的切換新應用程序,須要深思熟慮的計劃;好的用戶體驗是保證司機能夠繼續使用咱們平臺的關鍵,也是維護業務完整性的關鍵。android

對於新安卓司機端中,咱們不敢冒險,擔憂會影響用戶,因此咱們採用新的方式,開發兩個二進制包。雖然不常見,可是這個可讓咱們在特定城市按照比例開放測試版本,其餘司機仍使用現存版本。git

在2016年發佈新乘客端時,咱們在安卓版本下采用的這個策略,積累了必定經驗。github

爲了司機端支持包含兩個二進制包的組件包,咱們須要改變應用類,啓動器,接收器,和服務以便它們能在獨立和組合模式下運行。咱們也須要更加邏輯代碼,在不影響司機正常接單的狀況下,能夠進行 新舊功能順暢地切換。spring

這種策略證實了它的價值,由於咱們讓新應用程序爲300多萬司機無縫使用新應用程序。緩存

兩個應用合二爲一

在一個包中開發兩套代碼的想法是不一樣需求的結果。首先,在2016年開發新的乘客端時,Uber平臺尚未過渡到一個庫,因此咱們使用兩個庫。一個用於如今的應用程序,另外一個是新的。經過從頭開始構建新程序,咱們發現若是沒有技術債務工程師能夠快遞迭代,設計解決方案,而且使用新技術Buck構建新程序。這個方法還確保了新應用程序不會泄露程序代碼,這些文件可能被一些技術愛好者和競爭對手反編譯並泄露咱們的重構計劃。安全

咱們在舊應用程序的倉庫中建立新應用程序,可是直到開發後期纔將兩個應用程序合併到一個包中。最初將兩個程序分開,發佈新應用程序測試版本的同時,也持續維護舊代碼的程序。新測試程序支持發佈給特定區域,收集有效反饋信息。隨着正式版本的發佈,一個包中包含兩套程序能夠更好的控制發佈過程。bash

其次,雖然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);
 }
 }
}

做者:猛獁象翻譯組
連接:https://juejin.im/post/5c7cea1b51882507ae09dba6
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
複製代碼

爲了達到咱們的目標,咱們有三個不一樣的構建,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
相關文章
相關標籤/搜索