先看结论与判断条件

  • 长稳测试应由可执行状态机定义阶段、动作、断言、超时和证据,单纯延长运行时间不会自动增加有效覆盖。
  • 持续前台、前后台循环、受控系统回收观察和持久任务属于不同生命周期,应分阶段记录并允许单项失败。
  • 每个阶段都要有业务断言,进程仍在或页面还能显示都不能替代数据正确、任务完成和资源可用。
  • ANR 与异常退出必须通过 ApplicationExitInfo、trace、tombstone、日志和测试时间窗关联,不能只看最后一条自动化输出。
  • WorkManager 调度被接受不代表任务按时执行或具备幂等性,约束、重试、配额和重复执行都要纳入回执。
  • 云真机矩阵扩大型号和系统覆盖,但时长、容量与设备可用性有限,发布结论必须明确真实执行矩阵和未覆盖项。

长稳验收的核心是状态变化,不是挂钟时间

把 App 保持在一个静态页面数小时,只能证明该页面在当时设备条件下没有显性退出。它覆盖不了重复导航、缓存积累、前后台生命周期、Binder 重连、任务重试和系统资源变化。有效长稳测试应把时间用于重复真实状态转换,并在每次转换后检查业务结果和资源状态。

测试对象首先绑定 APK 摘要、versionCode、applicationId、签名、ABI 和加固配置摘要。设备记录型号、系统、内存、方向、locale、电源、网络与温度条件。任何候选或关键环境变化都要建立新 run_id,不能把不同设备或重新构建后的片段拼成一条连续时间线。

状态机至少定义阶段名称、进入条件、动作、最大持续时间、业务断言、允许事件、失败事件、采样点和退出条件。某个阶段超时应记录失败并保存现场,而不是继续跑完后只报告总时长。这样长稳结论能回答哪里稳定、哪里阻塞和哪里尚未覆盖。

长稳测试与单纯计时的区别
维度单纯计时状态机验收发布价值
操作页面静置重复真实动作覆盖生命周期
断言进程存在阶段业务结果发现静默错误
失败最终是否退出阶段级失败原因可以定位
时间总时长阶段起止与超时可复现
证据截图或日志尾部结构化事件时间线可审计
结论笼统稳定已测范围和缺口避免扩张

先设计阶段,再决定运行多长时间

一个实用序列可以从安装和冷启动开始,进入持续前台核心路径,再执行若干前后台循环,随后观察受控系统回收条件,验证持久任务,最后返回前台完成业务一致性检查。顺序不是固定模板,关键是每个阶段都有独立目的,且能在失败时保留之前已经完成的回执。

持续时间应来自业务周期和已知风险窗口,例如令牌刷新、长连接心跳、缓存淘汰、定时同步或媒体会话,而不是从一个整齐数字出发。测试计划记录为什么选择这个窗口,以及它没有覆盖哪些更长周期。没有依据的超长运行会消耗设备时间,却不一定触发新的行为。

每个阶段设置有界重试。设备暂时离线、自动化控件未出现和业务断言失败要使用不同错误码;只有明确属于基础设施的短暂错误才允许重试。业务失败不能通过重复直到偶然成功来抹掉,首次失败及后续结果都应保留。

建议阶段与阶段出口
阶段主要动作业务断言出口
候选确认核对安装与摘要版本和签名匹配身份一致
持续前台循环核心路径结果持续正确达到计划窗口
前后台切换离开并返回会话和界面可用循环完整
回收观察进入受控系统条件退出类型可解释证据齐全
持久任务满足或改变约束任务状态符合契约完成或有界失败
最终一致性返回并查询状态业务结果未静默损坏生成回执

持续前台必须循环核心业务断言

前台阶段应选择会重复分配资源、访问 Native 代码、刷新数据或驱动渲染的真实核心路径。每轮记录开始、关键步骤、业务结果、耗时、进程和资源摘要。只做随机点击会增加路径数量,却很难复现;只做健康检查又可能绕开加固影响最大的类加载和 Native 入口。

断言要验证用户可观察结果和必要的内部一致性,例如列表内容对应固定数据集、媒体播放状态与时间推进一致、离线操作进入正确队列。页面未崩溃不代表返回值正确,自动化点击成功也不代表后台任务完成。每轮至少有一个能失败的业务断言。

失败后立即保存当前阶段、最近动作、截图、结构化日志、进程列表和系统诊断,再决定是否继续。若自动化脚本在异常页面继续点击,后续大量错误会掩盖首个故障。长稳测试的价值是捕获最早偏离,而不是生成最多红色日志。

