先看结论与判断条件

  • 内存验收必须绑定候选摘要、设备型号、系统版本、ABI、场景步骤、采样周期和冷暖状态,单个峰值没有独立结论。
  • PSS、RSS、Java heap、Native heap 和图形分配回答不同问题,应保留同一时间线,不能用其中一个数替代整体压力。
  • 冷启动、资源密集核心路径、前后台切换和长时运行要分开执行,避免不同生命周期的峰值被混在一个平均值中。
  • OOM 判断要结合 ApplicationExitInfo、Java 异常、Native tombstone、系统日志与测试时间窗,进程消失本身不是充分归因。
  • 加固前后比较必须保持设备、数据集、操作序列和采样工具一致,并用多次运行观察分布,而不是挑选一次最好或最差结果。
  • 线上 Android vitals 适合补充真实用户信号,但受安装来源、用户同意和统计口径限制,不能代替发布前设备回归。

先把验收问题改写成可复核条件

App 加固可能改变类加载、代码布局、Native 库装载和初始化时序,这些变化可能影响不同内存区域,但不能从机制描述直接推断候选一定增加或减少内存。验收问题应写成:在同一设备、相同数据、相同步骤和相同采样条件下,加固候选是否出现可重复的峰值、稳定区间或异常退出差异。

测试对象首先绑定 APK 摘要、versionCode、applicationId、ABI、签名和加固配置摘要。基线包也要保留相同字段,并确认除待比较处理外没有混入业务代码、资源、依赖或编译参数变化。若两个包不是可辩护的对照,结果只能作为各自观测,不能归因为加固。

通过标准不能只写不 OOM。应为每个场景定义成功动作、采样起止点、需要观察的内存分区、允许的退出类型、证据缺口和复跑条件。没有预先定义门槛时,测试人员容易在结果出来后临时选择指标,既无法复现,也无法解释为什么同一峰值在不同批次得到不同判断。

内存回归回执必须固定的测试条件
条件最低记录变化影响处理
候选身份APK 摘要与配置摘要测试对象改变新建批次
设备型号、系统、RAM 与 ABI压力和回收策略变化禁止直接合并
应用状态新装、冷启或已有缓存分配与加载路径变化分组统计
业务数据脱敏数据集版本对象数量和资源体积变化固定输入
操作步骤动作、次数和节奏峰值窗口变化脚本化或记录
采样工具版本、命令和周期口径与扰动变化重新建立基线

四类场景必须拆开测量

冷启动场景观察进程创建、Application 和 Provider 初始化、首个 Activity、首帧以及核心资源装载。测试前要明确是否强制停止、是否清理数据、页面缓存是否存在和目标进程是否残留。启动后的瞬时峰值可能很高,却很快回落;因此既要记录峰值,也要记录可交互后稳定窗口。

核心路径选择真实内存压力最大的业务动作,例如大图解码、复杂列表、地图、音视频或大量 Native 计算,但必须使用公开安全且固定的数据集。每个动作设置开始、阶段完成和资源释放标记,以便把峰值关联到具体操作。只在首页空闲采样,无法覆盖用户真正会触发的分配。

前后台切换与长时运行回答另外两个问题。切后台可能触发生命周期释放、缓存收缩或系统回收;反复恢复可能暴露监听器、线程和 Native 对象泄漏。长时场景则观察稳定区间是否持续上移。两者都要规定循环动作和停止条件,不能无限运行直到失败再把时间差当作结论。

内存压力场景与主要观测
场景起点主要观测完成条件
冷启动进程不存在初始化峰值与稳定回落核心界面可交互
核心路径固定业务状态阶段峰值和释放动作结果完成
前后台切换前台稳定缓存收缩与恢复增量规定循环结束
长时运行路径预热完成稳定区间趋势达到预定时长或门禁
低内存干预受控设备状态回调和进程退出状态与记录齐全
异常路径可重复错误输入分配失败和资源释放错误语义明确

PSS、RSS 与各堆必须同时解释

进程总内存不是单一数字。RSS 统计驻留页但共享页会在多个进程重复出现,PSS 按比例分摊共享页,更适合做进程间汇总;Java heap 反映托管对象,Native heap 反映本地分配,图形缓冲和映射文件又可能不完全进入前两者。报告应保留指标名称与单位,避免把不同口径拼成一条曲线。

峰值用于发现短时容量压力,稳定区间用于观察路径结束后保留多少,增长斜率用于判断重复动作是否持续积累。单次垃圾回收、页面换入或后台系统活动会造成抖动,所以同一场景要重复运行,并保存原始时间序列。只有汇总表而没有原始样本,无法判断峰值是真实路径事件还是采样噪声。

