先看结论与判断条件

  • 升级预检必须从真实已发布基线出发,开发机旧包或同版本重建文件不能代表用户设备上的安装身份和数据状态。
  • applicationId、可接受的 versionCode 和兼容签名关系是平台更新条件,任一错配都不能用全新安装成功弥补。
  • 应用签名密钥、上传密钥和 Play App Signing 责任不同,预检应核对用户设备最终看到的签名身份,而不是只看上传文件。
  • 签名轮换要结合目标 SDK、轮换证明、分发渠道和历史安装基础判断,文档支持能力不等于当前用户都能连续升级。
  • split 安装会话中的 base、packageName、versionCode 和签名证书必须一致,抽查单个 base APK 不能代表完整交付集合。
  • 静态预检通过后仍需在代表性设备上覆盖安装,验证启动、数据迁移、组件、权限、后台任务和关键业务状态。

先定义升级连续性,不要只测新包能启动

全新安装和覆盖升级走的是不同状态。全新安装没有历史签名关系、旧数据库、持久化账号、已授予权限、后台任务和缓存;升级则必须在保留应用身份与用户状态的前提下替换代码和资源。一个候选能够在空设备启动,只能证明安装后某条路径可达,不能证明线上用户能从旧版本连续升级。

升级连续性至少包含安装身份、版本顺序、签名关系、split 集合、数据迁移和运行语义。静态预检先回答包名、versionCode、证书与安装契约是否满足;设备预检再回答真实系统能否完成覆盖安装,以及升级后关键状态是否仍可读取和更新。两类证据缺一不可,也不能由同一条“安装成功”日志代替。

基线选择决定结论可信度。团队应从实际渠道保存已发布 APK 或可重建的设备交付集合,记录文件摘要、包名、versionCode、签名证书和发布时间。开发目录中的旧 debug 包即使版本接近,也可能使用不同证书和配置。工程判断是按真实用户占比与风险选择多个基线,而不是永远只从上一内部测试版升级。

全新安装与覆盖升级的证据差异
场景初始状态主要判断不能替代
全新安装无应用与历史数据新候选可被安装和首次启动覆盖升级
上一版本升级近期发布身份与数据常规版本连续性长期滞后用户路径
跨版本升级旧 schema 与旧权限状态多步迁移是否可合并单步迁移
签名轮换升级历史证书与目标平台差异轮换关系是否被接受同证书升级
split 组合升级设备实际安装集合完整会话契约和资源一致性只测 base APK

用真实发布基线核对 applicationId 与 versionCode

How Android app updates work 说明应用更新需要保持 applicationId,并使用可接受的 versionCode,同时满足签名关系。applicationId 改变会被系统视为另一应用,无法覆盖原安装;versionCode 不符合更新顺序时,常规更新路径也会失败。预检应从 APK 解析这些字段,而不是只读取 Gradle 配置,因为最终产物才是设备判断对象。

版本名称适合用户展示,不承担平台升级顺序。两个候选可以有不同 versionName 却使用错误 versionCode,也可能因多渠道配置出现相同 versionCode 的不同字节。清单要保存基线与候选的 applicationId、versionCode、versionName 和文件摘要,比较时以平台字段和真实字节为准。任何重新构建都产生新候选摘要。

平台更新条件不等于业务防重放策略。versionCode 较高只能说明顺序符合基本更新要求,不能证明服务端仍接受该客户端、数据库迁移完整或旧漏洞已经处置。工程判断是先用平台字段排除不可能升级的候选,再把最低支持版本、服务端契约和数据迁移作为独立业务门禁。

基线与候选静态对照字段
字段基线来源候选来源失败含义
applicationId已发布 APK 解析最终候选解析系统将其视为不同应用
versionCode设备或渠道基线最终候选解析不满足预期升级顺序
文件摘要已发布归档候选归档报告对应了不同文件
签名关系基线证书与历史记录候选证书和证明更新身份可能不连续
split 契约设备交付集合候选交付集合安装会话可能不闭合

区分应用签名密钥、上传密钥与设备身份

Sign your Android app 区分应用签名密钥、上传密钥、证书和 Play App Signing 的责任。启用托管签名后,团队上传的制品可以由上传密钥认证,用户设备收到的 APK 则由应用签名密钥签署。若预检只比较本地上传文件证书,可能验证了错误身份。证据必须说明文件处于哪一交付阶段。

Android apksigner 可验证签名方案覆盖并打印证书,也要求签名后不再修改 APK。基线和候选都应保存 apksigner 完整输出、工具版本、文件摘要和证书 SHA-256。工具验证成功证明当前文件签名结构有效,但不自动证明证书是正确生产身份,也不证明渠道最终交付文件与本地文件相同。