前台循环的证据层次
层次示例证明内容不足
自动化动作点击或滑动成功输入已发送不证明业务正确
界面断言目标状态可见页面结果符合预期不证明持久状态
业务断言固定数据结果一致核心逻辑可用仅覆盖该路径
进程检查目标进程存在当前采样时存活不证明无静默错误
资源摘要内存和线程趋势提供压力背景不单独证明泄漏
系统诊断ANR 与退出记录系统侧异常信号需绑定时间窗

前后台切换要验证生命周期与会话连续性

前后台切换不是简单按 Home 再打开。测试要明确离开方式、后台停留条件、返回入口和目标 Activity,记录 onStop、后台任务、连接状态与恢复后的业务断言。通知跳转、最近任务列表、深链和直接启动入口可能产生不同返回路径,应按产品实际使用选择。

切后台期间系统可能限制网络、延迟任务、释放图形资源或降低进程重要性。加固候选返回前台后,要核对类加载、Native 句柄、会话凭据、Binder 连接、页面数据和待提交操作是否仍符合契约。不要为了测试绕过系统限制或强行常驻进程,那会改变真实环境。

循环次数和停留时间应覆盖快速切换与较长后台两个层次,并保持加固前后条件一致。若设备厂商有额外电源策略,应单独记录。一次成功返回不能代表重复切换稳定;一次系统回收也不能自动判失败,关键在于退出类型是否允许、重新进入后的业务断言是否满足。

前后台阶段的检查点
检查点离开前后台期间返回后
进程记录 PID 与重要性观察是否存活或退出确认新旧进程身份
会话记录非敏感状态标识不主动绕过限制验证授权语义
Native 资源记录功能可用允许生命周期释放重新调用并断言
网络记录连接状态观察系统约束核对重连与失败语义
待办操作固定测试任务记录调度状态检查完成或重试
界面保存业务位置不假设 Activity 存活验证返回入口和结果

系统回收观察与人工终止必须分开

系统因资源压力回收后台进程、用户主动停止、应用自身退出、Java 崩溃、Native 崩溃和 ANR 是不同事件。长稳计划要预先列出允许与禁止的退出类型。人工终止可以验证某些重新进入路径,但不能冒充系统回收,也不能据此推断真实低内存行为。

ApplicationExitInfo 能提供退出原因、时间和相关诊断信息,部分系统版本还可返回 ANR trace 或 Native tombstone。采集时要按候选、进程、用户、时间窗和阶段关联。历史退出记录或另一个测试 run 的崩溃不能补到当前时间线,缺失记录也要说明系统与采集限制。

重新进入后应验证必要业务状态,但本篇重点是长稳阶段的存活、退出归属和业务连续性,不把所有状态恢复策略展开成另一套答案。若退出符合系统预期而业务断言失败,仍是当前阶段失败;若业务恢复正常但退出原因异常,也必须进入调查。

退出类型与长稳判定
事件是否预期关键证据判定
计划内正常停止按用例定义阶段和回调检查清理
系统资源回收部分阶段允许exit reason 与重要性检查返回业务
Java 崩溃不允许堆栈与候选身份阶段失败
Native 崩溃不允许tombstone 与匹配符号阶段失败
ANR不允许trace 与时间线阶段失败
基础设施断连非业务结论设备和测试日志阻塞并复核

持久任务要验证约束、重试和幂等

Android persistent work 文档说明,WorkManager 的持久化调度可跨进程退出和设备重启保存任务,并受约束、重试和系统配额影响。将 WorkRequest 成功入队只证明调度请求被接受,不代表任务会立即运行、按预想时刻完成或业务操作天然幂等。

长稳状态机应记录任务唯一标识、入队时间、约束、attempt、开始、结束、结果和业务副作用摘要。测试要覆盖约束满足、约束暂不满足、任务重试、应用进程离开和再次进入。若业务操作可能重复提交,先提供幂等键或事务边界,再验证系统重试不会造成重复结果。

测试不应通过不断轮询或持有前台来强迫任务执行,那会绕过真实调度条件。超时后保存当前 WorkInfo、设备约束、系统日志和时间线,区分尚未调度、正在运行、重试等待、取消和失败。云真机的会话时长可能不足以覆盖长约束窗口,必须把这类用例放到可持续设备。

用状态机脚本汇总阶段回执

