先看结论与判断条件

  • 应用内更新只有在 Play 认定目标版本对测试账号可用时才能形成有效回归。
  • 源版本、目标 versionCode、轨道、签名与派生 APK 必须属于同一发布链。
  • Flexible 流程要覆盖下载完成后的确认和 Activity 重建,immediate 要覆盖全屏更新与恢复。
  • 取消、网络中断、存储不足、进程回收和安装失败都要有继续或退出规则。
  • Play 状态、安装器状态和应用实际版本是三类证据,不能互相代替。
  • 验收只覆盖列明账号、轨道、源版本、设备和目标候选。

先证明测试账号确实能看到目标更新

先把测试账号、源版本、目标版本和更新可用性写成可复核对象,而不是凭页面印象判断。账号、国家、轨道、installedVersion、availableVersion 和查询时间应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,流程未启动是发布不可见还是客户端调用问题才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。

用 Play 测试轨道和授权账号查询可用性,记录传播等待。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。

目标版本对测试账号不可见时先标记 blocked,并把剩余未知写进报告。Play 发布传播和账号资格不由加固代码单独决定。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。

先证明测试账号确实能看到目标更新检查表
检查对象输入证据判定动作失败处置
测试账号、源版本、目标版本和更新可用性身份账号、国家、轨道、installedVersion、availableVersion 和查询时间流程未启动是发布不可见还是客户端调用问题目标版本对测试账号不可见则阻塞
同源基线候选摘要与时间窗锁定比较对象来源不明则阻塞
执行记录用 Play 测试轨道和授权账号查询可用性,记录传播等待保存系统回执只有口头结论则补证
适用边界Play 发布传播和账号资格不由加固代码单独决定限制结论范围不得扩大为全局结论

绑定 Play 轨道、签名与派生 APK

App Bundle、轨道、证书、split 与派生 APK要从状态变化而不是最终页面开始核对。把bundle 摘要、edit/track、证书摘要、split 配置和设备 APK 摘要按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断测试安装包是否真实来自目标 Play 发布候选发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。

从发布 edit 和设备安装结果双向核对候选与证书。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。

验收不能只看操作完成或命令退出码。本地侧载包与 Play 轨道候选或签名不一致说明链路仍缺少可归因证据;本地测试派生 APK 不能替代真实 Play 轨道验收。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。

绑定 Play 轨道、签名与派生 APK检查表
检查对象输入证据判定动作失败处置
App Bundle、轨道、证书、split 与派生 APK身份bundle 摘要、edit/track、证书摘要、split 配置和设备 APK 摘要测试安装包是否真实来自目标 Play 发布候选本地侧载包与 Play 轨道候选或签名不一致则阻塞
同源基线候选摘要与时间窗锁定比较对象来源不明则阻塞
执行记录从发布 edit 和设备安装结果双向核对候选与证书保存系统回执只有口头结论则补证
适用边界本地测试派生 APK 不能替代真实 Play 轨道验收限制结论范围不得扩大为全局结论

分别画出 flexible 和 immediate 状态机

处理flexible、immediate 的客户端更新状态时,先固定应用身份、候选摘要、平台版本和可变条件。updateType、availability、installStatus、bytes、错误码和时间必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论每种更新类型在哪个状态停住或丢失监听,否则重试成功也只能说明环境变了,不能支持技术结论。

为两种类型分别建立允许、取消、失败和恢复路径。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。

出现flexible 和 immediate 状态被混在一条结论,应回到上一份同源输入,比较首次分叉点并保留两侧回执。两种流程的 UI 与恢复契约不同,不能互相推定。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。

分别画出 flexible 和 immediate 状态机检查表
检查对象输入证据判定动作失败处置
flexible、immediate 的客户端更新状态身份updateType、availability、installStatus、bytes、错误码和时间每种更新类型在哪个状态停住或丢失监听flexible 和 immediate 状态被混在一条结论则阻塞
同源基线候选摘要与时间窗锁定比较对象来源不明则阻塞
执行记录为两种类型分别建立允许、取消、失败和恢复路径保存系统回执只有口头结论则补证
适用边界两种流程的 UI 与恢复契约不同,不能互相推定限制结论范围不得扩大为全局结论

验证下载完成后的确认与版本切换

先把下载、等待确认、安装和启动新版本写成可复核对象,而不是凭页面印象判断。下载完成、用户确认、安装会话、包替换、首次启动和版本应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,下载完成后是否真正进入安装并切换 versionCode才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。

下载后读取安装状态与 PackageInfo,不只看进度条。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。

下载完成但 versionCode 始终未切换时先标记 blocked,并把剩余未知写进报告。版本切换不证明应用数据迁移正确,数据迁移属于相邻专题。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。

