先看结论与判断条件

  • 测试拓扑必须来自最终合并 Manifest,源码片段和模块清单无法代表打包后的组件进程归属。
  • 每个 Linux 进程都有独立 Application 实例、堆和静态字段,初始化幂等必须按进程核对,不能沿用单进程假设。
  • ContentProvider、Service、Receiver 和 Activity 的首个触发路径不同,应为每个声明进程设计可重复的唤醒步骤。
  • 启动测量要区分进程创建、组件可用、TTID 与 TTFD,并记录冷暖状态,不能用一次主界面耗时代表远程进程。
  • 退出分析要把 ApplicationExitInfo、ANR trace、Native tombstone、进程名、时间窗和候选摘要关联,避免错认责任进程。
  • 加固前后比较必须使用相同包、进程拓扑、设备条件和用例;发现差异时保留限制,不虚构加固因果或兼容结论。

最终 Manifest 才是进程拓扑的起点

Android Manifest 定义应用组件、权限、intent filter、SDK 约束和应用元数据,组件的 android:process 决定它是否进入主进程、包内私有远程进程或显式命名进程。多模块工程经过 manifest merger 后,库清单可以新增 Provider、Service 或 Receiver,最终拓扑往往比主模块源码中看到的更复杂。

回归前应从发布候选 APK 中提取最终 Manifest,并列出 application 默认进程与每个组件的有效进程名。没有单独声明 process 的组件继承 application 设置;以冒号开头的名称属于应用私有进程,需要与 package 组合成完整名字。显式全名还可能由 SDK 引入,不能用固定后缀名单过滤。

静态拓扑只说明系统可以把组件安排到哪些进程,不证明这些组件在真实路径中被创建。测试矩阵必须为每个进程找到至少一个合法触发入口,并标记所需前置条件、权限、账号、网络和系统状态。无法公开或安全触发的组件应列为阻塞项,不能默认为已覆盖。

Manifest 进程声明的解析规则
声明位置示例形态有效进程测试含义
application 未声明缺省应用包名建立主进程基线
application 声明冒号后缀包名加私有后缀无覆盖组件继承
组件未声明缺省继承 application按继承结果分组
组件私有进程冒号后缀包名加组件后缀单独触发和取证
组件全名进程完整名称声明值本身核对共享边界
合并后新增组件库清单来源以最终结果为准纳入回归矩阵

先固定候选身份和合并结果

同一个版本号可能对应不同加固配置、渠道包和 manifest 合并结果。回归回执应绑定 APK 摘要、versionCode、applicationId、构建变体、签名身份和提取出的 Manifest 摘要。若候选重建或重新加固后摘要改变,旧进程矩阵只能作为规划参考,不能直接证明新包通过。

需要同时保存加固前后的最终 Manifest,并对 package、application process、组件名称、component type、process、exported、enabled、permission 和 directBootAware 做结构化差异。预期之外的组件消失、进程名变化或权限变化应先阻止测试,因为后续所谓没有启动可能只是组件已被错误移除或入口被改变。

manifest 一致也不代表运行语义一致。加固可能改变类加载、Native 库装载、反射可达性和初始化顺序;这些变化不会全部反映在清单中。静态差异检查负责确认测试对象,设备回归负责观察实际创建、调用和退出,两类证据要分别保留。

候选与拓扑回执的绑定字段
字段来源用途变化后处理
APK SHA-256最终候选锁定测试字节重建后全部回执失效
Manifest SHA-256候选内清单锁定组件拓扑重新生成矩阵
applicationId最终清单解析私有进程名不得沿用旧进程名
versionCode最终清单关联发布版本与摘要共同记录
签名身份候选证书区分渠道和权限关系签名变化重验
构建与加固配置摘要受控流水线解释候选差异变更进入新批次

每个进程都要单独核对初始化顺序

多进程应用不是一份 Application 对象服务所有组件。系统创建新的应用进程时,会在该进程建立独立运行时、类静态状态和 Application 实例;声明在远程进程的 Provider 或 Service 可能在主界面出现之前初始化。若全局初始化代码默认只运行一次,实际会在每个相关进程分别执行。

测试应给初始化事件设计公开安全的结构化日志,至少包含候选版本、完整进程名、PID、组件名、阶段、单调时间和结果。敏感配置、令牌和业务数据不能写入日志。通过同一进程内事件序列可以确认 Application.attachBaseContext、Application.onCreate、Provider.onCreate 和目标组件回调是否出现重复、缺失或顺序异常。

