先看结论与判断条件
- 首次安装、卸载和卸载后重装具有不同前置状态、动作与数据预期,应分别建用例,不能串成一条脚本后只看最终页面。
- 每条结果都要绑定 applicationId、versionCode、签名证书摘要、上传候选摘要以及设备实际获得的 APK set,文件名不是候选身份。
- 首次安装重点验证合法安装会话、最终包身份、初始配置生成、首次启动和核心功能入口,而不是只看桌面图标出现。
- 卸载重点验证目标包确实移除、进程与入口不再可达,并按产品声明检查本地、外部和服务端状态,不把未声明清理范围当缺陷。
- 重装不是覆盖升级;它必须从已验证的卸载后状态出发,重新安装同一测试候选,并验证数据恢复、重新认证与核心路径重入符合明确契约。
- AAB 需要本地 bundletool 派生和受控测试轨道两类证据,调试签名生成的 APK set 只能用于结构检查,不能冒充生产重装候选。
把三条路径建成三个可验证的状态转换
很多回归脚本把安装、启动、卸载、再安装连续执行,最后只判断首页是否打开。这种做法丢失了最关键的中间状态:首次安装前是否真的没有旧包,卸载后目标包是否确实消失,重装前是否存在允许恢复的数据。正确矩阵应为每条路径定义 precondition、action、expected package state、expected data state 和 failure receipt,任何前置条件不成立都不能继续。
首次安装的起点是目标 applicationId 未安装,并且测试数据处于声明的 fresh 状态;卸载的起点是当前候选已安装、可启动且关键本地状态已记录;重装的起点是卸载动作已经获得回执,包查询确认目标版本不存在,保留状态与清除状态符合产品契约。三条路径可以使用同一设备,但每次都要从可复现快照恢复,不能沿用上一轮偶然残留。
矩阵还要把安装生命周期与覆盖更新分开。覆盖更新保留已安装包身份并替换到更高版本,关注签名与版本连续;卸载后重装先移除包,再创建新的安装会话,关注退出后的数据边界和重新初始化。本文只处理首次安装、卸载与重装,不用旧版本到新版本的升级链替代这三个问题。
| 路径 | 前置包状态 | 主要动作 | 必须验证的结果 |
|---|---|---|---|
| 首次安装 | 目标 applicationId 不存在 | 安装当前候选并首次启动 | 包身份、初始状态、权限入口和核心路径 |
| 卸载 | 当前候选已安装且基线可用 | 请求卸载并等待最终回执 | 包移除、入口退出、进程停止与数据契约 |
| 卸载后重装 | 卸载成功且后置状态已核对 | 重新安装指定候选并启动 | 安装身份、恢复或重建状态、重新认证和核心路径 |
| 覆盖更新 | 旧版本仍安装 | 使用兼容签名和新 versionCode 更新 | 属于另一条升级连续性测试,不替代重装 |
| 清数据后启动 | 包仍安装但应用数据被重置 | 不经过卸载直接启动 | 只验证数据初始化,不证明卸载行为 |
| 换设备安装 | 设备与数据来源发生变化 | 在另一设备执行安装 | 用于矩阵覆盖,不是同设备重装对照 |
候选、签名和派生文件必须全程同链绑定
三条路径的第一项证据不是截图,而是候选身份。记录 applicationId、versionCode、构建变体、签名证书摘要、上传 AAB 或 APK 的 SHA-256、构建编号与渠道。首次安装、卸载前基线和重装使用哪个候选都要明确。若重装时临时换了重新签名的包,即使页面相同,也已不是同一条生命周期链。
Android 通过应用签名建立安装与更新身份,签名方案覆盖的文件区域和适用平台版本存在差异。签名验证可以证明包内容完整且由对应签名者签发,但不能证明业务逻辑安全,也不能证明加固后生命周期路径正确。门禁应把签名结果当必要身份条件,并继续验证安装会话、数据状态和设备行为。
AAB 场景还要绑定设备实际安装的 base 与 split。PackageInstaller 要求同一 split 安装中的 base、packageName、versionCode 和签名证书一致;bundletool 可以按设备规格生成 APK set。只保存 AAB 文件名无法解释某台设备的安装结果,至少还要保存设备规格、派生参数、APK set 摘要、文件清单和实际安装回执。
- 候选以 SHA-256、applicationId、versionCode 和签名摘要标识
- 首次安装、卸载基线和重装分别登记实际候选
- 手工重签、重新构建或重新派生会创建新测试对象
- AAB 场景保存设备规格、base 与 split 文件清单
- split 的包名、版本和签名身份保持一致
- 签名通过不会被写成业务兼容或安全通过
首次安装要验证初始状态如何建立
首次安装前先确认目标 applicationId 未安装,包查询、桌面入口和目标进程均不应显示旧候选。还要定义 fresh data 的含义:是否允许系统备份恢复、外部文件复用、服务端账号识别或设备级标识参与。不同产品的答案不同,测试不能擅自假设卸载会清除所有状态;应把允许保留和必须重建的状态写进用例。
安装阶段保存 PackageInstaller session 创建、文件写入、commit 和最终状态回执。split 安装必须把合法 base 与全部目标 split 放进同一会话,并核对实际安装后的 applicationId、versionCode 和签名证书。桌面出现图标只说明入口可见,不代表会话对象、动态特性、Native 库或资源已经正确到达目标设备。
首次启动验证应用是否能创建初始配置、数据库或缓存,是否进入预期的登录或引导状态,权限请求是否来自正确业务动作,以及核心路径能否完成最小闭环。测试应保存阶段码和失败位置,而非只写“白屏”或“闪退”。没有真实项目回执时,不能声称当前加固候选已经通过初始化、权限或数据创建。
| 阶段 | 输入或前置 | 断言 | 失败回执 |
|---|---|---|---|
| 设备准备 | 目标包不存在、数据策略已声明 | 前置状态与 fresh 定义一致 | 包查询、数据快照、设备标识 |
| 安装会话 | 当前候选与完整派生集 | commit 得到最终成功状态 | session ID、状态码、消息和时间 |
| 身份核对 | 已安装包 | 包名、versionCode、签名符合候选 | 安装后包信息与摘要映射 |
| 首次启动 | 未初始化应用状态 | 进程和首屏进入预期阶段 | 阶段码、退出和可见错误 |
| 初始数据 | 声明的恢复与重建策略 | 关键状态按契约创建或恢复 | 数据版本、来源与校验结果 |
| 核心路径 | 完成必要认证与权限 | 最小业务闭环可重复执行 | 步骤、断言和失败位置 |
卸载要验证退出边界,而不是只看图标消失
卸载用例必须从一个已知可用的安装状态开始。先记录当前 applicationId、versionCode、签名、进程、关键本地状态和外部依赖,再发起卸载并等待最终结果。若卸载前候选本身无法启动,后续即使包被移除,也不能证明完整生命周期可用;该结果只覆盖卸载机制,不覆盖业务退出和重装前提。
卸载完成后,应再次查询目标包、进程与入口,确认旧版本不再可启动,并按产品声明检查数据边界。应用私有数据、用户选择保留的文件、共享目录、系统备份、账号服务端状态和推送注册可能采用不同策略。工程判断应把这些对象逐项列出 expected_state,不能用“全部清空”或“全部保留”这样未经产品确认的笼统断言。
卸载还要处理异步退出。后台任务、通知入口、快捷方式、外部文件和服务端令牌可能在包移除后留下可观察状态。测试应验证这些入口不会把用户带入不存在的组件,并记录需要服务端注销或客户端过期的项目。平台卸载成功不等于所有业务状态自动清理,服务端策略也不能由 PackageInstaller 回执证明。
- 卸载前候选身份、启动基线和关键状态已记录
- 卸载动作取得最终回执而非只发出请求
- 包查询、入口和目标进程确认旧候选不可达
- 本地、外部、备份和服务端状态分别声明预期
- 后台入口、通知与快捷方式不会进入不存在的组件
- 未声明清理范围不会被擅自写成加固缺陷
重装从卸载后状态开始,不等同覆盖更新
重装的第一条回执是卸载后状态,而不是新的安装成功截图。用例必须引用前一条卸载记录,确认目标包不存在,并保存允许保留的数据快照。若直接在旧包上安装相同或更高 versionCode,那是覆盖安装;它没有经历包移除,也无法验证重新初始化、恢复来源、服务器会话和外部文件冲突。
重装可分为同候选重装和新候选重装,但两者用途不同。同候选重装适合验证卸载与重新进入的确定性;新候选重装则同时改变生命周期状态和候选内容,出现差异时难以归因。发布回归应优先用同一最终候选完成闭环,再另建明确的版本迁移用例,不能把两种结果合并为一个“重装通过”。
安装完成后,要根据数据契约判断进入初始状态、恢复状态还是需要重新认证。若保留数据存在,验证其版本、所有者和完整性;若状态应重建,验证旧记录不会被误用;若服务端仍识别账号,验证本地授权能否按当前策略重新建立。核心业务必须重新进入并完成闭环,单纯显示登录页不能证明重装恢复正确。
| 重装条件 | 期望数据状态 | 必须验证 | 错误替代 |
|---|---|---|---|
| 同候选、无允许保留状态 | 重新建立初始状态 | 安装身份、初始化与核心路径 | 把清数据后启动当重装 |
| 同候选、有声明保留状态 | 按契约恢复指定对象 | 来源、版本、完整性和权限 | 假设所有旧数据都应恢复 |
| 服务端账号仍存在 | 本地会话按策略重新建立 | 认证挑战、令牌更新与注销状态 | 只看服务端账号可查询 |
| 外部文件仍存在 | 仅消费允许版本与所有者的文件 | 格式、归属、失败回退 | 看到文件就直接复用 |
| 新候选卸载后安装 | 同时包含候选变化 | 单独记录版本与内容差异 | 与同候选重装合并结论 |
| 旧包未卸载直接安装 | 覆盖更新语义 | 签名与 versionCode 连续 | 标记为卸载后重装 |
AAB 派生、签名和渠道交付分别取证
Build and test Android App Bundles 提供本地 bundletool 和测试轨道两类验证路径。本地可以针对设备规格生成、检查并部署 APK set,适合确认 base 与 split 组合;测试轨道用于观察应用商店处理后的实际交付。两类证据都应绑定最终上传 AAB,但本地成功不能替代线上交付,线上某台设备成功也不能证明所有派生配置都已抽样。
bundletool 生成 APK set 时要显式记录签名参数。若未提供发布签名,生成物可能使用调试签名,它可以帮助检查资源和 split 结构,却不能用于验证生产重装身份。重装测试尤其依赖签名连续:测试包、卸载前基线和渠道候选如果签名来源不同,服务端、备份或外部状态观察可能被不同应用身份干扰。
渠道矩阵应写清 direct APK、企业 split 或商店 AAB,不要把结果横向复制。对每个渠道至少抽查首次安装、卸载和同候选重装三条路径,并保存设备实际获得的文件集合。渠道之间若使用不同签名托管、版本编号或动态特性交付策略,应分别建立候选身份,不以同一营销版本名称视为完全相同。
- 本地 APK set 与测试轨道结果分别保存
- 每份 APK set 绑定 AAB 摘要、设备规格和 bundletool 版本
- 发布签名参数明确,调试签名不参与生产结论
- base、split、packageName、versionCode 和签名一致
- 直接分发、企业 split 与商店 AAB 分渠道判定
- 动态特性和设备条件未覆盖部分进入发布边界
用只读脚本校验安装生命周期矩阵
下面的 Python 脚本读取脱敏测试结果,要求每个设备规格恰好包含 fresh_install、uninstall 和 reinstall 三条路径。每条记录都要携带同一 candidate_sha256、前置包状态、预期数据状态、结果和失败回执。脚本不安装或卸载任何应用,只验证矩阵完整性并输出发布 gate,适合放在回执汇总之后。
路径前置规则是显式的:首次安装前和重装前 package_present 必须为 false,卸载前必须为 true;三条路径都要声明 expected_data_state。失败记录必须提供 failure_receipt,成功记录则要求 identity_verified 为 true。这样可以阻止用缺少候选身份的截图、没有卸载回执的重装结果或前置状态错误的覆盖安装混入矩阵。
脚本把缺少路径、候选摘要不一致、失败回执为空、成功后身份未核对和不确定结果标记为 BLOCK。它不判断数据是否真的清除,也不推断某次失败由加固造成;这些需要具体状态证据和设备日志。实际流水线还应把原始 PackageInstaller 回调、APK set 摘要和测试时间绑定到 receipt ID。
import json
import sys
from collections import defaultdict
from pathlib import Path
PATH_RULES = {
"fresh_install": False,
"uninstall": True,
"reinstall": False,
}
VALID_RESULTS = {"success", "failed", "inconclusive"}
REQUIRED = {
"device_profile",
"path",
"candidate_sha256",
"pre_package_present",
"expected_data_state",
"result",
"identity_verified",
"failure_receipt",
}
def stop(message):
raise SystemExit(f"invalid lifecycle matrix: {message}")
if len(sys.argv) != 2:
raise SystemExit("usage: python lifecycle_gate.py RESULTS_JSON")
result_path = Path(sys.argv[1]).resolve()
if not result_path.is_file():
stop(f"file not found: {result_path}")
payload = json.loads(result_path.read_text(encoding="utf-8"))
records = payload.get("records")
if not isinstance(records, list) or not records:
stop("records must be a non-empty list")
by_device = defaultdict(dict)
for index, record in enumerate(records):
if not isinstance(record, dict):
stop(f"record {index} must be an object")
missing = sorted(REQUIRED.difference(record))
if missing:
stop(f"record {index} missing {missing}")
path = record["path"]
device = record["device_profile"]
candidate = record["candidate_sha256"]
if path not in PATH_RULES or not isinstance(device, str) or not device:
stop(f"record {index} has invalid path or device")
if not isinstance(candidate, str) or len(candidate) != 64:
stop(f"record {index} has invalid candidate_sha256")
if record["pre_package_present"] is not PATH_RULES[path]:
stop(f"record {index} has wrong package precondition")
if not isinstance(record["expected_data_state"], str) or not record["expected_data_state"]:
stop(f"record {index} lacks expected_data_state")
if record["result"] not in VALID_RESULTS:
stop(f"record {index} has invalid result")
if record["result"] == "failed" and not record["failure_receipt"]:
stop(f"record {index} lacks a failure receipt")
if path in by_device[device]:
stop(f"device {device} repeats path {path}")
by_device[device][path] = record
report = []
for device, paths in sorted(by_device.items()):
missing_paths = sorted(set(PATH_RULES).difference(paths))
candidates = {item["candidate_sha256"] for item in paths.values()}
blocked = bool(missing_paths) or len(candidates) != 1
blocked = blocked or any(item["result"] != "success" for item in paths.values())
blocked = blocked or any(not item["identity_verified"] for item in paths.values())
report.append({
"device_profile": device,
"candidate_sha256": next(iter(candidates)) if len(candidates) == 1 else None,
"missing_paths": missing_paths,
"gate": "BLOCK" if blocked else "PASS",
})
print(json.dumps({"lifecycle_gate": report}, ensure_ascii=False, indent=2))设备矩阵与发布规则负责最终关闭
设备矩阵不只是型号列表。Firebase Test Lab 的矩阵由设备、系统、方向和 locale 等配置组成,任一执行失败都会影响矩阵判断。安装生命周期还应补充 ABI、渠道、存储状态和备份策略。选择范围要基于真实用户与最低支持边界;云端矩阵可以扩大覆盖,却不能自动代表所有厂商状态和线下分发环境。
发布阻断条件应写入规则:任一路径前置状态不成立;候选或签名身份错配;PackageInstaller 最终回执缺失;卸载后目标包仍可达;重装没有引用有效卸载回执;数据状态与产品契约冲突;核心路径无法重入;矩阵结果不确定。阻断记录要指出具体设备、路径、阶段和缺失字段,便于直接复现。
通过也只在证据覆盖范围内成立。最终候选需在目标设备与渠道上完成三条路径,身份核对、数据断言和核心业务回执齐全,未测配置明确列出。需要评估实际加固版本时,可通过御盾中央平台提交脱敏候选摘要、签名摘要、APK set 清单、三条路径回执、数据状态契约和失败阶段,先核对生命周期对象,再判断是否调整加固范围。
- 每个目标设备规格都包含首次安装、卸载和重装三条记录
- ABI、渠道、存储、locale 和备份策略进入矩阵维度
- 失败定位到具体路径、阶段、候选和数据断言
- 所有成功结果完成安装后身份核对
- 核心路径重入回执与安装会话回执分别保存
- 未覆盖设备、渠道和数据恢复条件写入发布边界
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| split 安装要求 base、packageName、versionCode 与签名证书一致。 | Android PackageInstaller 描述安装会话、split APK 一致性和最终状态回调。 | API 约束不能证明应用商店最终派生的全部 split 已被抽样,也不能证明三条生命周期路径已通过。 |
| Android 覆盖更新要求保持应用身份、可接受的 versionCode 和兼容签名关系。 | How Android app updates work 说明 applicationId、versionCode、签名证书与轮换证明对更新的要求。 | 更新条件不等于卸载后重装语义,也不能替代业务数据恢复与重新认证测试。 |
| Android 通过应用签名建立安装和更新身份,不同签名方案覆盖不同文件区域与平台版本。 | AOSP app signing 说明 APK 签名用途、验证关系与各签名方案的适用范围。 | 签名有效只证明完整性与签名者身份,不证明加固逻辑安全、生命周期兼容或业务数据正确。 |
| App Bundle 可通过本地工具与测试轨道验证设备交付行为。 | Build and test Android App Bundles 说明使用 bundletool、本地部署和测试轨道测试 AAB。 | 本地生成不能完全替代应用商店线上交付回执,单次轨道安装也不覆盖全部设备派生。 |
| bundletool 可以从 AAB 生成、检查和部署按设备配置选择的 APK set。 | bundletool 文档说明 APK set 生成、设备规格选择、检查与部署能力。 | 未明确发布签名时工具可能使用调试身份,不能把该派生集当成生产重装候选。 |
| Android 测试矩阵由设备型号、系统、方向和 locale 等维度组成。 | Firebase Test Lab Android matrices 描述设备矩阵配置和矩阵执行结果。 | 云真机范围仍需依据真实用户与最低支持条件选择,不代表所有 ABI、厂商和渠道。 |
| 首次安装、卸载与重装应分别定义前置包状态和数据状态。 | 工程判断:三条路径的状态转换不同,合并脚本只检查最终页面会掩盖无效前置与中间失败。 | 具体数据保留、备份恢复、服务端注销和外部文件策略必须由项目契约与实测确认。 |
| 同候选重装应从已验证的卸载后状态开始,并重新完成核心路径。 | 工程判断:覆盖安装、清数据后启动与卸载后重装改变的变量不同,不能互相替代证据。 | 没有目标候选、设备、渠道和三条真实回执时,不能声明当前加固版本已通过生命周期回归。 |
工程常见问题
首次安装成功后直接覆盖安装,能算重装验证吗?
不能。覆盖安装保留已安装包并遵循更新身份;重装必须先取得卸载回执、确认包不存在,再重新安装并核对数据与核心路径。
卸载后是不是所有应用相关数据都必须消失?
不能统一假设。应分别声明本地、外部、备份和服务端状态的预期,再按产品契约验证保留、清除或重新认证。
桌面图标出现能否说明加固包首次安装通过?
不能。还要核对 PackageInstaller 最终状态、已安装身份、初始数据、首次启动、必要权限和最小核心业务闭环。
同一个文件名的 APK 可以沿用之前的生命周期结果吗?
不可以。候选应按文件摘要、applicationId、versionCode、签名和派生集标识,字节或签名变化就需要新的回执。
bundletool 本地三条路径都成功,是否足够发布 AAB?
不够。本地派生可检查特定设备组合,还需要受控测试轨道验证线上处理后的真实交付,并明确未覆盖设备。
排查安装生命周期回归需要准备哪些材料?
准备候选与签名摘要、APK set 清单、设备条件、三条路径前置状态、PackageInstaller 回执、数据契约和核心路径失败阶段。