覆盖 Activity 重建和进程回收

Activity 实例、进程、监听器与当前状态要从状态变化而不是最终页面开始核对。把onCreate、onResume、监听注册、保存状态、进程标识和恢复结果按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断重建后是否恢复状态而不是重复启动或永久等待发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。

在关键状态旋转屏幕、后台回收或重建 Activity 并保存事件。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。

验收不能只看操作完成或命令退出码。Activity 重建后监听丢失或重复确认说明链路仍缺少可归因证据;生命周期行为必须以真实设备回执为准。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。

覆盖 Activity 重建和进程回收检查表
检查对象输入证据判定动作失败处置
Activity 实例、进程、监听器与当前状态身份onCreate、onResume、监听注册、保存状态、进程标识和恢复结果重建后是否恢复状态而不是重复启动或永久等待Activity 重建后监听丢失或重复确认则阻塞
同源基线候选摘要与时间窗锁定比较对象来源不明则阻塞
执行记录在关键状态旋转屏幕、后台回收或重建 Activity 并保存事件保存系统回执只有口头结论则补证
适用边界生命周期行为必须以真实设备回执为准限制结论范围不得扩大为全局结论

测试取消、断网、空间不足与失败恢复

处理取消、网络、存储、安装错误和重试时,先固定应用身份、候选摘要、平台版本和可变条件。用户动作、网络状态、可用空间、错误码、重试次数和最终版本必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论失败后能否安全重试、取消或继续使用旧版本,否则重试成功也只能说明环境变了,不能支持技术结论。

在授权设备控制网络与空间,失败现场不被重装覆盖。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。

出现失败只能靠清数据或重装恢复,应回到上一份同源输入,比较首次分叉点并保留两侧回执。测试不得破坏真实用户数据或生产轨道。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。

区分 Play、PackageInstaller 与应用版本回执

先把Play 更新信息、安装会话与包管理器版本写成可复核对象,而不是凭页面印象判断。Play 回执、session ID、PackageInfo、证书和时间顺序应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,每层状态是否时间一致且指向同一更新会话才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。

按时间线合并三类回执但保留原始来源。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。

三类回执时间或会话无法关联时先标记 blocked,并把剩余未知写进报告。PackageInstaller 成功不等于业务首次启动通过。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。

区分 Play、PackageInstaller 与应用版本回执检查表
检查对象输入证据判定动作失败处置
Play 更新信息、安装会话与包管理器版本身份Play 回执、session ID、PackageInfo、证书和时间顺序每层状态是否时间一致且指向同一更新会话三类回执时间或会话无法关联则阻塞
同源基线候选摘要与时间窗锁定比较对象来源不明则阻塞
执行记录按时间线合并三类回执但保留原始来源保存系统回执只有口头结论则补证
适用边界PackageInstaller 成功不等于业务首次启动通过限制结论范围不得扩大为全局结论

用代码审计更新状态日志

脱敏更新事件日志和状态序列要从状态变化而不是最终页面开始核对。把caseId、候选摘要、预期状态、观察状态、结果和证据按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断日志是否覆盖必需状态且错误通过被拒绝发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。

把状态记录写成 JSON,让校验器拒绝预期观察不一致。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。

验收不能只看操作完成或命令退出码。预期状态和观察状态不一致却标记通过说明链路仍缺少可归因证据;代码只审计日志,不调用 Play API、不发布版本。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。

审计 Play 应用内更新状态日志
import hashlib
import json
import sys
from pathlib import Path

REQUIRED = set([
  'caseId',
  'candidateSha256',
  'sourceVersion',
  'track',
  'expectedState',
  'observedState',
  'result',
  'evidenceRef'
])
ALLOWED_RESULTS = set([
  'pass',
  'blocked',
  'mismatch'
])
FORBIDDEN = {'token', 'password', 'privateKey', 'secret', 'customerData'}

def stop(message):
    raise SystemExit(message)

def digest(path):
    hasher = hashlib.sha256()
    with path.open('rb') as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b''):
            hasher.update(chunk)
    return hasher.hexdigest()

def text(record, field, index):
    value = str(record[field]).strip()
    if not value:
        stop(f'record {index} has empty {field}')
    return value

def validate(record, index):
    if not isinstance(record, dict):
        stop(f'record {index} is not an object')
    if FORBIDDEN.intersection(record):
        stop(f'record {index} contains forbidden fields')
    missing = sorted(REQUIRED - record.keys())
    if missing:
        raise SystemExit(f'record {index} lacks fields: {missing}')
    for field in REQUIRED:
        if field != 'sourceVersion':
            text(record, field, index)
    result = text(record, 'result', index)
    if result not in ALLOWED_RESULTS:
        stop(f'record {index} has an unsupported result')
    expected = text(record, 'expectedState', index)
    observed = text(record, 'observedState', index)
    evidence = text(record, 'evidenceRef', index)
    if expected != observed and result == 'pass':
        stop(f'record {index} passes despite a mismatch')
    if expected != observed and not evidence:
        stop(f'record {index} has a mismatch without evidence')
    return text(record, 'caseId', index)