初始化幂等不能只靠布尔静态字段。每个进程都有自己的静态变量,且组件并发进入时仍可能发生竞争。数据库迁移、文件初始化、Native 库准备和跨进程单例要明确进程所有者、锁粒度、失败重试与超时。回归应同时触发两个进程,观察共享资源是否被重复创建或长期占锁。

逐进程初始化检查点
检查点主进程远程进程失败信号
Application 创建冷启动触发首个组件触发缺失或重复循环
Provider 初始化可能早于首页可成为进程首入口主线程阻塞或异常
Native 库装载按主路径需要远程路径需独立核对UnsatisfiedLinkError
配置读取使用本进程缓存不得假设静态共享默认值分叉
共享文件或数据库并发参与者并发参与者锁等待或重复迁移
失败重试有界且可观测有界且可观测启动风暴或永久卡死

组件触发路径要覆盖真实生命周期

Activity 通常由界面导航触发,Service 可能由显式启动、绑定或系统调度唤醒,Receiver 依赖广播条件,Provider 则可能在首次内容访问甚至应用启动早期创建。只执行启动主 Activity,会漏掉没有被主路径调用的远程服务和 Provider,无法说明其加固后类加载与初始化正常。

每个组件用例要定义触发前状态、合法动作、预期进程、首个可观察回调、完成条件和清理步骤。绑定 Service 应核对 binder 连接、断开与重绑;Provider 应核对最小只读查询或公开测试接口;Receiver 应使用允许的测试广播路径。不能为了测试扩大 exported 或移除权限,因为那会改变待验证产品。

退出行为同样按组件设计。停止 Service、解除绑定、取消任务、进程被系统回收和进程崩溃不是同一种事件。回归目标是确认每种合法退出后的回调、资源释放和其他进程影响,而不是通过强制终止制造单一结果。任何破坏性或越权操作都不应进入公开测试脚本。

组件触发与完成判据
组件合法触发完成证据常见遗漏
Activity显式导航或测试启动首帧与业务可交互只看进程存在
Servicestart 或 bind目标回调与服务结果未测重绑和停止
Provider最小查询或初始化入口onCreate 与查询结果只测主进程 Provider
Receiver允许的系统或测试事件onReceive 完成后台限制未记录
Job 组件受控调度条件任务开始与结束设备状态不一致
独立进程入口组件对应动作完整进程名与 PID仅凭包名判断

共享状态要按 IPC 和存储边界验证

不同进程不共享 Java 或 Kotlin 静态字段,也不共享普通内存缓存。一个进程更新登录态、开关或初始化标记后,另一个进程若只读取启动时缓存,可能长期看到旧值。测试应列出真正跨进程共享的状态及传递机制,例如 Binder、Provider、数据库、文件或系统服务,并核对更新可见性和失败语义。

共享数据库和文件带来锁、事务与刷新问题。回归应让两个进程按业务允许顺序读写,记录事务边界、等待时间、错误码和最终一致性;遇到超时不能简单归因于加固。需要先区分应用自己的锁设计、文件系统 I/O、数据库竞争和 Binder 回调阻塞,再比较同条件的加固前候选。

IPC 接口还需要核对调用线程和进程归属。Binder 回调可能落在 binder 线程池,组件又可能把工作切回主线程;若加固后初始化变慢,线程等待会被放大。日志应同时记录发起进程、处理进程、线程名、请求标识和完成状态,但不记录敏感负载。

用脚本从 Manifest 生成进程检查矩阵

下面的 Python 脚本读取公开或经脱敏的最终 AndroidManifest.xml,解析 application 与 activity、activity-alias、service、receiver、provider 的 process 声明,按有效进程名分组输出 JSON。私有冒号进程会与 package 合成完整名称,组件未声明时继承 application 默认值。

脚本对输入文件、XML、package、application 节点、组件 name 和空 process 做失败检查;它不会安装应用、调用组件或读取设备隐私。输出中的 checks 是测试计划字段,提醒每个进程都要核对初始化、共享状态、主线程边界和退出归属,并不声称这些检查已经执行。

真实二进制 Manifest 通常需要由受控工具先解码为 XML,再交给脚本。解码工具版本、原 APK 摘要和 XML 摘要应写入回执。生成矩阵后,测试负责人仍需为每个组件补充合法触发方式与完成条件;自动分组无法推断业务入口、权限前置和系统调度条件。

