ARTICLE · 1354327
卸载重装全测过,老用户一升级就崩
关键词:覆盖安装 · 本地数据迁移 · 跨版本升级 · 升级路径覆盖
版本发布前,测试把新版装了一遍:卸载旧版、安装新版、打开、登录、把主流程全跑通,全绿,发版。
上线当天,评论区开始刷屏:"更新完 App 就打不开了。"
开发一脸茫然:测试环境明明全过了。
问题多半不在新版本本身,而在于一件很多团队默认没发生的事:新安装和从旧版本升级,根本不是一回事。测试装的是一个空白的 App,老用户手机里装的是一个拖着旧数据跑的 App。
新安装测试验证的是"新版本能不能正常开始",升级测试验证的是"老用户能不能带着旧世界平稳走过来"。这两件事,缺一不可。
01 新装和升级,读的不是同一份数据
假设旧版本保存用户信息时,年龄存的是字符串:
{
"name": "张三",
"age": "25"
}
新版本调整了数据结构,要求年龄是数字:
{
"name": "张三",
"age": 25
}
新用户安装,直接以新格式创建数据,一切正常。
老用户升级,本地文件里躺着的还是字符串。如果新代码拿它当数字用,异常就在这里等着。结构变了,代码没变,变的只是数据从哪来——这正是全新安装永远测不出来的那一类问题。
02 十行代码,复现升级崩溃
假设新版本要判断用户是否成年:
def is_adult(user):
return user["age"] >= 18
new_user = {"name": "李四", "age": 25}
print(is_adult(new_user))
新数据,输出 True,正常。
换成老版本留下的数据:
old_user = {"name": "张三", "age": "25"}
print(is_adult(old_user))
直接抛错,关键信息是这两行:
TypeError: '>=' not supported between
instances of 'str' and 'int'
字符串和整数没法比大小。这段代码在全新安装的机器上永远不会跑到这个分支——因为新装机器上根本没有旧格式的数据。崩溃的不是新功能,是新功能踩到了旧世界。
03 修法:读旧数据前先过一道迁移
常规做法是在读取入口加一层迁移,把旧格式转成新格式:
def migrate_user(user):
fixed = user.copy()
age = fixed.get("age")
if isinstance(age, str):
fixed["age"] = int(age)
return fixed
old_user = {"name": "张三", "age": "25"}
fixed_user = migrate_user(old_user)
print(fixed_user)
输出:
{'name': '张三', 'age': 25}
理想情况下旧数据就此翻新。但迁移代码自己也会翻车,测试时要对着这几条验:
空字符串。旧数据里 age 是 "",int("") 直接抛错。迁移没有兜底,等于把崩溃从读数据挪到了迁移里。
非法值。"廿五"、"25岁"、带空格的 " 25",都不是干净的数字。
字段缺失。更老版本可能根本没有 age 字段,get 返回 None,isinstance 判断得接得住。
迁移跑两次。迁移中断后重跑,已经转成数字的数据再转一遍会不会出错?迁移逻辑也要考虑幂等。
迁移代码是所有旧数据的第一读者。它崩了,崩的就是每个老用户的第一次启动——这个位置的容错要求,比新功能高得多。
04 升级测试最容易漏的几条路径
很多团队测新版时直接装最新包,而真实用户的起点千差万别:
第三行值得强调:如果产品允许用户从 V1.0 直接升到 V3.0(应用商店就是这么干的),那 V1.0→V2.0、V2.0→V3.0 两段迁移会被一次性跳过还是依次执行?这取决于实现方式,而很多团队只在开发机上装过中间版本,从未验证过跳跃路径。
第六行单独说说。"升级后首次启动"是崩溃高发区:很多实现把数据迁移塞在启动流程里同步做,迁移一慢,启动就卡;迁移一抛错,App 直接起不来——用户看到的正是"打不开"。
05 V1.0 的草稿,到 V3.0 还在吗
一个特别容易被漏掉的场景。用户在 V1.0 里存了一份没提交的草稿,之后几个月没打开 App。V3.0 发布,应用商店自动帮他把 App 更新了。
如果开发只考虑了 V2.0 到 V3.0 的变化:
V2.0 → V3.0 测过
V1.0 → V3.0 没人考虑
那 V1.0 时代结构的草稿,到了 V3.0 就成了没人认识的旧数据。
最糟糕的结局不是报错,而是静默清空:解析失败,代码兜底成空对象,草稿无声无息地没了。用户几个月攒的内容,一次升级全部蒸发,而日志里可能连一行 error 都没有——因为程序认为一切正常。
这类问题,测新功能永远撞不上。它只在"带着旧数据跨版本"这一种姿势下出现。
06 不用穷举,按数据结构变化选路径
历史版本一堆,不可能每个组合都测。真正决定测什么的,是哪些版本动过本地数据结构:
V1.0:用户年龄保存为字符串
V1.5:增加草稿保存功能
V2.0:用户年龄改为数字
V3.0:调整草稿存储格式
每个动过结构的版本,都是一条升级路径的起点。据此圈出必测清单:
V1.0 → V3.0(跳过两段迁移)
V1.5 → V3.0(跳过一段迁移)
V2.0 → V3.0(相邻升级)
全新安装 V3.0(基线对照)
四条路径,比穷举所有版本组合务实得多,而且每一条都有明确的考察点。
每条路径跑完,升级前后要对比的东西就这几样——它们都是用户"以为还在"的东西:
判"还在"的标准要提前和产品对齐:是"数据还在"就算过,还是"打开就能用"才算过。草稿文件还在但解析出来是空对象,属于最阴的那种失败。
版本结构变更记录建议维护成一张常驻表格,每次发版前更新。它比任何人的记忆都可靠。
如果产品走应用商店灰度发布,灰度本身就是一次小流量的真实升级测试——但灰度里的用户结构不可控,哪条升级路径占比多少全看运气。上面这几条路径,最好在灰度开始前就全部走完。
07 卸载重装,不等于升级测试
最后是最重要的一条纪律:卸载旧版再装新版,不是升级测试。卸载会把本地数据一起带走,你测的是"空白机器上的新版本"。
真正的升级测试要保留旧数据再覆盖安装。两个平台的做法和坑各不相同:
Android:直接覆盖安装高版本 APK 即可,但有个前提经常被忽略——签名必须一致。测试机上的 debug 签名包,覆盖安装 release 签名的正式包会直接报"应用未安装"。所以签名不一致导致的升级失败,本身就是一个必测项,别让它在灰度当天才暴露。
iOS:TestFlight 构建之间的升级,和 App Store 正式版的升级路径不完全一样,验证要用贴近真实发布渠道的方式做。
另外两个容易漏的观察点:升级过程的耗时间和流量(商店页面会展示包体大小,但升级中的中断续传也要验);以及降级不存在——用户升级后就回不去了,所以迁移写坏数据没有后悔药,这也是迁移代码要格外谨慎的原因。
老版本数据躺在手机里
↓
覆盖安装新版
↓
新代码读旧数据
↓
类型对不上 → 崩溃
↓
用户看到"打不开"
这条链上,迁移层能断在第三环,容错兜底能断在第四环,签名检查能拦在第二环之前。怕的是每环都以为别人会处理。
下次测 App 新版本,留一台装着老版本、塞满旧数据的测试机。先在旧版本里攒点数据——登录、存草稿、改设置——然后直接覆盖升级,重点看首次启动。
说不定就能抓到一个全新安装永远测不出来的 Bug。
很多版本事故不是新功能写错了,而是新版本忘记了:用户手机里还保存着旧世界的数据。
互动话题:你们发 App 新版本时,会专门做覆盖安装和跨版本升级测试吗?有没有遇到过新安装一切正常、老用户升级就崩的情况?欢迎在评论区聊聊。















































