下面的 Python 脚本读取一份公开安全的阶段计划和一份 JSON Lines 事件流。计划按顺序列出 stage 与 required_assertion,事件记录阶段、时间、进程存活、业务断言、ANR 和退出原因。脚本拒绝未知阶段、时间倒序、缺失事件和非法断言状态,并为每个阶段生成通过或失败摘要。

示例要求阶段至少出现一次业务断言,任何 fail、ANR 或非空退出原因都进入 issues。真实项目可以在计划中为某些阶段声明允许的退出原因,但允许项必须显式、有限并经过审批,不能在聚合后临时忽略。脚本不控制设备、不执行终止操作,也不读取用户数据。

事件流应在每个阶段完成时原子落盘,并与 APK、设备和 run_id 绑定。自动化进程意外终止时,已经完成的阶段仍可审计,未完成阶段明确为 missing。最终摘要不是稳定性证明本身,只有当其原始事件、系统诊断和测试条件都可复核时,才能作为发布回执。

校验长稳状态机并汇总阶段存活和异常
import json
import sys
from collections import defaultdict
from pathlib import Path

if len(sys.argv) != 3:
    raise SystemExit('usage: soak_summary.py plan.json events.jsonl')
plan_path = Path(sys.argv[1])
events_path = Path(sys.argv[2])
for path in (plan_path, events_path):
    if not path.is_file():
        raise SystemExit(f'missing input: {path.name}')
plan = json.loads(plan_path.read_text(encoding='utf-8'))
stages = plan.get('stages')
if not isinstance(stages, list) or not stages:
    raise SystemExit('plan stages are missing')
ordered = []
for item in stages:
    name = str(item.get('stage', '')).strip()
    assertion = str(item.get('required_assertion', '')).strip()
    if not name or not assertion or name in ordered:
        raise SystemExit('invalid or duplicate stage')
    ordered.append(name)
events = defaultdict(list)
last_timestamp = -1
for line_number, line in enumerate(events_path.read_text(encoding='utf-8').splitlines(), 1):
    if not line.strip():
        continue
    row = json.loads(line)
    stage = str(row.get('stage', ''))
    timestamp = row.get('timestamp_ms')
    if stage not in ordered:
        raise SystemExit(f'unknown stage at line {line_number}')
    if not isinstance(timestamp, int) or timestamp <= last_timestamp:
        raise SystemExit(f'non-monotonic timestamp at line {line_number}')
    if row.get('assertion') not in ('pass', 'fail'):
        raise SystemExit(f'invalid assertion at line {line_number}')
    last_timestamp = timestamp
    events[stage].append(row)
summary = []
for stage in ordered:
    rows = events.get(stage, [])
    if not rows:
        raise SystemExit(f'missing events for stage {stage}')
    issues = []
    if any(row['assertion'] == 'fail' for row in rows):
        issues.append('business-assertion-failed')
    if any(row.get('anr') is True for row in rows):
        issues.append('anr-observed')
    exits = sorted({str(row['exit_reason']) for row in rows if row.get('exit_reason')})
    if exits:
        issues.append('process-exit-observed')
    alive_samples = sum(1 for row in rows if row.get('alive') is True)
    summary.append({'stage': stage, 'events': len(rows), 'alive_samples': alive_samples, 'exit_reasons': exits, 'issues': issues, 'status': 'pass' if not issues else 'fail'})
print(json.dumps(summary, ensure_ascii=False, indent=2))

ANR 定位要看主线程之前发生了什么

Android ANR 诊断需要区分主线程阻塞、锁竞争、Binder、I/O 和组件超时。长稳运行中,资源逐渐积累或后台重连可能让一次普通调用最终跨过超时边界。trace 顶部看到等待并不一定是最初原因,需要对齐之前的锁持有、跨进程调用和 I/O 时间线。

测试应在阶段边界采集轻量健康信号,ANR 发生后保存 trace、ApplicationExitInfo、进程与线程、最近业务动作和候选身份。不要为了持续运行而自动关闭无响应对话框后继续计为通过;这会丢失首次故障现场,并让后续状态不可解释。

异常没有在本地矩阵复现时,可以参考 Android vitals 的线上 ANR、崩溃、启动和设备分布信号,但要遵守其安装来源、用户同意和统计口径限制。线上聚合用于发现设备族与版本线索,不能替代同候选的受控复现,也不能单凭趋势宣称加固因果。

设备矩阵、时长和发布结论必须诚实