按进程分组生成 Android 组件回归矩阵
import json
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from pathlib import Path

if len(sys.argv) != 2:
    raise SystemExit('usage: manifest_process_matrix.py AndroidManifest.xml')
manifest_path = Path(sys.argv[1])
if not manifest_path.is_file():
    raise SystemExit('manifest file is missing')
try:
    root = ET.parse(manifest_path).getroot()
except ET.ParseError as exc:
    raise SystemExit(f'invalid manifest XML: {exc}')
package_name = root.get('package', '').strip()
if not package_name:
    raise SystemExit('manifest package is missing')
android_ns = '{http://schemas.android.com/apk/res/android}'
application = root.find('application')
if application is None:
    raise SystemExit('application element is missing')

def process_name(value):
    selected = (value or package_name).strip()
    if not selected:
        raise SystemExit('component process is empty')
    return package_name + selected if selected.startswith(':') else selected

def component_name(element):
    value = (element.get(android_ns + 'name') or '').strip()
    if not value:
        raise SystemExit('component name is missing')
    return value

default_process = process_name(application.get(android_ns + 'process'))
matrix = defaultdict(list)
for kind in ('activity', 'activity-alias', 'service', 'receiver', 'provider'):
    for element in application.findall(kind):
        effective = process_name(element.get(android_ns + 'process') or default_process)
        matrix[effective].append({'type': kind, 'component': component_name(element)})
if not matrix:
    raise SystemExit('no Android components found')
checks = ['initialization', 'shared-state', 'main-thread', 'exit-reason']
output = [{'process': name, 'components': items, 'checks': checks} for name, items in sorted(matrix.items())]
print(json.dumps(output, ensure_ascii=False, indent=2))

启动耗时要区分进程创建、TTID 和 TTFD

Android 启动文档区分 TTID 与 TTFD,并要求说明启动状态和测量条件。多进程场景还应记录目标进程何时创建、Application 何时完成、组件何时可用。主 Activity 的 TTID 只覆盖界面首帧,不能代表稍后被唤醒的远程进程已经初始化完成。

性能比较需要同设备、同系统、同安装状态、同触发步骤和同候选类别。冷启动、温启动、热启动以及远程进程已存在与否会产生不同路径。单次数字只能作为样本,不能据此声称加固造成提升或回退;没有重复测量和分布统计时,应记录观测值与限制。

组件初始化应设置业务上可解释的超时,而不是为了通过测试无限等待。超时发生时保留主线程、Binder、I/O、锁和 Native 装载的时间线,再比较基线。若只在某个进程出现,应把进程名与组件入口作为排查主键,避免把主进程总体正常掩盖局部阻塞。

多进程启动时间点的职责
时间点适用对象说明不能替代
进程创建每个目标进程系统开始建立运行环境组件可用
Application 完成每个进程实例应用级初始化结束Provider 或 Service 完成
TTID可见 Activity首帧绘制时间业务数据完全展示
TTFD声明 fully drawn 的界面完整展示时间远程组件健康
Service 可用远程或主进程服务调用入口已响应长期稳定性
Provider 可查询内容提供者最小查询完成全部数据路径正确

退出记录必须关联进程名、时间窗和用户路径

ApplicationExitInfo 可以提供进程退出原因、时间等信息,部分版本还能关联 ANR trace 和 Native tombstone。采集时应查询与测试包、用户、时间窗和候选版本对应的记录,再按 processName、pid、reason、importance 和 timestamp 归类。只看到同包历史退出记录,不能说明它来自当前用例。

正常停止、系统资源回收、Java 崩溃、Native 崩溃、ANR 和用户请求退出需要不同判读。测试步骤要记录开始与结束时间、触发组件、预期进程和预期退出类型;出现额外进程退出时保留原始 reason 与描述,不先把它归为加固崩溃。无退出记录也不等于进程仍健康,应结合进程存在、组件结果和日志确认。

Native tombstone 和 ANR trace 必须与同构建符号、ABI 和文件摘要匹配。退出回执负责回答哪个进程在什么时间以何种系统原因结束,根因分析还要结合线程、锁、Binder、I/O、类加载和 Native 初始化。若证据不足,应写无法归因,而不是根据最后一行日志猜测。