多进程应用还要同时记录主进程和远程进程。只看主进程可能把分配转移误判成优化,只把各进程 RSS 相加又会重复计算共享页。工程判断应优先比较逐进程 PSS 与全应用汇总 PSS,同时展示 Java、Native 和图形分区,任何不可采集区域都要标记为空缺而不是填零。

常见内存指标的用途和限制
指标适合回答主要限制报告方式
PSS共享页分摊后的进程压力仍是采样时点逐进程及全应用
RSS实际驻留页规模跨进程相加会重复共享页单进程辅助
Java heap托管对象分配与回收不覆盖 Native 和图形与 GC 事件对齐
Native heap本地 allocator 压力不覆盖全部 mmap与 SO 路径对齐
Graphics纹理和缓冲相关压力设备与渲染后端差异单列并说明工具
系统可用内存设备整体压力背景受其他进程影响作为环境证据

采样设计要能区分峰值、稳定值和泄漏嫌疑

每条采样至少包含候选标识、run_id、scenario、process、timestamp、PSS、RSS、Java、Native、图形和阶段标记。时间戳应使用同一时钟域,阶段标记由测试动作产生。若工具返回未知或不支持,应保存 null 和原因,不能把缺失字段写为零,因为零会被误读成真实观测。

峰值比较要使用相同采样周期。周期太长可能错过瞬时峰,太短又可能放大测量扰动;选择依据应在测试计划中固定。稳定窗口应从业务动作明确完成后开始,并排除已知初始化阶段。长时趋势可以比较固定窗口的中位数,但不能用任意两点连线声称泄漏。

内存增长只是泄漏嫌疑,不是根因证明。缓存设计、图片池、JIT、文件映射和系统页面回收都可能让曲线在一段时间内上升。发现稳定窗口持续抬高后,再使用 heap dump、allocation profile、Native 分配跟踪或代码审查定位,同时保留工具带来的行为扰动。

原始采样记录的字段约束
字段类型必需性校验
run_id字符串必需同一次执行唯一
scenario字符串必需来自批准场景
timestamp_ms整数必需同 run 单调递增
pss_kb非负数必需缺失则整组阻塞
native_heap_kb非负数或 null建议不支持时写缺口
exit_reason字符串或 null条件必需进程结束时关联

OOM 结论必须落到退出原因和时间窗

进程从采样列表中消失,可能是正常退出、用户操作、系统资源回收、崩溃、ANR 或测试基础设施中断。ApplicationExitInfo 能提供退出原因与相关诊断信息,部分系统版本还可返回 ANR trace 或 Native tombstone。采集时必须按包、进程、用户、候选和测试时间窗筛选。

Java OutOfMemoryError、Native 分配失败和系统低内存杀进程的证据形态不同。报告应保留异常类型、退出 reason、进程名、重要性、时间、最后场景阶段和可用诊断附件。没有退出记录时不能自动写无 OOM,因为记录可能受系统版本、保留数量、权限或采集时机影响。

同一退出记录也不能独立证明加固是根因。需要在同设备同路径基线中比较,并排除业务数据变化、其他进程压力、平台差异和测试工具故障。若只在加固候选稳定复现,可以升级为回归嫌疑;直到定位具体分配或生命周期差异前,仍应避免写确定因果。

进程结束信号的判读
信号可能含义所需补充可下结论
Java OutOfMemoryError托管或相关分配失败堆栈与同构建映射该路径出现异常
Native tombstoneNative 崩溃符号、ABI 与 build ID该进程发生 Native 退出
系统低内存 reason资源压力下被回收importance 与设备状态系统记录的退出类型
ANR trace进程未及时响应主线程与锁时间线发生 ANR 记录
进程列表消失多种可能ApplicationExitInfo 与日志不能单独归因
采样中断工具或设备异常基础设施日志本次证据不完整

用聚合脚本保留原始样本和证据缺口

下面的 Python 脚本读取脱敏 JSON Lines 采样,每行包含 run_id、scenario、timestamp_ms、pss_kb 和可选 exit_reason。它按运行和场景分组,要求时间戳递增、PSS 非负、每组至少三个样本,输出峰值、全程中位数、末段中位数、退出原因和缺口。脚本只聚合真实输入,不生成阈值或通过结论。

末段中位数取每组最后一部分样本,用来提供稳定区间线索,不等于泄漏判断。正式分析应先按测试动作确定稳定窗口,而不是机械使用尾部样本;示例保留此简化,是为了展示如何把假设明确写入代码和输出。任何样本过少或时间倒序都会失败,避免残缺数据被悄悄汇总。