允许身份来源应受控。项目可以从已发布基线、渠道证书记录和审批台账建立当前签名关系,不应由待验候选自己声明“允许证书”。若基线与候选证书相同,仍需检查 scheme 与目标平台;若证书不同,则必须进入轮换关系核验,不能直接以“新证书更安全”为理由放行。

签名相关对象不能混用
对象使用位置预检问题常见误判
应用签名密钥用户设备上的应用身份候选是否延续更新身份只核对上传证书
上传密钥向托管渠道提交制品上传者是否被渠道接受把上传身份当设备身份
签名证书公开身份和摘要比较当前文件由谁签署只看证书主题名称
签名方案不同平台的验证规则目标设备是否覆盖总体 verify 成功就结束
候选摘要绑定实际文件字节回执是否对应同一文件只凭版本号归档

签名轮换要结合 v3.1、目标 SDK 与历史安装基础

APK Signature Scheme v3.1 描述签名轮换、最低轮换 SDK 以及与 v4 的关联。它提供平台能力,但并不意味着换证书后的候选能覆盖所有历史安装。旧系统版本、不同渠道、早期签名状态和用户实际安装来源都会影响连续性。预检必须按设备系统和基线签名状态建立矩阵。

轮换预检至少记录旧证书、新证书、轮换证明、最低轮换 SDK、签名方案结果和目标渠道。对低于轮换能力边界的设备,可能需要继续使用兼容签名路径;对支持新机制的设备,也要确认实际分发文件包含正确证明。本文只处理发布前连续性预检,不给出证书轮换操作步骤。

轮换关系的静态解析还需要设备验证。即使工具显示 lineage 存在,真实设备仍可能因为系统版本、安装来源或历史状态拒绝更新。工程判断是选择同一基线在轮换边界两侧的设备执行覆盖安装,并保存安装前证书、系统版本、候选摘要、安装结果和失败码。单一新设备通过不能代表历史用户群。

签名轮换连续性矩阵
维度需要记录验证方式边界
旧证书基线设备实际签名身份安装前提取并归档渠道记录不能替代设备事实
新证书候选签名身份apksigner 与候选摘要有效不等于关系兼容
轮换证明证书 lineage 与方案静态解析和策略核验仍受平台版本影响
最低轮换 SDK候选声明与目标范围边界设备对照测试文档能力不代表设备状态
分发渠道历史来源和交付签名渠道回执与设备包核对本地生成不能代替线上交付

split 安装会话必须满足同一安装契约

Android PackageInstaller 说明 split 安装会话要求 base、packageName、versionCode 和签名证书保持一致。现代应用的设备交付物可能包含 base、ABI、密度、语言和动态功能 split。只验证 base APK 的包名和证书,不能证明组合会话中的其他 split 属于同一版本与签名身份。

预检应先固定代表性设备规格,再生成或获取对应 APK 集,逐个解析 packageName、versionCode、splitName 和签名证书。缺失 base、混入旧 split、versionCode 不一致或证书不一致都应在安装前失败。设备上已有 split 集合也要记录,因为升级可能替换、增加或移除模块,残留状态会影响路径。

PackageInstaller API 约束不证明渠道服务器最终为所有设备生成的 split 都已抽样。工程判断是选择业务支持的 ABI、密度、语言和动态功能边界生成代表组合,再用真实渠道的内部轨道或等价回执补充。没有渠道证据时,只能记录本地 APK set 预检结果,不能扩大为线上交付已验证。

split 安装契约检查
检查会话要求异常示例处置
base 存在每个完整应用会话有 base只收集到配置 split停止安装并重建集合
packageName 一致全部 split 属于同一应用混入其他渠道文件隔离并追查归档
versionCode 一致会话是同一版本旧 ABI split 未替换重新生成设备集合
签名证书一致全部成员满足签名身份单个 feature 使用旧签名阻断并重新签名
splitName 唯一集合不含重复角色两份同名语言 split按设备规格重选

设备端升级回归要从历史状态开始

Android instrumented tests 适合验证依赖真实 Android 运行时、组件和系统 API 的语义。升级预检应先安装指定基线,写入可识别的测试数据和状态,再覆盖安装候选,最后运行设备端断言。这样可以验证数据库迁移、文件权限、ContentProvider、后台任务、通知渠道和关键业务入口,而不是只看安装命令退出成功。

