React Native無感升級在滿幫集團的實踐

xiangzhihong發表於2022-01-16

一、背景

滿幫集團移動團隊2018年初開始嘗試React Native,經過近三年的發展,目前已經承載了大部分的核心業務場景,涉及16+的業務模組、200+頁面,日均PV資料在千萬級。核心業務也使用React Native開發後,我們脫離了APP發版的限制,統一使用動態發版。相比於APP發版,動態發版頻率提高了很多,一週最低兩版,有時一週甚至會發5個版本。

2018年上線React Native時,用的是當時比較新的0.51版本。在後續的版本中,Facebook官方引入了諸多新的特性,比如Hooks、Hermes引擎等等。我們繼續使用0.51版本,這些新特性都無法使用,而且社群中很多基於更新版本ReactNative的第三方庫業務也無法使用。因此,在使用0.51版本3年之後,我們決定升級到目前較新的0.62版本。

二、0.62版本改進

在之前,我們一直使用的是0.51版本,不過,在經過近兩年的迭代後,React Native釋出了0.62版本,並且0.60以上版本相比之前的版本,效能有了大幅的提高,主要體現如下。

2.1 效能提升

相比於0.51版本,0.62最大的改進是,android上使用了Hermes作為JS執行引擎,在啟動速度、記憶體佔用、JS執行效率上都有非常大的提升。

2.2 穩定性提升

從0.51版本到0.62版本,修復了大量功能性和穩定性bug,比如Native 部分的SDK的健壯性得到了很大的加強,例如Android中 ReactHostView,在 show() 和 hide() 的安全性都進行了加強。又如ViewManager 部分, 不合法時直接進行了異常處理。

2.3 社群生態

ReactNative生態主要分為兩部分:

React 本身語言特性。

0.51 使用的是 React 16.0, 0.6x 使用的是 16.11.+, 中間新增了很多令人興奮的新特性,例如 16.2.0 的 Context 、 16.8.0 的 Hooks , 這些無疑是開發的利器!

React Native、React第三方庫

社群的第三方庫往往兩年一個週期提高 React 的依賴版本,例如比較有名到導航庫 :

React-Navigation , 並且增加很多實用的新特性, 例如 ReactNative 內部路由棧開始支援頁面間的啟用、回到後臺等特性。這在我們日常開發中非常實用。

2.4 Android端效能

0.6x開始,Android端引入了Hermes引擎,帶來了很大的效能提升。相比於JSC,Hermes最大的改進是支援直接執行JS程式碼預編譯的產物,因此冷啟動效能上有很大提升,同時記憶體佔用也有一定下降,但是包體積增大了一些。

在這裡插入圖片描述
為了瞭解清楚效能提升資料,我們在Android端對JSC和Hermes進行了效能對比測試,測試裝置:VIVO X21 RAM:6G

2.4.1 冷啟動耗時資料

在這裡插入圖片描述
從上圖可以看出,Hermes+HBC的冷啟動耗時相較於JSC+JS下降了50%以上,因此我們決定使用Hermes+HBC的方案。

2.4.2 包體大小資料

在這裡插入圖片描述
從上圖可以看出,HBC二進位制包的壓縮比明顯不如Jsbundle, 體積幾乎是後者的兩倍, 但是這一點可以通過後續的拆包, 端上轉化HBC 等手段規避。

2.4.3 程式碼指令處理速度

在這裡插入圖片描述
面對大量運算以及解析的時候, JSC 效能衰退特別嚴重,而Hermes則相對平穩, Hermes 和 JSC 耗時比基本在 1/6 左右, 出色的處理速度對幀率、 動畫流暢度都有很大的提升。

2.4.4 記憶體佔用

在這裡插入圖片描述
從上圖測算出來的資料可以得出兩條結論:

1、ReactNative 0.62 記憶體表現明顯優於 ReactNative 0.51,這得益於Hermes的載入機制,不會把整個檔案一次性load進記憶體解析。

2、ReactNative 0.62 的記憶體抖動較平緩, 這得益於 Hermes 執行的產物是二進位制, 而非JS程式碼,不需要二次轉碼。