输入不得包含用户标识、业务内容、令牌或内部地址。run_id 应为随机测试标识,scenario 使用公开分类,进程详细信息可以在受控回执中另行关联。输出要与原始文件摘要、聚合脚本版本和执行时间绑定,防止后续换样本却继续引用旧报告。

聚合内存场景峰值、稳定区间和退出证据
import json
import statistics
import sys
from collections import defaultdict
from pathlib import Path

if len(sys.argv) != 2:
    raise SystemExit('usage: aggregate_memory_samples.py samples.jsonl')
input_path = Path(sys.argv[1])
if not input_path.is_file():
    raise SystemExit('sample file is missing')
groups = defaultdict(list)
for line_number, line in enumerate(input_path.read_text(encoding='utf-8').splitlines(), 1):
    if not line.strip():
        continue
    try:
        row = json.loads(line)
    except json.JSONDecodeError as exc:
        raise SystemExit(f'invalid JSON at line {line_number}: {exc}')
    for field in ('run_id', 'scenario', 'timestamp_ms', 'pss_kb'):
        if field not in row:
            raise SystemExit(f'missing {field} at line {line_number}')
    if not isinstance(row['timestamp_ms'], int) or row['timestamp_ms'] < 0:
        raise SystemExit(f'invalid timestamp at line {line_number}')
    if not isinstance(row['pss_kb'], (int, float)) or row['pss_kb'] < 0:
        raise SystemExit(f'invalid PSS at line {line_number}')
    groups[(str(row['run_id']), str(row['scenario']))].append(row)
if not groups:
    raise SystemExit('no samples found')
