共计 8214 个字符,预计需要花费 21 分钟才能阅读完成。
一、背景
满帮团体挪动团队 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 天灰度。