测试数据要能区分迁移前后。可准备旧 schema 数据、登录状态、加密存储、待处理任务和用户配置,并记录预期保留、转换或清除的字段。升级后既要检查新版本启动,也要检查旧状态是否按业务规则迁移。若应用包含多进程、直接启动组件或开机任务,还要覆盖升级后首次进程唤醒。

单一设备通过不能代表完整 API、ABI 和厂商矩阵。设备选择应围绕最低支持版本、签名轮换边界、主要 ABI、厂商系统差异和关键存储行为。本文不提供通用设备数量或性能阈值。真实项目应根据用户分布和故障半径确定矩阵,并将每条结果绑定基线摘要与候选摘要。

代表性升级路径的回归对象
路径升级前状态升级后断言证据
数据库迁移旧 schema 与样本记录版本、字段和数据含义正确迁移日志与查询断言
账号与凭据已登录测试会话按产品规则保留或失效状态变化与重新认证结果
后台任务旧版本排队任务不重复、不丢失或按规则取消任务标识和执行记录
权限与组件已授予权限和已注册组件关键入口仍按系统规则工作instrumented test 结果
split 变化旧设备安装集合新集合完整且业务入口可达安装前后 split 清单

把静态预检、安装结果和业务回归分层放行

发布门禁可以分三层。第一层核对 applicationId、versionCode、摘要、签名和 split 契约;第二层在目标设备从基线执行覆盖安装,保存安装会话和系统结果;第三层执行数据迁移与关键业务断言。上层通过不应掩盖下层失败,报告要明确每一层的基线和候选身份。

失败类别要能直接路由。包名或 versionCode 错配归构建配置,证书或 lineage 错配归签名发布,split 不一致归交付生成,安装会话失败归平台兼容,数据迁移和组件语义失败归应用研发。统一写成“升级失败”会让团队重复安装,却不修真正的契约问题。

预检通过也只是限定范围的结论。它应写成某基线摘要、某候选摘要、某签名关系和某设备矩阵通过,不外推到未覆盖渠道或设备。没有真实项目回执时,本文只提供工程方法,不声称具体 APK 已兼容升级,也不声称任何攻击阻断或性能结果。

  • 基线来自真实发布归档或设备交付集合
  • 候选摘要在所有签名和修改完成后固定
  • applicationId 与 versionCode 由最终 APK 解析
  • 签名关系按目标设备和渠道判断
  • 完整 split 集合满足同一安装契约
  • 设备回归从有历史状态的基线开始
  • 每条结论绑定基线、候选和设备身份

用只读脚本核对升级身份与安装契约

下面的 Python 示例读取基线与候选的结构化检查结果。结果应由受控工具从实际 APK 或 APK set 提取,包含文件摘要、packageName、versionCode、签名验证、签名连续性判断和 split 列表。脚本拒绝包名变化、版本不递增、签名未验证、连续性未确认以及 split 的包名、版本或证书不一致。

脚本不会安装 APK,也不会自行判断证书轮换证明。signerContinuityVerified 必须来自平台感知的签名关系检查,并与原始 apksigner 输出和设备边界一起归档。把复杂轮换判断简化为证书字符串相等会误拒合法轮换,也可能漏掉渠道差异,因此示例只消费已核验结论并检查安装契约闭合。

准备 APK 升级连续性的软件加固评估时,可整理真实基线、最终候选、签名与 lineage 回执、设备规格、APK set、数据迁移方案和 instrumented test 结果,再通过御盾中央平台提交申请。正式结论必须绑定实际文件和设备,不能由静态设计清单代替。

  • 基线与候选检查结果来自实际文件
  • 文件摘要和签名完整回执另行归档
  • signerContinuityVerified 由平台感知检查产生
  • 全部 split 包名、版本和证书一致
  • 脚本通过后继续执行设备覆盖安装
  • 数据迁移与业务状态使用独立断言
  • 申请评估时提交真实升级证据
核对基线与候选升级身份及 split 契约的只读脚本
from pathlib import Path
import hashlib
import json
import sys

if len(sys.argv) != 3:
    raise SystemExit(2)
baseline_path = Path(sys.argv[1])
candidate_path = Path(sys.argv[2])
if not baseline_path.is_file() or not candidate_path.is_file():
    raise SystemExit(2)
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
required = ["apkSha256", "packageName", "versionCode", "signatureVerified", "certificateSha256", "splits"]
if any(field not in baseline or field not in candidate for field in required):
    raise SystemExit(2)
if baseline["packageName"] != candidate["packageName"]:
    raise SystemExit(2)