Firebase Test Lab Android matrices 说明矩阵由型号、OS、方向和 locale 等维度组成,任一执行失败都会影响矩阵结果。长稳任务还受到云会话时长、设备容量和基础设施稳定性约束。适合云端的短状态循环可以扩展覆盖,超出会话能力的持续任务应放在专用代表设备。

Android instrumented tests 适合驱动真实运行时、组件和系统 API 路径,但单一设备通过不能代表全部 API、ABI 与厂商组合。矩阵选择应依据最低支持范围、真实用户分布和已知风险,不按可用设备随意抽样。每个执行保留实际型号、OS、方向、locale、时长与基础设施状态。

准备长稳与后台切换评估时,应提交候选摘要、状态机计划、业务断言、设备矩阵、原始事件、WorkManager 回执、ApplicationExitInfo、ANR 与 Native 诊断以及未覆盖窗口。申请入口由御盾中央平台统一承接。没有真实执行和 HTTP 无关,本地计划或脚本通过不能写成长稳兼容、无异常或性能改善。

执行环境与适用任务
环境适合主要限制回执重点
本地开发设备快速定位和短循环代表性有限工具与设备状态
专用代表设备长时间持续运行数量有限完整时间线
云真机矩阵型号和系统扩展会话时长与容量实际矩阵结果
instrumented test可重复组件路径用例外路径缺失断言与失败现场
线上 vitals真实用户趋势样本与口径限制版本和设备分布
人工探索发现未知交互重复性较弱转化为固定用例

事实依据与适用边界

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

本文判断事实或工程依据适用限制
ApplicationExitInfo 可以提供进程退出原因和相关诊断信息。Android ApplicationExitInfo 说明退出原因、ANR trace 与新版本 Native tombstone 能力。退出记录必须与同一候选、进程、时间窗和用户路径关联。
Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号。Android vitals 说明 Play 侧质量指标及其观察范围。样本受安装来源、用户同意和统计口径限制,不能代替发布前长稳回归。
ANR 定位需要区分主线程、锁、Binder、I/O 和组件超时。Diagnose Android ANRs 给出 ANR 类型与排查方法。trace 表象堆栈不一定是最初阻塞源,需要结合阶段时间线。
WorkManager 持久任务会受约束、重试与系统配额影响。Android persistent work 说明持久任务的保存、约束和执行语义。请求入队不证明任务按时完成,也不证明业务操作具备幂等性。
云测试矩阵由设备型号、OS、方向和 locale 等维度构成。Firebase Test Lab Android matrices 说明 Android 矩阵配置与结果关系。云矩阵仍需依据真实用户和最低支持范围选择,并受时长与容量限制。
依赖真实 Android 运行时和组件的路径应通过设备端测试。Android instrumented tests 说明测试可在真实或模拟 Android 环境执行。单一设备、ABI 和 API 版本通过不能代表完整支持矩阵。
长稳验收必须给每个阶段设置业务断言和独立失败结果。工程判断:进程存活与总时长不能发现静默数据错误和局部阶段失败。阶段通过只覆盖已执行动作、设备与时间窗口,不能扩张到未知路径。
云真机时长不足的用例应转移到可持续的代表设备。工程判断:测试在阶段完成前被基础设施终止,无法形成完整长稳回执。专用设备提升持续时间,不自动提升型号覆盖或证明线上稳定。

工程常见问题

App 连续运行很久没有崩溃,能否算长稳通过?

不能只凭总时长。需要状态机覆盖真实动作、前后台、任务和退出观察,并在每个阶段保存业务断言与诊断证据。

前后台切换测试只按 Home 再打开是否足够?

不足。应定义离开方式、后台条件、返回入口、进程身份、会话、Native 资源、网络和业务结果,并覆盖重复切换。

WorkManager 入队成功是否代表后台任务正常?

不代表。还要观察约束、attempt、开始、结果、重试、配额和业务副作用,并验证重复执行的幂等性。

系统回收进程是否应直接判定失败?

取决于阶段契约。先用 ApplicationExitInfo 确认退出类型,再检查重新进入后的业务断言;异常退出和静默损坏仍应失败。

云真机矩阵能否替代专用长稳设备?

通常不能完全替代。云矩阵适合扩展短场景覆盖,长约束窗口和持续运行需考虑会话时长、容量与基础设施中断。

申请加固后长稳评估需要准备什么?

准备候选摘要、状态机、业务断言、设备矩阵、原始事件、持久任务回执、退出记录、ANR 与 Native 诊断和缺口,再通过御盾中央平台提交。

想用自己的 App 验证?

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

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