整體操作流程,涉及 4 個 ReactInstanceManager 和 5個頁面, 節省了 56 M 記憶體空間,收益確實可觀。

2.5 iOS端效能

從0.51升級到 0.62後,iOS端的JS引擎依然只有JSC。但是在Jsbundle之外,支援了RAM格式, 採用RAM 以及 inline方案,冷啟動速度和記憶體都可以得到很大的改善。但是考慮到我們後續要做基座拆分,因此沒有使用RAM格式,iOS端依然使用JSC+Jsbundle的方案。因此,iOS端在記憶體、冷啟動、指令執行速度上並沒有太大提升。不過最近釋出的React Native 0.64 版本,官方在iOS上也開始支援 Hemers。

從效能資料上看,Android端效能有非常大的提升。升級後也能使用hooks等React的最新特性,提升開發效率,因此我們決定升級到0.62版本。

三、執行一次無感知升級

3.1 挑戰和風險

3.1.1 多部門合作協作

如前所述,ReactNative承載了滿幫大部分的核心業務場景,涉及16+業務模組,200+頁面,50+開發人員。滿幫集團的業務在高速發展期,各種業務運營活動的開展都以天為單位計時。業務多、人員多、迭代節奏快、穩定性要求高。需要統籌多個測試、開發團隊以及發版團隊等多條線的工作。

3.1.2 SDK升級與高頻率發版並行

為了滿足快節奏的業務迭代,我們每週最低釋出最低兩次動態版本(最高一週能釋出5次)。我們要求技術改造不能影響業務迭代(包括APP版本迭代和動態版本迭代),任何業務需求不能因為技術改造而延期。因此我們需要 0.51 發版工作和 0.62 升級工作同步進行, 而且要互不干擾。

3.1.3 降低升級成本

快節奏高頻率的發版下,SDK升級不能給業務需求的開發、測試帶來過多的負擔,需要把對業務開發、測試的影響儘可能的降低。本次升級作為跨越3年的大版本升級,涉及到非常多的 Release Note,我們需要盡力從底層相容這些差異性,從而儘可能得降低開發人員修改面以及降級測試人員的迴歸力度,近而降低各方面的成本。

3.1.4 保證線上穩定

滿幫集團核心的兩款APP日均UV在500萬級別,對APP體驗的要求又非常嚴苛,異常率上升萬分之一,都會導致客戶投訴率提升,穩定性保障是升級方案的重中之重。但是,無論我們方案做得多完美,誰也不能保證不會有意外狀況發生。因此我們需要在第一時間感知到線上異常,降低影響並及時修復。

本次React Native SDK升級,就像是給一輛以120碼高速行駛的重型卡車更換輪胎,稍有不慎就會翻車。

3.2 升級方案原則

3.2.1 低風險

主要主要包括兩點:

1.業務風險低:不影響業務需求的迭代。

2.穩定性風險低:不影響線上的穩定性,異常率要控制在極低的水平。

3.2.1.1 釋出方案設計

為了滿足上面兩個條件,我們決定採用分批次、灰度的方式釋出上線。

分批就是把線上使用者分成多個批次,一個批次上線完成後再進行其他批次的。滿幫有四款APP:運滿滿司機端、貨車幫司機端、運滿滿貨主端、貨車幫貨主端,我們結合業務特性分析後,採用的方案是兩個司機端第一批上,兩個貨主端第二批上。

灰度放量現在業內用的非常普遍了,這裡不再解釋含義,下文中會詳細說明灰度方案的細節。

3.2.1.2 告警和回退方案設計

真正做到低風險,我們還需要把線上問題扼殺在搖籃, 我們需要告警機制。升級前的滿幫ReactNative已經有告警機制,因此我們只需要把 0.62 拆分出一個統計維度單獨計算, 因為前期灰度的量少,如果和原先的告警機制複用就很難觸發告警條件。

遇到線上頑疾短時間內無法解決的我們還需要有降級方案,能夠短時間內把線上的 0.62 切換到 0.51, 待問題解決後再切回 0.62.。

