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

猛獁象翻譯組發表於2019-03-04

如何在不影響整個業務情況下重構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);
 }
 }
}

作者:猛獁象翻譯組
連結: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

相關文章