if len(sys.argv) != 2:
    stop('usage: python validate_in_app_update.py public-checks.json')
source = Path(sys.argv[1]).expanduser().resolve()
if not source.is_file():
    stop('public check file is missing')
raw = source.read_bytes()
data = json.loads(raw.decode('utf-8'))
records = data.get('updateCases') if isinstance(data, dict) else None
if not isinstance(records, list) or not records:
    raise SystemExit('updateCases must be a non-empty list')
identifiers = [validate(record, index) for index, record in enumerate(records)]
if len(identifiers) != len(set(identifiers)):
    raise SystemExit('record identifiers are duplicated')
summary = {
    'inputSha256': hashlib.sha256(raw).hexdigest(),
    'records': len(records),
    'results': {state: sum(1 for item in records if item['result'] == state) for state in sorted(ALLOWED_RESULTS)},
    'status': 'pass',
}
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))

按源版本和设备关闭更新验收

处理源 versionCode、设备、轨道、目标候选与回执时,先固定应用身份、候选摘要、平台版本和可变条件。通过路径、失败路径、未测设备、owner 和复核状态必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪些源版本和设备已验证连续升级,否则重试成功也只能说明环境变了,不能支持技术结论。

从最低支持源版本逐设备抽查并登记未覆盖范围。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。

出现只验证最新源版本或单一设备,应回到上一份同源输入,比较首次分叉点并保留两侧回执。结论不扩展到未列明渠道、账号、版本或设备。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
更新必须保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。How Android app updates work:更新必须保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。平台更新条件不等于业务层防重放策略。
发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。Sign your Android app:发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。文档不能确认某个实际包使用了正确生产证书。
bundletool 与测试轨道可重现和验证 AAB 派生 APK 的设备交付行为。Build and test Android App Bundles:bundletool 与测试轨道可重现和验证 AAB 派生 APK 的设备交付行为。本地生成不能完全替代 Play 线上交付回执。
split 安装要求 base、packageName、versionCode 与签名证书一致。Android PackageInstaller:split 安装要求 base、packageName、versionCode 与签名证书一致。API 约束不证明 Play 服务器最终生成的所有 split 已被抽样。
依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。Android instrumented tests:依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。单一设备通过不能代表完整 API、ABI 和厂商矩阵。
Track release 明确记录 versionCodes、status、userFraction、countryTargeting 与更新优先级;inProgress 和 halted 状态可表达灰度覆盖与停止继续分发。Google Play Developer API tracks:Track release 明确记录 versionCodes、status、userFraction、countryTargeting 与更新优先级;inProgress 和 halted 状态可表达灰度覆盖与停止继续分发。halted 不影响已经安装该版本的用户,不能被写成客户端自动回滚或召回。
应用内更新验收必须把 Play、安装器与实际包版本三层状态关联。工程判断:任一层单独成功都可能仍停在下载、等待确认或包未切换状态。关联依赖会话、时间和版本字段,平台传播延迟要单独标注。
Flexible 与 immediate 更新需要独立覆盖生命周期和失败恢复。工程判断:两种流程的用户交互、Activity 行为和恢复节点不同。具体 UI 由应用实现,文章不承诺固定文案或响应时间。

工程常见问题

Play 控制台已经发布,为什么查询不到更新?

核对测试账号资格、国家、轨道、源 versionCode、目标版本、发布传播和安装来源;控制台发布不保证指定设备立即可见。

下载到 100% 就算更新成功吗?

不算。还要确认安装状态、用户确认、包替换、目标 versionCode 和新版本首次启动。

Flexible 和 immediate 可以共用一套回归吗?

状态字段可复用,但交互、Activity 重建、确认和失败恢复路径不同,必须分别验收。

侧载目标 APK 能验证 Play 应用内更新吗?

只能辅助检查候选运行,不代表 Play 轨道可用性、split 选择、签名和更新会话真实有效。

旋转屏幕后卡住怎样判断?

记录重建前后的更新状态、监听注册、进程、会话和 PackageInfo,确认是状态恢复、重复请求还是监听丢失。

提交御盾诊断准备什么?

准备源与目标 versionCode、轨道、账号资格、bundle 和设备 APK 摘要、签名、设备、两类流程日志、错误码和 PackageInfo 回执。

想用自己的 App 验证?

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

继续阅读: App 加固 PoC 如何形成发布结论