3.2.2 低成本:

這裡的低成本是指對業務開發、測試的影響儘可能的降低。降低程式碼修改量、修改難度,從而降低開發投入的人力成本;降低影響範圍,從而縮小測試迴歸範圍、降低迴歸力度,從而節省測試投入的人力成本。

3.2.2.1 一套程式碼

為了降低風險,我們採用的是多批次灰度放量的形式釋出上線,整個上線週期會持續很長一段時間,在上線期間,各個業務模組都在不斷的迭代開發新的需求。也就是說,存量的業務程式碼和新需求的業務程式碼,都要相容兩個版本的SDK。要相容兩個版本的SDK,最簡單的方案是維護兩套程式碼,分別適配兩個版本的SDK,但是這樣需要寫兩遍程式碼,對業務開發來說是非常沉重的負擔。為了避免這種負擔,我們提出了一套程式碼適配兩個版本SDK的方案。

3.2.2.2 開發環境切換

一套程式碼適配兩個版本SDK,程式碼當然要放在一個分支上。在開發業務需求時需要分別在兩個版本SDK環境上執行程式碼,我們提供了環境切換指令碼,可以使用一行命令切換到不同的ReactNative環境上。比如:司機端線上上已經進行灰度放量了,貨主端還沒有開始放量,對於需要同時在司機&貨主兩端執行的程式碼,開發人員可以通過指令碼切換到不同的環境進行開發, 如下圖。
在這裡插入圖片描述

3.2.2.3 程式碼修改掃描

為了進一步降低開發人員的適配成本,我們開發了專門的指令碼工具,可以掃描出所有需要修改的地方,並給出具體的修改方法。

採用如上的方案,我們做到了升級風險完全可控(通過多批次灰度升級控制穩定性),又把開發人員的適配成本將到了最低(通過一套程式碼適配兩個版本的SDK和指令碼掃描修改內容)。

四、前期準備

4.1 API changes梳理

升級之前,需要先梳理兩個版本SDK之間的API差異,對0.51到0.62的所有修改有全面的認識。API change分為兩種:

  • breaking change
  • 非breaking change

我們的方法是暴力閱讀了0.51~0.62所有版本的Release Note,整理出了所有breaking changes,並給每個breaking change制定專門的適配方案。例如AsyncStorage,0.51版本的AsyncStorage用法是xxx,0.62版本的用法是yyy,所以0.51版本的程式碼和0.62版本的程式碼互相不相容。我們的適配方案是統一採用我們自己封裝的Bridge[MBBridge.app.storage]。

//npm install --save @react-native-community/asyncstorage
不建議使用
// import AsyncStorage from '@react-native-async-storage/async-storage';


// 建議修改為Bridge形式
// 根據KEY獲取VALUE
MBBridge.app.storage.getItem({ key: BootPageModalKey.KEY_IS_SHOW_BOOTPAGEMODAL }).then(res => {
  if (this.isGuidanceSwitch(res?.data?.text)) {
    retuReactNative null
  }
})


// 儲存<KEY,VALUE>
MBBridge.app.storage.setItem({ key: Constant.StorageKey.Common.RefeReactNativeame, text: commonStore.refeReactNativeame })

4.2 程式碼適配方案

公司業務迭代節奏是一週三版甚至更多, 且主要使用的是ReactNative技術棧, 因此如果在如此快節奏的開發節奏下還要同步兩套程式碼(0.51 && 0.62) , 那成本就太高了。因此我們覺得采用一套程式碼能夠同時適配 0.51 和 0.62的方案:對於所有不相容的API,封裝一層適配層,遮蔽底層差異。如下圖所示:
在這裡插入圖片描述
例如,導航庫的適配思路如下:

修改前如下

import { StackNavigator } from "native-navigation"
const RootStack = StackNavigator(...)
export default class xxxx extends Component<any, any> {
  render() {
    retuReactNative (
      <RootStack screenProps={this.props} />
    )
  }
}

修改後,ReactNative-lib-protocal 就是我們的協議層

