简介
本文记录了笔者在设计控制 App (热)更新系统时的一些心得, 同时也自认为整个流程足够完善, 所以带有指南
的标识, 如果读者在阅读完后有疑问或更好的见解情在评论区留言讨论.
注1: 阅读之前读者应该了解Semver
semver.org/
注2: 出于一些现实原因, 本文版本号设计并不严格遵守语义化版本号(Semver), 读者也应根据现实原因自行设计版本号系统.
本文相关的代码: github.com/MonchiLin/A… demo App 由 React Native + Expo 驱动(这意味着如果想要运行 demo 需要有两者的一些经验), 如果读者仅想测试效果可以使用 nextjs 编写的网页版测试.
常用名词
Version: 一个可读的版本号, 由三个数字组成, 例如 1.1.0
, 1.2.9
, 78.20.0
别名: 可读版本号, VersionName
BuildNumber: 一个在每次构建时增加的版本号, 用于标识小修改更新. 别名: 构建版本号, VersionCode
hotfixes: 热更新版本号
基座版本: 与 Version
语义兼容, 因为在 App 热更新相关的逻辑中, 代码分为两个部分, 逻辑代码和原生代码, 以 React Native
为例, js 代码设计到业务逻辑是经常变动的, 但是使用的一些原生库不会经常变动, 同时原生库也不能通过热更新更新, 必须要安装 app 才能更新, 这里笔者将原生代码部分称为基座, js 代码部分称为逻辑.
阅读本文大概需要 30 分钟 :) Have a nice time.
版本号设计
版本号意味着什么? 为什么我们需要版本号?
在笔者看来在产品开发中, 版本号不仅是一种标识, 也是一种营销策略和品牌, 以 鸿蒙OS
为例, 鸿蒙OS 2
意味着一个新品牌发布, 隐含了其第一次面向消费者推送, 以及整个生态的构建等含义, 所以像 鸿蒙OS 2``鸿蒙OS 3
都是可读版本号, 也都不会轻易的改变自己的版本号的任何一位, 而是在重大变更时才会改变.
视角
结合以上观点版本号的用途最少分以下三个视角:
库开发人员
通过版本号确定功能调整, 即开发者使用版本号来记录内容修改的意图, 更改 minor
表示增加新功能, 更改patch
表示修复 bug
, 更改 major
表示发布大版本更新.
程序开发者
版本号至少有两个, <可读版本号> <构建版本号>, 以我们都爱的万物起源 鸿蒙OS
为例, 可以看到他的版本号为 3.0.0.116
, 这里 3.0.0
就是可读版本号, 116
就是构建版本号, 使用可读版本号+构建版本号
开发者可以以较低的成本更新版本号, 用户也可以通过这种简单可读性高的版本号进行反馈.
程序使用者(用户)
版本号只有一个, 即下图中的 3.0.0.116
, 用户可以针对此版本进行问题反馈, 了解功能变化.
程序开发者如何更新版本号
上文提到, 程序最少要有两个版本号 <可读版本号(Version)> <构建版本号(BuildNumber)>, 这里给不熟悉 App 开发的小伙伴解释一下, Android
和 iOS
都是在打包时也都是要提供这两个版本号的.
Android 版本号管理文档: developer.android.com/studio/publ…
iOS 版本号参考文档: help.apple.com/app-store-c…
限制
到目前为止, 已经有了很多限制:
- 可读版本号做为品牌的一部分不可以轻易修改
- 每次版本更新要可以表现出来
- 构建版本号最好可以对应到 Androd 的
VersionCode
以及 iOS 的BuildNumber
, 这样开发者就不用额外维护 Android 和 iOS 的构建版本号了.
限制1和限制2的解决办法:
通过修改构建版本号来体现版本变化
例如:
1.1.0+1 --修改BUG--> 1.1.0+2
1.2.0+0 --修改BUG--> 1.2.0+1
限制3的解决办法
例如 1.1.0+1
构建版本号为 1
这时候我们直接拿去给 iOS
用是可以的, 但是对 Android
来说却不行, 因为 Android
的构建版本号会随着 cpu 不同, 千位自动改变, 这会导致我们的 App 安装在用户手机上后构建版本号变成了 (x86)2001``(x86_64)3002
从而导致版本对比机制失效.
为了解决这个问题, 笔者在网上检索后发现了一个办法, 即给 major minor patch
递增, 使其主动变大, 这样就可以规避到 cpu 不同导致版本对比机制失效的问题. 下文会给出使用 JS 获取递增构建版本号的实现方法.
现在再来整理一下笔者的实现思路:
通过可读版本号
计算出新的构建版本号
, 这样就无需维护构建版本号.
那么新的问题就出现了, 为了给构建版本号递增, 我们还需要维护一个真正的构建版本号, 上文中我们提到的构建版本号实际上是通过可读版本号自动计算出来的, 例如可读版本号为 "1.1.0"
时构建版本号为 1010000
,可读版本号为 "2.1.0"
时构建版本号为 2010000
, 真正的热更新版本号则用hotfixes
表示, 最终
构建版本号 = 通过可读版本号计算出来的构建版本号 + 热更新版本号
JS 实现.
/**
* 在该实现中, minor/patch/hotfixes 最多 99 位, 应该足够覆盖大多数场景
*/
function getBuildNumber(version, hotfixes) {
let [majorStr, minorStr, patchStr] = version.split(".");
const major = Number(majorStr);
const minor = Number(minorStr);
const patch = Number(patchStr);
return (major * 1000000) + (minor * 10000) + (patch * 100 + hotfixes);
}
// 1.1.0 可读版本号, 0 热更新版本号
getBuildNumber("1.1.0", 0)
// 1010000
案例归纳
我的 App 无需热更新, 只判断是否有更新
api 返回
{
"最新可读版本": "2.1.0"
}
通过后台 api 获取数据, 并且与本地对比即可
我的 App 需要热更新, 在有热更新时热更新, 在有可读版本更新时更新全量包
api 返回
{
"最新可读版本": "2.1.0",
"最新热更新": "2010010"
}
通过后台 api 获取数据, 判断本地可读版本是否为最新,如果不是最新则优先更新可读版本, 如果可读版本是最新的, 则判断热更新版本是否需要更新.
我的 App 需要热更新, 在有热更新时优先热更新, 在有可读版本更新时更新全量包
api 请求
{
"客户端可读版本": "2.2.0"
}
api 返回
{
"最新可读版本": "2.1.0",
"最新热更新": "2010010"
}
如上一个情况不同的是, 这种情况会优先热更新, 没有热更新了才会判断可读版本是否有更新, 否则才会尝试更新可读版本, 为了判断是否有热更新, 客户端必须告诉服务器 客户端的可读版本
不同可读版本对应的热更新是不可以混用的.
可以看到, 一,二种情况客户端都不需要告知服务器客户端的基座版本, 这意味着后台的数据库表也可以很简单, 表以平台作为主键, 记录最新版本即可, 如下表
平台 | 最新版本 |
---|---|
Android | 1.2.0+1020000 |
iOS | 1.2.0+1020000 |
而第三种情况则较为复杂, 除去最新版本, 数据库还需要记录基座版本对应的热更新版本, 于是表就变成了, 因为我们需要基座版本
和基座热更新版本
来确认客户端是否可以热更新
平台 | 最新版本 | 基座版本 | 基座最新热更新版本 |
---|---|---|---|
Android | 2.0.0+2000000 | 1.2.0 | 1020010 |
Android | 2.0.0+2000000 | 2.0.0 | 2000000 |
iOS | 2.0.0+2000000 | 1.2.0 | 1020010 |
iOS | 2.0.0+2000000 | 2.0.0 | 2000000 |
以上面四条数据为基础, 下面列一下客户端更新情况
客户端版本 | 基座更新 | 热更新更新 | 备注 |
---|---|---|---|
1.1.0+1010000 | 是 | 否 | 更新到 2.0.0+2000000 |
1.2.0+1020000 | 否 | 是 | 先进行热更新, 更新到 1.2.0+1020010 |
1.2.0+1020010 | 是 | 否 | 更新到 2.0.0+2000000 |
2.0.0+2000000 | 否 | 否 | 为检测到更新 |