if int(candidate["versionCode"]) <= int(baseline["versionCode"]):
    raise SystemExit(2)
if not baseline["signatureVerified"] or not candidate["signatureVerified"]:
    raise SystemExit(2)
if not candidate.get("signerContinuityVerified", False):
    raise SystemExit(2)
splits = candidate["splits"]
if not isinstance(splits, list) or not splits:
    raise SystemExit(2)
split_names = set()
for split in splits:
    if split.get("packageName") != candidate["packageName"]:
        raise SystemExit(2)
    if int(split.get("versionCode", -1)) != int(candidate["versionCode"]):
        raise SystemExit(2)
    if split.get("certificateSha256") != candidate["certificateSha256"]:
        raise SystemExit(2)
    split_name = str(split.get("splitName", ""))
    if split_name in split_names:
        raise SystemExit(2)
    split_names.add(split_name)
if "base" not in split_names:
    raise SystemExit(2)
canonical = json.dumps(candidate, sort_keys=True, separators=(",", ":"))
receipt_digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
result = {"status": "eligible-for-device-upgrade-test", "packageName": candidate["packageName"], "fromVersionCode": baseline["versionCode"], "toVersionCode": candidate["versionCode"], "splitCount": len(splits), "receiptDigest": receipt_digest}
print(json.dumps(result, ensure_ascii=False, indent=2))

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
应用更新需要保持 applicationId、满足签名关系并使用可接受的 versionCode。How Android app updates work 描述 Android 应用更新的基本条件。平台更新条件不等于业务防重放、数据迁移或服务端兼容策略。
应用签名密钥、上传密钥、证书与 Play App Signing 承担不同发布责任。Sign your Android app 说明 Android 发布签名与托管签名流程中的身份分工。文档不能确认某个实际候选使用了正确生产证书或最终渠道签名。
apksigner 可验证签名方案覆盖和证书,签名后修改 APK 会使签名失效。Android apksigner 说明签名与 verify 工具的行为和修改边界。工具验证成功不证明密钥管理、渠道生成、候选归档或设备升级正确。
v3.1 涉及签名轮换、最低轮换 SDK 和与 v4 的关联。APK Signature Scheme v3.1 描述 Android 签名轮换的方案属性。轮换能力仍受分发渠道、系统版本和历史安装基础约束。
split 安装会话要求 base、packageName、versionCode 与签名证书保持一致。Android PackageInstaller 描述多 APK 安装会话和 split 约束。API 约束不证明渠道为所有设备生成的 split 已被真实抽样。
依赖真实 Android 运行时和系统 API 的升级语义应在设备端验证。Android instrumented tests 说明设备或模拟器测试可以访问真实 Android 框架能力。单一设备通过不能代表全部 API、ABI、厂商修改和历史状态。
升级预检应同时绑定真实基线摘要、候选摘要、签名关系和设备矩阵。工程判断:只有固定四类身份,静态结果、安装会话和业务回归才能相互复核。清单闭合不自动证明所有用户路径兼容,仍需真实项目证据。
全新安装成功不能替代从已发布版本执行覆盖升级。工程判断:全新安装缺少历史签名、数据、权限、任务和 split 状态。具体迁移和业务断言由应用设计决定,本文不提供通用通过阈值。

工程常见问题

新 APK 可以全新安装并启动,为什么还要做升级预检?

全新安装没有旧签名关系、数据库、权限、账号、后台任务和 split 状态。升级必须从真实旧版本覆盖安装,并验证这些历史状态按产品规则迁移。

只从上一个 versionCode 升级是否足够?

不一定。仍有用户停留在更旧版本或不同签名、split 与数据状态。应根据真实用户分布和风险选择多个代表基线,不能只用内部测试版。

Play App Signing 场景应该比较上传证书还是应用签名证书?

升级身份关注用户设备最终接收 APK 的应用签名关系。上传密钥用于向渠道认证上传者,两者责任不同,证据中必须标明交付阶段。

apksigner 验证成功能否证明签名轮换升级一定成功?

不能。还要核对 lineage、最低轮换 SDK、分发渠道和历史安装基础,并在轮换边界两侧的代表设备上执行真实覆盖安装。

为什么 base APK 通过预检后还要检查 split?

PackageInstaller 会检查同一会话中的 base、packageName、versionCode 和签名证书。旧 split、混入其他版本或签名不一致都会破坏完整安装契约。

申请 APK 升级连续性评估前要准备什么?

准备真实发布基线、最终候选、签名和轮换回执、代表设备与 APK set、数据迁移方案及 instrumented test 结果,再从御盾中央平台提交申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收