import { createStackNavigatorCompat, createAppContainerCompat } from "@ymm/ReactNative-lib-protocal"
const RootStack = createStackNavigatorCompat(...)
export default class StickerPageRouter extends Component<any, any> {
  render() {
    const App = createAppContainerCompat(RootStack)
    retuReactNative (
      <App screenProps={this.props} />
    )
  }
}

然後,協議實現層程式碼如下。

import { NavigationActions } from 'react-navigation';
export default class StackActionsCompat {
static reset(resetAction: any){
retuReactNative NavigationActions.reset(resetAction)
}
static push(pushAction: any) {
retuReactNative NavigationActions.push(pushAction)
  }
static pop(popAction: any) {
retuReactNative NavigationActions.pop(popAction)
  }
static popToPop() {
retuReactNative NavigationActions.popToTop()
  }
}

這樣業務開發同學就可以實現一套程式碼跑在兩個React Native版本上, 節省維護兩套程式碼的成本。

4.3 指令碼工具

這裡的工具包括三個:

1、 API檢查工具(支援本地 && CI/CD);

2、程式碼工程環境切換工具;

3、執行環境檢查工具。

4.3.1 API檢查工具

API檢查工具是為了檢查那些在 0.51 環境能執行而在 0.62 上不再相容的API,為了解決這個問題, 我們針對兩個版本眾多變動點抽象出API檢測的規則。檢查工具使用 Python 指令碼編寫,開發同學既可以在本地檢查(直接執行python指令碼或者執行npm命令) 也可以在Jekins打包時啟用該檢查, 檢查效果如下:
在這裡插入圖片描述
在這裡插入圖片描述

4.3.2 環境切換工具

工程環境切換工具則是為了方便開發同學能夠方便得切換 0.51 和 0.62 的協議實現層 和 配置檔案(package.json、metro.config.js 等) ,這塊可以用 Shell 或者 Python 實現。

這個工具保證了業務開發同學能夠在一個分支上進行開發,而不用把關注點放在 0.51 和 0.62 的API差異和配置差異上。

4.3.3 環境檢查工具

執行環境檢查工具則是為在測試環境檢查 ReactNative SDK 環境和 Bundle 產物不匹配的情況,例如 0.51 原生SDK載入了 0.62 的 Bundle/HBC, 或者 0.62 原生SDK載入了 0.51 的Bundle 包,從而避免不必要的麻煩以及溝通成本:

在這裡插入圖片描述

五、落地方案

下圖是我們升級計劃的一個簡圖, 整個流程以角色為緯度,劃分為四條主線: 開發人員、測試人員、APP版本、 動態版本。每一條主線對應的時間軸在關鍵時間點都有詳細的Action。

例如:對於業務開發人員(第一條線)來說, 需要在 2020-12-18 日把適配的業務程式碼合入 dynamic-1231 主釋出分支,隨後 0.51、0.62 公用一套程式碼直至整個升級流程結束。

在這裡插入圖片描述

5.1、分批升級

如前所述,我們採用的是分批升級的方案,司機端APP第一批上線,貨主端APP第二批上線。

Android端為了這次升級,對 React Native 的環境進行了外掛化。為了儘可能的控制風險,第一批次Android司機端上線是通過外掛動態釋出的形式進行的:0.62版本的SDK和HBC的產物同時通過動態升級的方式下發到端上。動態釋出的方式可以非常靈活的控制灰度節奏:為了確保穩定性,我們可以把灰度時間拉的足夠長。而且,我們的動態升級平臺支援線上實時回滾。

我們基於穩定性的角度出發,決定Android端通過動態升級的方式上線。但是保證穩定性的同時,也不能影響業務需求上線,0.62版本的SDK和HBC產物線上上灰度釋出的同時,也會基於0.51版本的SDK和jsbundle產物同步做業務需求的釋出。也就是說:0.51 和 0.62 環境需要在較長時間的線上上並行存在。

在這裡插入圖片描述
整個灰度過程中比較重要的一點是線上環境的功能同步問題:0.51 和 0.62 的產物會分別以各自的節奏釋出到線上,不能相互干擾,但是同時又都必須包含所有的業務需求。