ANR、平台变化和设备矩阵共同决定验收范围

Android ANR 诊断需要区分主线程阻塞、锁竞争、Binder 调用、I/O 和组件超时。多进程应用可能只有远程 Service 进程发生 ANR,主界面仍可短暂操作。回归应分别采集每个目标进程的主线程状态与跨进程调用链,trace 顶层表象不一定是最初阻塞源。

Android 16 的部分行为变化不依赖 targetSdk,旧目标版本也需要运行回归。测试矩阵不能只根据 targetSdk 推断是否受影响,应在实际支持系统上执行组件触发、后台限制、进程退出与通知等相关路径。平台变化提供排查维度,不代表所有加固应用都会出现同一问题。

发布前应汇总最终 Manifest 拓扑、逐进程触发回执、初始化事件、共享状态检查、启动条件、ApplicationExitInfo、ANR 与 Native 诊断、设备和 API 覆盖。申请评估由御盾中央平台统一承接。缺少同一候选与真实设备证据时,只能标记未验证,不能声称多进程兼容、性能改善或异常已被阻断。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
Manifest 定义 Android 组件、权限、intent filter、SDK 约束和应用元数据。Android app manifest 说明 Manifest 的结构与声明职责。静态清单不证明组件已实际创建,也不证明运行时访问控制和业务行为正确。
ApplicationExitInfo 可以提供进程退出原因和相关诊断信息。Android ApplicationExitInfo 说明退出原因、ANR trace 与新版本 Native tombstone 能力。退出记录仍需与同一候选、进程名、用户、时间窗和触发路径关联。
应用启动测量应区分 TTID 与 TTFD,并记录启动状态和条件。Android app startup time 定义启动状态、TTID 与 TTFD 的测量语义。一次主界面启动耗时不能代表远程进程,也不能证明加固造成因果变化。
ANR 排查需要区分主线程、锁、Binder、I/O 和组件超时。Diagnose Android ANRs 给出 ANR 类型和定位方法。trace 表象堆栈不一定是最初阻塞源,仍需结合跨进程时间线。
依赖真实 Android 运行时和组件的语义应通过设备端测试验证。Android instrumented tests 说明 instrumented test 在真实或模拟 Android 环境中运行。单一设备、ABI 或 API 版本通过不能代表完整支持矩阵。
Android 16 的部分行为变化不依赖 targetSdk。Android 16 all-app behavior changes 列出适用于所有应用的行为变化。平台变化不意味着每个多进程或加固应用都会触发同一故障。
多进程回归必须为最终 Manifest 中每个有效进程安排独立触发路径。工程判断:未实际创建的进程无法提供初始化、状态和退出证据。触发成功只覆盖该入口和当次条件,不能扩张到未执行组件。
加固前后的比较应绑定候选摘要、进程拓扑、设备条件和相同用例。工程判断:测试对象或条件变化会破坏差异归因。条件一致仍不自动证明观察差异由加固单独造成,需要进一步根因证据。

工程常见问题

主进程能正常进入首页,是否代表多进程加固通过?

不代表。远程 Service、Provider、Receiver 或独立 Activity 可能尚未创建,必须分别触发并核对初始化、IPC、主线程和退出结果。

为什么不能只读取主模块的 AndroidManifest.xml?

库和动态模块会通过 manifest merger 增加或覆盖组件属性。测试拓扑必须来自最终候选中的合并清单,并绑定该候选摘要。

不同进程能否共享 Kotlin object 或静态布尔值?

不能把它们当作跨进程共享状态。每个进程有独立运行时和静态字段,跨进程状态应通过明确的 IPC 或存储机制协调。

ApplicationExitInfo 中出现崩溃就能归因于加固吗?

不能。先绑定候选、进程名、时间窗和触发路径,再结合 trace、tombstone、线程与加固前基线定位,证据不足时应标记无法归因。

只测 targetSdk 对应的平台版本够不够?

不够。部分平台行为变化适用于所有应用,应在实际支持的 API、ABI 和代表设备上运行同一进程矩阵并记录覆盖缺口。

申请多进程加固回归评估要准备什么?

准备候选摘要、最终 Manifest、进程矩阵、组件触发步骤、初始化日志、IPC 与共享状态检查、退出记录、诊断材料和设备覆盖,再通过御盾中央平台提交。

想用自己的 App 验证?

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

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