先看结论与判断条件
- 升级测试的起点是仍在用户设备上的真实旧版本与状态,不是任意历史 APK;每条路径要绑定旧包和加固候选的 SHA-256。
- 平台更新资格至少涉及 applicationId、可接受的 versionCode 和兼容签名关系,安装成功只证明平台门槛通过,不证明业务迁移正确。
- 上传密钥、应用签名密钥、证书和 Play App Signing 责任必须区分,本地重签名候选不能冒充商店最终签名对象。
- 升级前要构造版本化数据契约,覆盖数据库、偏好、文件、账号、密钥引用、队列和功能开关,升级后验证语义而非只比文件数量。
- 覆盖安装会进入新的进程与初始化路径,应检查组件、Provider、Native 库、后台任务、异常恢复和首次启动,而不是沿用旧进程观察。
- 云真机矩阵应从真实支持范围和版本分布选择;单一设备、单一旧版本或安装器返回成功都不能代表升级兼容完成。
升级回归必须从真实旧状态出发,不能用全新安装替代
未加固旧版本升级到加固版本的风险不只在安装阶段。旧版本已经留下数据库、偏好、文件、账号令牌、Keystore 引用、任务记录和功能状态;加固候选还可能改变类加载、启动组件、Native 载荷和异常路径。卸载后重装会清除关键历史状态,最容易漏掉的迁移问题反而全部消失。
每个升级场景应由 oldCandidate、newCandidate、stateFixture 和 expectedContract 四个对象组成。oldCandidate 必须是实际发布过或仍受支持的版本,newCandidate 是准备交付的精确加固文件,stateFixture 描述升级前业务状态,expectedContract 规定升级后哪些数据保留、转换、失效或要求重新登录。
测试顺序是安装旧包、运行到指定状态、停止进程、就地覆盖新包、启动新进程并验证契约。过程中不得清数据,也不能用调试菜单跳过迁移。若必须调整测试环境,应在场景定义中提前声明并让基线与候选一致,不能遇到失败才临时清理。
| 对象 | 内容 | 身份字段 | 失败条件 |
|---|---|---|---|
| 旧候选 | 真实未加固版本 | APK 摘要、versionCode、证书 | 来源不明或已重签 |
| 新候选 | 待验收加固版本 | APK 摘要、配置、签名阶段 | 与交付文件不同 |
| 状态夹具 | 账号、数据与业务进度 | fixtureId 与 schema | 人工状态不可复现 |
| 结果契约 | 保留、迁移、失效规则 | contractVersion | 只检查能否启动 |
| 设备环境 | 系统、ABI、locale 与安装器 | deviceIdHash 与构建 | 环境中途变化 |
先验证 applicationId、versionCode 与更新身份
How Android app updates work 说明,应用更新需要保持 applicationId,使用可接受的 versionCode,并满足兼容签名或轮换证明等条件。这些是平台识别同一应用并允许覆盖更新的基础。若包名变化,系统会把它视为另一应用;若版本关系不被接受,安装器会拒绝正常升级。
安装资格门禁应在业务测试前执行。记录旧包和新包的 applicationId、versionCode、证书信息与文件摘要,再执行覆盖安装并保存安装器原始结果。不要根据文件名、市场版本名或构建目录推断身份。相同 versionName 也可能对应不同 versionCode 与不同二进制。
平台允许安装不等于业务防重放、服务端版本策略或数据迁移已经正确。本文只处理旧版到新版的前向升级,不把降级重放与证书轮换策略混入同一结论。需要这些能力时应建立独立威胁模型、授权规则和验收路径。
| 字段 | 要求 | 证据 | 不能推出 |
|---|---|---|---|
| applicationId | 前后属于同一应用身份 | 产物解析与安装结果 | 业务账号连续 |
| versionCode | 满足目标渠道更新规则 | 前后产物元数据 | 迁移脚本正确 |
| 签名关系 | 满足平台兼容要求 | 证书与轮换材料 | 密钥保管安全 |
| APK 摘要 | 锁定精确文件 | SHA-256 回执 | 该文件已发布 |
| 安装器结果 | 覆盖会话成功或明确失败 | 原始状态和消息 | 核心路径通过 |
签名连续性要区分上传密钥与最终应用签名
Sign your Android app 区分应用签名密钥、上传密钥、证书和 Play App Signing 的责任。开发团队上传的 AAB 可以由上传密钥签名,而用户设备收到的 APK 由应用签名密钥签名。因此,本地拿上传密钥生成的 APK 不能代表商店最终更新对象。
升级矩阵要标注 signingStage。若验证本地直接分发 APK,应使用对应渠道允许的签名关系;若验证 Play 路径,应从受控测试轨道获得最终派生 APK 或真实安装回执。证书摘要只是身份事实的一部分,不能证明私钥保管、服务端发布授权或加固效果。
遇到签名不兼容时,不能通过卸载旧包后安装新包来宣称问题解决,因为这改变了用户数据和应用身份连续性。正确结果是阻断该升级候选,核对发布链、历史证书和平台支持的签名关系。任何涉及密钥的材料都应保留在受控系统,公开脚本只记录证书摘要。
| 阶段 | 对象 | 可用证据 | 误用 |
|---|---|---|---|
| 本地调试 | 调试 APK | 受控诊断记录 | 冒充商业升级 |
| 直接分发 | 渠道签名 APK | 证书和安装回执 | 套用 Play 结论 |
| Play 上传 | 上传签名 AAB | 上传文件和轨道记录 | 当作设备最终证书 |
| Play 分发 | 应用签名 APK | 真实安装或派生回执 | 用本地重签包替代 |
| 证书摘要 | 公开身份指纹 | 前后候选记录 | 暴露私钥或证明保护强度 |
Split 与 PackageInstaller 会话也要保持候选一致
Android PackageInstaller 文档说明 split 安装中的 base、packageName、versionCode 和签名证书需要保持一致。AAB 分发场景下,设备可能收到 base 与多个配置或功能 split。只抽查 base APK,无法证明完整安装会话中所有 split 都属于同一版本和签名集合。
升级证据应记录设备实际安装的包集合、ABI、语言、密度、动态模块状态和安装来源。若旧版本曾下载按需模块,新版本升级后要验证模块状态与业务数据是否合理延续。平台 API 约束可以拒绝明显不一致的会话,却不能证明 Play 服务器所有设备组合都已被团队抽样。
工程判断是为代表设备保存 oldInstalledSetDigest 与 newInstalledSetDigest,并把每条升级用例绑定 device spec。通用 APK、本地 `.apks` 和线上派生 APK 不能混为同一对象。若 split 集变化,测试要解释这是版本设计、设备选择还是构建异常。
| 字段 | 作用 | 异常信号 | 处理 |
|---|---|---|---|
| base identity | 定义包和版本基础 | base 与 split 不同版本 | 拒绝会话 |
| split names | 标识配置与功能模块 | 缺失已用模块 | 验证交付状态 |
| ABI 与配置 | 匹配设备选择 | 安装错误架构 | 核对 device spec |
| 签名证书 | 维持会话身份一致 | split 证书不一致 | 阻断候选 |
| installed set digest | 锁定设备实际集合 | 回执无法对应 | 重新采集 |
数据迁移要验证业务语义,不只确认文件还在
升级前数据应按业务域构造,而不是随手操作几步。至少考虑数据库 schema 与关键记录、SharedPreferences、内部文件、账号与会话、Keystore alias 引用、待处理任务、功能开关和下载资源。每项都要定义 preserve、migrate、invalidate 或 regenerate,避免测试人员凭肉眼判断。
文件存在不代表语义正确。数据库可能完成 schema 升级却丢失约束,偏好键可能保留但类型改变,密文可能存在却因别名、认证条件或序列化变化无法读取。验证应通过公开业务接口或受控测试接口读取规范化结果,不直接把内部字节相等当作成功。
迁移还要覆盖中断和幂等。若应用在迁移中被终止,再次启动不能重复扣减、重复提交或进入半完成状态。工程判断是保存 migrationState、fromSchema、toSchema、completedSteps 与 recoverableError,并测试首次升级、迁移中断后恢复和升级完成后的再次启动。
| 数据域 | 预期动作 | 验证方法 | 失败示例 |
|---|---|---|---|
| 数据库 | 迁移 schema 与记录 | 业务查询和约束 | 表存在但字段语义错误 |
| 偏好与配置 | 保留或转换键值 | 类型化读取 | 旧类型解析失败 |
| 账号与会话 | 按策略延续或失效 | 鉴权状态与刷新路径 | 静默退出或错误复用 |
| 密钥引用 | 保持别名或明确重建 | 受控加解密契约 | 密文在但无法读取 |
| 任务与文件 | 恢复、重排或清理 | 幂等和状态机 | 重复执行或永久卡住 |
覆盖安装后必须从新进程验证初始化和核心路径
覆盖更新会使后续运行进入新候选代码和新的进程初始化。测试应在升级前停止旧进程,完成安装后重新启动,确认 Application、ContentProvider、组件、Native 库和保护载荷按新版本建立。不能让旧进程或测试框架缓存掩盖类加载、符号或初始化问题。
Android instrumented tests 在真实 Android 环境执行,可访问组件和系统 API,适合验证迁移后的数据库、权限、文件、Keystore、Service、Provider 和核心业务路径。测试应区分安装前状态准备、升级后首次启动、核心读写、后台恢复和再次启动,每一阶段都绑定同一新候选。
核心路径要按风险选择,包括登录续期、离线数据读取、授权判断、关键交易状态、推送或深链入口以及后台任务。本文不声称这些路径已由任何产品自动覆盖。具体集合取决于应用资产和失败成本,未提供项目证据时只能列为待验证。
| 阶段 | 主要检查 | 证据 | 阻断条件 |
|---|---|---|---|
| 安装完成 | 包、版本、签名与 split | 安装器和包信息 | 身份不连续 |
| 首次启动 | 迁移、组件和 Native 加载 | 事件、日志与断言 | 退出或半迁移 |
| 核心读写 | 业务数据语义 | instrumented test | 状态丢失或误用 |
| 后台恢复 | 任务、Service 与进程重建 | 设备运行回执 | 重复或永久暂停 |
| 再次启动 | 幂等与稳定状态 | 第二次契约结果 | 迁移重复执行 |
退出证据和设备矩阵决定结论能覆盖多远
Android ApplicationExitInfo 可提供历史进程退出原因、ANR trace,并在适用新版本提供 Native tombstone。升级首次启动若发生闪退、ANR 或系统终止,应将记录与旧候选、新候选、安装完成时间、caseId 和用户路径关联。退出原因不能脱离 trace、堆栈和迁移状态直接当作根因。
Firebase Test Lab Android matrices 由设备型号、OS、方向和 locale 等维度组成,任一执行失败都会影响矩阵判断。升级矩阵还要增加旧版本和状态夹具维度。设备选择应来自最低支持范围、真实版本分布和高风险厂商,而不是只挑当前可用的最新设备。
云真机结果仍有边界。它不能自动复现全部安装来源、厂商账号、推送环境或真实用户历史数据。工程判断是先在可控矩阵跑确定状态,再对关键渠道执行小范围真实升级验证。每个失败都要保留安装、启动、退出和数据契约证据,不能用其他设备通过抵消。
- 退出记录绑定升级时间窗和新候选摘要
- ANR trace、Java 堆栈与 Native tombstone 分层解释
- 设备矩阵包含真实支持 OS 与 ABI
- 旧版本选择来自实际分布和支持策略
- locale、方向和设备状态明确
- 任一执行失败单独处置,不被平均掩盖
- 云矩阵结论不外推所有安装渠道
用模拟器脚本执行不清数据的就地升级骨架
下面的 Bash 示例只允许在 Android 模拟器运行。它接收旧 APK、新 APK、包名、启动组件、instrumentation runner 和空证据目录,记录两份文件摘要,安装旧包并执行 before 契约,再使用 `adb install -r` 就地升级,最后执行 after 契约和包信息采集。
脚本不卸载应用、不清数据、不读取密钥,也不在真实设备运行。任何安装、启动、instrumentation 或包身份检查失败都会返回非零状态。测试 APK 与状态夹具需由项目另行准备;脚本只是公开安全的执行骨架,不能替代签名解析、商店轨道回执和业务断言设计。
准备未加固到加固版本的升级评估时,可整理真实旧版本、加固候选、签名与 split 身份、状态夹具、迁移契约、首次启动、核心路径、退出记录和设备矩阵,再通过御盾中央平台提交申请。脚本通过不等于项目升级兼容已经完成。
- 脚本仅在单一 Android 模拟器运行
- 旧包与新包摘要先于安装保存
- 覆盖升级使用 -r 且不卸载清数据
- 升级前后均执行版本化数据契约
- 包身份、启动和 instrumentation 失败即阻断
- 证据目录必须是新的空目标
- 模拟器结果不冒充商店或真实设备发布回执
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 6 ]; then
exit 2
fi
baseline_apk=$1
hardened_apk=$2
package_name=$3
start_component=$4
test_runner=$5
evidence_dir=$6
if [ ! -f "$baseline_apk" ] || [ ! -f "$hardened_apk" ] || [ -e "$evidence_dir" ]; then
exit 2
fi
device_count=$(adb devices | awk 'NR > 1 && $2 == "device" {count += 1} END {print count + 0}')
if [ "$device_count" -ne 1 ]; then
exit 2
fi
is_emulator=$(adb shell getprop ro.boot.qemu | tr -d '\r')
if [ "$is_emulator" != "1" ]; then
exit 2
fi
mkdir -p "$evidence_dir"
shasum -a 256 "$baseline_apk" > "$evidence_dir/baseline.sha256"
shasum -a 256 "$hardened_apk" > "$evidence_dir/hardened.sha256"
adb install "$baseline_apk" | tee "$evidence_dir/install-baseline.txt"
if ! adb shell cmd package list packages | grep -Fx "package:$package_name" > "$evidence_dir/package-before.txt"; then
exit 3
fi
adb shell am force-stop "$package_name"
adb shell am start -W -n "$start_component" > "$evidence_dir/start-before.txt"
adb shell am instrument -w -e upgrade_phase before "$test_runner" > "$evidence_dir/contract-before.txt"
adb shell dumpsys package "$package_name" > "$evidence_dir/dumpsys-before.txt"
adb shell am force-stop "$package_name"
adb install -r "$hardened_apk" | tee "$evidence_dir/install-upgrade.txt"
if ! adb shell cmd package list packages | grep -Fx "package:$package_name" > "$evidence_dir/package-after.txt"; then
exit 3
fi
adb shell am start -W -n "$start_component" > "$evidence_dir/start-after.txt"
adb shell am instrument -w -e upgrade_phase after "$test_runner" > "$evidence_dir/contract-after.txt"
adb shell dumpsys package "$package_name" > "$evidence_dir/dumpsys-after.txt"
printf '%s\n' "upgrade-evidence-complete"事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 更新需要保持 applicationId,使用可接受的 versionCode,并满足兼容签名或轮换证明条件。 | How Android app updates work 描述应用更新身份与版本条件。 | 平台更新条件不证明业务数据迁移、服务端防重放或核心路径正确。 |
| 发布流程需要区分应用签名密钥、上传密钥、证书和 Play App Signing 责任。 | Sign your Android app 描述 Android 应用签名与 Play App Signing 流程。 | 文档不能确认某个实际候选使用正确生产证书,也不能证明密钥保管安全。 |
| Split 安装要求 base、packageName、versionCode 和签名证书保持一致。 | Android PackageInstaller 描述安装会话和 split APK 的一致性约束。 | API 约束不能证明 Play 服务器生成的全部设备 split 组合已被项目抽样。 |
| 依赖真实 Android 运行时、组件和系统 API 的升级后语义可以通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在真实 Android 环境执行。 | 单一设备通过不能代表完整 API、ABI、系统版本和厂商矩阵。 |
| 历史进程退出信息可以提供退出原因、ANR trace,并在适用版本提供 Native tombstone。 | Android ApplicationExitInfo 描述进程退出记录和诊断材料。 | 退出记录仍需关联同一候选、升级时间窗和用户路径,不能单独确定根因。 |
| 云真机矩阵由设备、OS、方向和 locale 等维度组成,执行失败会影响矩阵判断。 | Firebase Test Lab Android matrices 描述 Android 测试矩阵的配置与结果。 | 云设备仍需按真实用户和最低支持范围选择,不能覆盖全部渠道与历史状态。 |
| 未加固旧版本到加固版本必须执行不清数据的就地覆盖升级。 | 工程判断:卸载重装会消除数据库、偏好、账号、密钥引用和任务等关键历史状态。 | 就地升级只是必要测试路径,仍需签名、迁移、启动、业务和设备证据。 |
| 升级契约应验证数据语义、迁移幂等和中断恢复,而不只检查文件存在。 | 工程判断:保留字节可能因 schema、类型、密钥引用或状态机变化而无法继续使用。 | 具体数据域和恢复策略取决于项目设计,未有项目证据时不能声称迁移通过。 |
工程常见问题
卸载旧版再安装加固版能算升级测试吗?
不能。卸载会清除关键历史状态,必须从真实旧包准备数据后执行不清数据的覆盖安装,才能验证迁移与身份连续性。
新包能覆盖安装成功是否代表升级兼容?
不代表。安装成功只说明平台身份和版本门槛通过,还要验证数据迁移、新进程初始化、核心业务、异常恢复和设备矩阵。
使用上传密钥签名的本地 APK 能代表 Play 更新吗?
不能。上传密钥与应用签名密钥责任不同,Play 分发路径应使用测试轨道或真实派生安装回执验证最终签名对象。
升级后数据库文件存在是否说明迁移成功?
不能。应通过业务契约检查 schema、关键记录、约束和幂等状态,文件存在但字段语义错误仍属于迁移失败。
需要测试所有历史版本升级吗?
应根据仍受支持版本、真实用户分布、schema 跃迁和高风险变化选择路径,不能只测最近版本,也不必无依据穷举不可达版本。
申请加固升级路径评估前应准备哪些材料?
准备真实旧 APK、加固候选、签名与 split 身份、状态夹具、迁移契约、首次启动、核心路径、退出记录和设备矩阵,再从御盾中央平台提交申请。