例如:0.51 每 2 天一個版本, 而 0.62 灰度週期是 10 天, 因此需要保證使用者無論使用的是 0.62 還是 0.51 都需要包含最新的功能, 我們的策略如下:

在這裡插入圖片描述
如上圖所示,0.51 和 0.62 的釋出是兩條平行的線, 0.62 的版本號在設計上要大於 0.51的版本號(保證了0.62 的產物永遠不會被 0.51 的產物覆蓋),每一次 0.51 業務包的釋出都會同步釋出一個 0.62 的業務包,因此可以保證以下兩點:

1、線上使用者使用的功能始終是最新的;

2、0.62產物始終按照自己的節奏灰度, 不會被0.51的產物覆蓋。

待0.62灰度全量之後, 線上不會再發布 0.51 的業務包,線上升級切換完成即可。

5.2、CI/CD

在這裡插入圖片描述
由於0.51 和 0.62 的業務 Bundle 需要較長時間的線上上並行存在, 以及兩個版本的環境、產物也會存在不相容的情況。因此除了有測試階段的環境檢測手段之外, 還需要在CI/CD階段插入我們的一系列校驗流程:

1.環境切換;

2.把檢查相容API的python指令碼整合到構建過程中;

3.根據產物型別來生成版本號規則:

  • 0.51版本號規則 :5.91.xxx.yy
  • 0.62版本號規則:5.91.1xxx.yyyy

4.針對Android的hbc產物生成額外的map並上傳ftp。

5.3、資料準備

這裡主要是埋點,用來和 0.51 版本的資料區分開,我們期望線上 0.62 版本 產生的資料能準確得反應升級的真實情況(訪問佔比、穩定性), 同時我們也為 0.62 的資料單獨配置了告警策略。

在這裡插入圖片描述

六、線上驗證

專案上線以後,我們所要做的就是及時跟進線上資料, 驗證前期實驗室資料, 以及關注監控資料, 及時調整方案。

6.1 每日報表輸出

在62升級包灰度期間,每日會有報表輸出, 包括每個模組的DAU、PV、JS異常使用者數、JS異常率、SDK異常使用者數以及SDK異常率等, 開發和測試同學可以從整體瞭解線上執行的狀況。我們也提前制定了預案,異常率達到一定閥值時就停止灰度。
在這裡插入圖片描述

6.2 效能資料輸出

以Android端的效能資料為例,最終我們從線上採集到的效能資料如下,和我們線下測量的資料基本吻合:

在這裡插入圖片描述

1、包大小

Android端採用的是Hermes+HBC的方案,打包輸出的產物從字串格式的.jsbundle變為二進位制包 .hbc, 包體積因此增長了 45% 以上。這是以空間換時間的一個優化(JIT 變為 AOT)。

2、冷啟動

採用Hermes+HBC的方案後,指令執行速度大大提升,冷啟動時長時減少了約64% ,啟動速度提升了近三倍,基本符合我們之前的測試和預期。

3、熱啟動

我們做了引擎複用機制,引擎建立一次後,會駐留記憶體,因此第二次啟動就是熱啟動了。熱啟動的過程相較於冷啟動,沒有JS程式碼載入、初始化執行等耗時操作。因此、熱啟動時長几乎沒有改善,這也基本符合我們之前的測試和預期。

4、記憶體佔用

Hermes引擎執行HBC,省掉了JS程式碼解釋的過程,因此冷啟動單頁面執行時記憶體減少了 30%以上。

6.3 後續批次

第一批次的升級工作基本告一段落, 在這中間會沉澱不少最佳實踐:上線計劃、容錯方案、測試計劃、效能分析 等,第二批次貨主端的升級工作僅需要在這基礎上做微調整, 上線風險和整體計劃會流暢很多。

司機端第一批次對穩定性做了基本的驗證,我們可以確定風險整體可控。因此,貨主端第二批次上線時,直接採用了跟隨APP發版的方式上線。

iOS端沒有外掛化機制,因此兩個批次都是採用跟隨APP發版的方式上線,使用AppStore預設的7天灰度。

相關文章