results = []
for (run_id, scenario), rows in sorted(groups.items()):
    rows.sort(key=lambda item: item['timestamp_ms'])
    if len(rows) < 3:
        raise SystemExit(f'too few samples for {run_id}/{scenario}')
    timestamps = [item['timestamp_ms'] for item in rows]
    if len(set(timestamps)) != len(timestamps):
        raise SystemExit(f'duplicate timestamps for {run_id}/{scenario}')
    values = [float(item['pss_kb']) for item in rows]
    tail = values[-max(3, len(values) // 3):]
    exits = sorted({str(item['exit_reason']) for item in rows if item.get('exit_reason')})
    results.append({'run_id': run_id, 'scenario': scenario, 'samples': len(rows), 'peak_pss_kb': max(values), 'median_pss_kb': statistics.median(values), 'tail_median_pss_kb': statistics.median(tail), 'exit_reasons': exits, 'gaps': [] if exits else ['exit-reason-not-observed']})
print(json.dumps(results, ensure_ascii=False, indent=2))

Native 内存问题要绑定符号和实际构建

加固后若出现 Native 崩溃、分配失败或异常增长,调试材料必须与候选中实际 SO 的 ABI、build ID 和文件摘要一致。Debug Android native code 说明 Native 调试与 tombstone 还原依赖正确符号和构建产物。拿未加固库或另一个 ABI 的符号解析地址,可能给出看似合理但错误的函数位置。

符号文件用于定位,不应随公开报告披露私有实现。受控证据包可以保存 tombstone、未剥离符号摘要、映射文件、加固前后 SO 摘要和解析工具版本。若加固改变代码布局,必须使用与输出候选匹配的符号映射链,不能直接继承输入 SO 的地址关系。

Native heap 只是本地分配的一部分,mmap、共享库页面、图形缓冲和驱动资源可能另有统计入口。调查时要把分配栈与场景阶段对齐,并确认释放是否延迟到生命周期结束。单个 Native 数字上升不能证明泄漏,更不能证明攻击阻断或防护强度。

设备实验室与线上 vitals 互为补充

Android instrumented tests 可以在真实运行时驱动固定业务步骤,适合把场景、阶段标记和断言自动化。设备实验室能扩展型号与系统覆盖,但设备目录、稳定性和容量会变化,计划必须记录实际分配到的型号、OS、ABI 和执行时间。平台显示可用不代表它覆盖目标用户的全部厂商特性。

Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号,可用于观察发布后的真实环境趋势。其样本受安装来源、用户同意和统计口径限制,也无法自动构造严格的加固前后对照。因此线上变化应作为调查线索,并与版本、设备和退出记录关联。

发布前实验回答受控路径能否在已测矩阵运行,线上 vitals 回答部分真实用户是否出现质量信号。两边都不能扩张到未覆盖设备。若实验室通过但线上某设备族异常,应补充代表设备和场景;若线上暂无线索,也不能删掉发布前压力测试。

本地、设备实验室与线上信号的分工
证据层优势限制使用方式
本地代表设备可控和可深度诊断型号覆盖少建立可重复基线
设备实验室扩展型号与系统容量和设备目录变化记录实际设备回执
instrumented test步骤和断言自动化用例外路径不覆盖固定场景重复运行
ApplicationExitInfo系统退出原因受时间窗和保留限制绑定候选和进程
Android vitals真实用户质量信号样本与统计口径限制观察发布趋势
Native 诊断定位本地崩溃与分配依赖匹配符号受控根因分析

发布门禁要保留原始数据和结论边界

NIST SSDF 强调保留来源、构建、验证和变更证据。内存门禁应保存候选与基线身份、场景定义、设备回执、原始采样摘要、聚合脚本、退出记录、诊断附件和审批结论。汇总数字必须能回溯到原始时间序列,修订阈值或排除样本也要记录原因。

发现回归嫌疑时先判断是否可重复,再按 Java、Native、图形、映射、进程拆分和生命周期定位。一个场景阻塞不应被其他场景平均掉;同样,某个设备失败也不能直接外推全部设备。发布决定应列出通过、失败、阻塞和未覆盖项,而不是只给一个绿色总状态。

准备内存与 OOM 评估时,应提交候选和基线摘要、加固配置差异、场景脚本、设备与系统、原始采样、退出记录、Native 符号身份和证据缺口。申请入口由御盾中央平台统一承接。没有同候选的真实数据时,不写无 OOM、内存已优化、兼容通过或性能收益。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
ApplicationExitInfo 能提供进程退出原因和相关诊断信息。Android ApplicationExitInfo 说明退出原因、ANR trace 与新版本 Native tombstone 能力。退出记录必须与同一候选、进程、用户路径和测试时间窗关联。
Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号。Android vitals 说明 Play 侧质量指标及其观察入口。线上样本受安装来源、用户同意和统计口径限制,不能替代受控内存回归。
依赖真实 Android 运行时的内存场景应通过设备端测试执行。Android instrumented tests 说明测试可在真实或模拟 Android 环境中运行。单一设备、ABI 和 API 版本通过不能代表完整支持矩阵。
设备目录、稳定性和容量会变化,计划应记录实际型号与系统回执。Test Lab available devices 说明 Android 测试设备目录和可用性信息。设备可用不代表覆盖目标业务全部厂商特性和资源压力。
Native 崩溃与 tombstone 还原依赖匹配的符号、架构和构建产物。Debug Android native code 说明 Native 调试和符号材料的使用关系。调试材料可用不等于内存回归已定位或生产兼容通过。
安全发布需要保留来源、构建、验证和变更证据。NIST SP 800-218 SSDF 将安全开发和供应链证据纳入组织实践。组织级框架不定义单个候选的内存阈值、加固能力和验收结果。
内存峰值必须与稳定区间、场景阶段和退出原因共同判读。工程判断:单个时点无法区分瞬时初始化、缓存保留、持续增长和采样噪声。组合观测仍是回归证据,不自动证明具体分配点或加固因果。
加固前后比较必须保持设备、数据、步骤和采样口径一致。工程判断:比较条件变化会破坏差异解释,产生不可复核的归因。条件一致只能提高可比性,仍需重复运行和根因证据。

工程常见问题

只看一次 PSS 峰值能判断加固引入内存回归吗?

不能。应在同设备同路径重复采样,同时比较峰值、稳定窗口、各内存分区和退出原因,并保留原始时间序列。

进程突然消失是否就是 OOM?

不是。需要结合 ApplicationExitInfo、Java 异常、Native tombstone、系统日志、时间窗和测试基础设施状态区分退出类型。

Java heap 没有增长,为什么总内存仍可能上升?

Native heap、mmap、共享库页面、图形缓冲和其他分配不一定体现在 Java heap。应同时观察 PSS、Native、图形和进程拆分。

内存曲线持续上升就能确定存在泄漏吗?

不能直接确定。缓存、JIT、文件映射和延迟释放都可能造成阶段性增长;先固定稳定窗口重复复现,再用堆和分配工具定位。

设备实验室全部通过是否代表所有用户设备安全?

不代表。目录和容量变化,代表设备也不能覆盖全部厂商特性;应记录实际矩阵,并用线上质量信号补充未知环境。

申请加固后内存与 OOM 评估要准备什么?

准备候选和基线摘要、加固差异、场景脚本、设备回执、原始采样、退出记录、Native 符号身份和缺口,再通过御盾中央平台提交。

想用自己的 App 验证?

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

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