Privacy Sandbox 退场与广告 SDK 启动治理
Android 上的 Privacy Sandbox 曾是一组用于隐私广告与第三方 SDK 隔离的平台技术:Topics 提供粗粒度兴趣主题,Protected Audience 负责设备端广告受众与选取,Attribution Reporting 负责广告归因,SDK Runtime 用独立进程承载适配后的 SDK。Google 已决定让这四项技术退出。我们翻旧文章和设计页时,仍会看到 getTopics()、selectAds()、registerSource()、SdkSandboxManager.loadSdk() 与独立 sandbox(隔离运行环境) 进程的写法;在面向 Android 17 的新代码里,这些已经是历史方案,不再是可采用的平台路径。
讨论分为两部分:
- Android 14–16 遗留 SDK Runtime 的行为与迁移要点;
- Android 17 上普通嵌入式广告 SDK 的启动治理。
这些环节的做法见同章其他小节:启动任务编排见 启动任务编排框架,懒初始化见 延迟与懒初始化;Provider 自动初始化见 ContentProvider 启动优化,多进程成本见 多进程启动优化;指标设计见 启动监控与度量。
1. Android 17 版本结论
1.1 平台 API 已明确退场
我们先看平台自己的说法。Android 17 / API 37 的公开 API 文档对 SdkSandboxManager、SandboxedSdkProvider 和 SandboxedSdk 给出相同结论:三个类都标注 deprecated(已废弃),SDK sandbox 不再受支持。
packages/modules/AdServices 源码与公开文档一致:
SdkSandboxManager类带有@Deprecated,并受 SDK sandbox API deprecation 的 feature flag(功能开关)约束;SandboxedSdkProvider与SandboxedSdk同样 deprecated(已废弃,不应再用于新代码);sandbox_app_flags.aconfig定义sdk_sandbox_no_op_impl和sdk_sandbox_api_deprecation;loadSdk()走到 no-op(不执行实际加载)分支时,会在调用方传入的 Executor(回调执行线程)上返回LoadSdkException(LOAD_SDK_SDK_SANDBOX_DISABLED, ...)。
API 符号仍然存在,只说明代码可以编译;设备上的能力是否可用,SDK_INT >= 34 判断不了,要以 API 37 的废弃说明和运行期行为为准。Android 17 新项目的广告架构要避开 SdkSandboxManager、runtime-enabled SDK(为 SDK Runtime 改造的 SDK)交付包和旧 sandbox 生命周期。
1.2 其他 AdServices API 也不能作为新依赖
Google 在 2025 年 10 月 17 日公告退役相关技术,官方状态页把 Android 上的 Topics、Protected Audience、Attribution Reporting、SDK Runtime 等能力列为“Deprecate and remove”,但没有给出一个对所有这些能力都适用的 Android 移除版本。对新业务来说,源码存在、manager 对象可以取得、旧接入文档仍在线,这些都只是退场过程中的中间状态,说明不了能力可用。
Android 17 Framework 源码里的行为更具体:
| 能力 | Android 17 行为 | 迁移判断 |
|---|---|---|
| Topics | TopicsManager 已废弃;Android 17 客户端无论服务返回成功或失败,都会向调用方返回废弃异常 | 停止新查询,不再为它提前启动服务进程 |
| Ad Selection / Custom Audience | 两个 manager 均已废弃;广告选取、加入/离开受众等入口会把结果改写为废弃异常 | 移除设备端竞价与受众维护依赖 |
| Attribution Measurement | MeasurementManager 已废弃,但注释明确处于 soft removal(先废弃、随后分阶段拒绝调用) | 停止新集成;把当前仍能成功的调用视作迁移窗口,不视作长期保证 |
| SDK Runtime | API 37 deprecated,官方说明 sandbox 不再受支持 | Android 17 明确关闭平台路径 |
旧 Topics epoch(主题更新周期)、竞价 JavaScript、registration(注册) URI(注册端点地址)的网络获取与延迟报告,如今只用来解释遗留流量,算不上还要继续优化的高频启动路径。
标识符这边要单独对待:Privacy Sandbox 退场并不会自动扩大 GAID(Google Advertising ID,广告标识符)、App Set ID(同一开发者应用集合标识)或第一方标识的用途,替代方案仍要重新通过隐私、政策、安全和性能评审。
1.3 版本表
| 系统 / 工具 | 状态 | 工程决策 |
|---|---|---|
| Android 13 / API 33 | 基础 API 提供 manager 与状态查询;loadSdk() 文档另列 Ad Services Extensions 4,不能只看基础 API 级别推断完整可用性 | 本文维护策略限定 API 34–36;扩展设备另行核对接口、状态与供应方支持 |
| Android 14–16 / API 34–36 | 平台 loadSdk() 的历史支持区间;设备状态与可独立更新的 Ad Services 扩展版本仍会影响可用性 | 只维护已发布产品,必须检查能力并准备失败降级 |
| Android 17 / API 37 | 平台 API deprecated,官方说明 SDK sandbox 不再受支持 | 停止新接入;遗留代码在 API 37 明确关闭 |
AndroidX privacysandbox-sdkruntime | 预发布版 1.0.0-alpha19 已 deprecated,release note 说明不会再更新 | 不作为 Android 17 替代方案;现有使用方制定移除计划 |
| 普通嵌入式广告 SDK | 通常随 AAR(Android 库归档)放入宿主 App 进程,除非 SDK 自己提供其他受支持的进程模型 | 按 Provider、Application、线程、I/O、内存和隐私约定治理 |
Privacy Sandbox 的 SDK Runtime architecture 与 developer guide 仍可用于理解 Android 14–16 的设计,但页面也注明内容属于可能变化的设计提案。判断 Android 17 当前能力时,以 API 37 的废弃说明、AndroidX release note(版本说明)和 android-17.0.0_r1 源码为准。
2. Android 14–16 的历史行为
我们只有维护 Android 14–16 旧设备、迁移遗留代码时才会用到这一节;Android 17 的新路径从第 3 节讲起。
2.1 loadSdk() 做了什么
App 侧的调用链在历史实现里是这样的:
App process
-> SdkSandboxManager.loadSdk(name, params, executor, receiver)
-> system_server / SDK sandbox service
-> 为该 App 创建或复用 SDK sandbox process
-> SandboxedSdkProvider.onLoadSdk(params)
-> 返回 SandboxedSdk(IBinder)
-> App 通过 Binder 调用 SDK 接口
对照上面的调用链,三个对象各司其职:system_server 承载 Android 核心系统服务,SDK sandbox process 是为当前 App 创建的隔离进程,IBinder(Android 进程间通信机制) 是跨进程接口句柄。整条链路包含进程创建、SDK 代码加载、Provider 准备和 Binder 回调。loadSdk() 是异步 API,结果稍后经 receiver(结果接收回调)返回,但这份工作本身有成本:调用方如果在首帧前等 receiver,页面仍然要等 sandbox 冷启动。
这套历史 API 还带着几条约束:
- 第一个 SDK 触发该 App 的 sandbox 进程创建,后续 SDK 可以复用同一 sandbox;
loadSdk()只允许前台调用,后台调用通过 receiver 返回失败;SandboxedSdkProvider.onLoadSdk()只做“能够处理后续请求”级别的准备,长时间 I/O、网络和依赖其他 SDK 已加载的初始化都留在这个回调之外;- sandbox 死亡后,已加载 SDK、Binder 与远程 UI 状态都会丢失,客户端需要监听死亡并重新建立状态;
SandboxedSdk.getInterface()返回 Binder,事务仍要处理线程安全、远程进程死亡、超时和大 payload(一次调用携带的大量数据)。
2.2 SharedPreferences 同步的用途
隔离在 sandbox 进程里的 SDK 要读宿主配置时,靠的是这条同步通道:addSyncedSharedPreferencesKeys() 只同步 App 默认 SharedPreferences(持久化少量键值配置的 API)中显式登记的 key。官方 API 文档注明:
- App 重启后需要重新登记同步 key;
- 同一应用只能在单个进程里使用这套同步管理;
- 移除 key 后,已经同步到 sandbox 的对应值会被删除。
同步适合少量稳定标量,也就是字符串、数字、布尔值这类单个值,不适合复制整份业务配置。每个 key 都应有数据所有者、默认值、版本与隐私用途。
高频状态走一次调用,接口本身携带完整的业务含义;大对象则用文件描述符或受支持的数据通道传递,避免在 Binder 与 Bundle 中反复复制。
2.3 遗留能力探测
维护 Android 14–16 时,能力判断需要同时约束系统版本和 sandbox 状态。下面的 34..36 是本文迁移层主动维护的基础系统区间,不是完整的 SDK Extensions 能力矩阵:官方 loadSdk() 参考同时列出基础 API 34 和 Ad Services Extensions 4,扩展版本元数据也不保证每台旧设备实际可用。若产品必须覆盖其他扩展设备,应另行验证扩展、接口和供应方支持;API 37 仍明确关闭旧路径。下面的代码只用于现有迁移层:面向 API 37 编译时会产生 deprecated 警告,所以用局部 Suppress 抑制,并把这段仍需移除的兼容代码标成技术债。
@Suppress("DEPRECATION")
fun canUseLegacyPlatformSdkSandbox(): Boolean {
if (Build.VERSION.SDK_INT !in 34..36) {
return false
}
return SdkSandboxManager.getSdkSandboxState() ==
SdkSandboxManager.SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION
}
API 37 上这段判断直接返回 false,不再尝试 deprecated 的 loadSdk()。拿到 false,我们知道的也只是“平台 sandbox 路径不可用”;主线程上是否同步加载旧广告 SDK、要不要改用嵌入式版本、功能是否关闭、能否延后加载,都由广告网关的明确策略决定。这个网关,就是业务页面和各家广告 SDK 之间的统一接口。
2.4 AndroidX compat 的历史语义
旧版 backcompat(向后兼容)方案用 SdkSandboxManagerCompat 把接口统一。在没有平台 Runtime 的设备上,Android Gradle Plugin 与 Bundletool 会生成包含 SDK 的 APK 变体,把 SDK 的 DEX(Android 字节码文件) 作为 assets(随包资源)保存。
客户端库首次加载时把 DEX 提取到 code_cache,再用独立 ClassLoader(类加载器)放进 App 进程执行。
这里的“兼容”与平台隔离是两回事:
- 代码仍在应用进程内执行;
- 独立 ClassLoader 能减少类名冲突,但独立 Linux 进程、UID 或内存空间这种隔离,它给不了;
- 首次提取 DEX、类加载和资源处理会增加存储与启动成本;
- Binder 形式的接口可以保持同一套调用方式,但 bundled SDK(随 App 打包的 SDK)位于本进程时,不一定发生跨进程事务;
- AndroidX 该库已经 deprecated,撑不起面向 Android 17 的长期抽象。
alpha19 本来就是尚未稳定的预发布版本,而这个库已经停止更新。现有产品若仍依赖它,应固定版本、保留兼容测试,并与 SDK/广告供应方确认替代交付;只给 SdkSandboxManagerCompat 换一层名字再继续扩展新功能,算不上迁移。
3. Android 17 上的广告 SDK 启动模型
SDK Runtime 退场后,广告 SDK 的常见形态回到宿主进程:AAR、dynamic feature(按需交付的动态功能模块),或 SDK 自己声明且受支持的 Android 组件。要看它花了多少启动成本,我们从 merged manifest(App 与全部依赖合并后的清单)和实际构建产物看起,Application 里那几行初始化代码只是其中一角。
3.1 建立 SDK 清单
每次 SDK 升级,我们至少把下表信息记一遍。表里几个词先约定好:“传递依赖”指 SDK 间接带入的库,exported 表示组件能否被其他 App 调用,authority 是 ContentProvider 的唯一名称:
| 维度 | 检查项 |
|---|---|
| 依赖 | 直接/传递 AAR、版本、构建变体、native(C/C++)库与新增方法数 |
| Manifest | Provider、Service、Receiver、Activity、metadata(清单键值配置)、进程名与 exported |
| 自动初始化 | Provider、AndroidX App Startup initializer(初始化器)、ContentProvider authority |
| 线程 | 启动线程、线程池大小、HandlerThread(自带消息循环的工作线程)、定时任务 |
| I/O | SharedPreferences、数据库、缓存目录、文件扫描与 native 库加载 |
| 网络 | 首个请求触发点、DNS 域名解析、TLS 加密连接、配置与素材预取 |
| 内存 | Java/native heap(托管对象与 C/C++ 分配的堆内存)、Bitmap、WebView、线程栈与主/远程进程 PSS |
| 生命周期 | 初始化是否幂等(重复调用结果不变)、Activity Context 引用、前后台切换、进程重建 |
| 数据 | 读取字段、同意状态、上传时机、保留与删除策略 |
| 控制 | 负责人、分批放量开关、关闭广告位和回滚 SDK 的路径 |
即使 SDK 文档声称“异步初始化”,也要在 trace(系统时间线) 里验证:异步 API 可能在返回前就同步执行类加载、Manifest 查询、Preferences 读取和线程创建,完成回调也可能回到主线程。
3.2 把广告能力拆成阶段
| 阶段 | 允许的工作 | 禁止依赖 |
|---|---|---|
| 进程绑定 / Provider | 仅保留 SDK 强制且无法关闭的最小工作,并测量成本 | 网络、缓存扫描、大对象、广告素材 |
Application.onCreate() | 注册轻量网关、读取已验证的本地功能开关 | 等待广告 SDK ready、首次广告请求 |
| 首帧前 | 页面是否保留广告容器的本地决策 | SDK 冷初始化、远程配置、广告返回 |
| 首帧后 | 设备和产品允许时触发初始化 | 无上限并发、立即与主线程或渲染线程争用 CPU |
| 临近广告入口 | 初始化、请求、素材准备 | 阻塞页面主线程 |
| 广告位可见 | 展示已 ready 内容,或按产品规则占位/跳过 | 同步执行备用初始化路径 |
“首帧后”仍可能影响 TTFD(Time to Full Display,主要内容完整可用时间)和第一段交互。在低端设备、首屏动画或 Jetpack Compose 首次生成并稳定 UI 期间,初始化要延迟到明确的空闲窗口或临近广告入口,并用实验确认首个广告可用率与启动体验之间的取舍。
3.3 用状态机管理一次会话的初始化
SDK init(初始化)收口到一处:广告网关持有 App 级状态,把同时到达的多次请求合并成同一个任务,失败则保存为调用方可以读取的结果,页面不再各自发起。
下面的状态机把初始化收敛为四个状态:Idle(未开始)、Initializing(进行中)、Ready(可用)、Failed(会话失败)。网关内部共享同一个 Deferred,也就是可以被多个协程等待的结果;页面只调用 prewarm() 或 awaitReady(),不拿到可被任意调用方 cancel() 的共享任务。示例没有假设 SDK 能在后台线程初始化,具体 initializer 要按供应方要求切到正确线程:
sealed interface AdSdkState {
data object Idle : AdSdkState
data object Initializing : AdSdkState
data class Ready(val client: AdClient) : AdSdkState
data class Failed(val cause: Throwable) : AdSdkState
}
class AdSdkGateway(
private val appScope: CoroutineScope,
private val initializer: suspend () -> AdClient,
) {
init {
require(appScope.coroutineContext[Job] != null) {
"appScope must have an owned Job"
}
}
private val mutex = Mutex()
private var inFlight: Deferred<Result<AdClient>>? = null
private val _state =
MutableStateFlow<AdSdkState>(AdSdkState.Idle)
val state: StateFlow<AdSdkState> = _state.asStateFlow()
private suspend fun start(): Deferred<Result<AdClient>> {
val task = mutex.withLock {
appScope.coroutineContext.ensureActive()
inFlight?.let { return@withLock it }
_state.value = AdSdkState.Initializing
val created = appScope.async(start = CoroutineStart.LAZY) {
try {
val client = initializer()
currentCoroutineContext().ensureActive()
_state.value = AdSdkState.Ready(client)
Result.success(client)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (failure: Exception) {
_state.value = AdSdkState.Failed(failure)
Result.failure(failure)
}
}
inFlight = created
created.invokeOnCompletion { cause ->
if (cause != null) {
// Also runs when cancelled before the body ever starts.
_state.value = AdSdkState.Failed(cause)
}
}
created
}
// Publish the shared task before executing supplier code; no Mutex held.
task.start()
return task
}
suspend fun prewarm() {
start()
}
suspend fun awaitReady(timeoutMillis: Long): Result<AdClient>? {
require(timeoutMillis > 0)
return withTimeoutOrNull(timeoutMillis) {
start().await()
}
}
}
appScope 由 App 管理并包含受控的 Job,通常使用 SupervisorJob。普通初始化异常作为 Result.failure 缓存;CancellationException 继续传播,致命 Error 不被包装成普通业务失败。完成钩子也处理“任务体尚未开始就取消”的情况,避免状态停在 Initializing。StateFlow 更新可在线程安全的完成钩子中进行,钩子里不调用 SDK 或操作 UI。
调用方自己的等待超时返回 null,共享初始化仍由 appScope 持有;调用方取消或共享任务取消则传播取消异常,不能统统计成普通超时。这个 deadline 依赖协作式取消,不能中断供应方在主线程上的阻塞调用,也不能承诺页面一定按时放行。如果产品需要停止真实 SDK 工作,供应方必须提供可验证的取消协议。

图:页面只得到等待结果,不得到可任意取消的共享任务;某个页面超时不会自动终止 SDK 初始化。
示例把失败与已取消任务都保留在本会话内,不自动重试;Failed.cause 仍要区分普通错误与取消。代码没有公开 reset 接口,若业务允许重试,应由网关在旧任务完全终止后按错误类型和退避策略开启新会话。Ready 仅表示初始化成功,后续请求仍须处理 SDK 生命周期和远端死亡;initializer 不得递归等待同一网关的 awaitReady()。

图:只有普通 Exception 包装为 Result.failure;取消与致命 Error 从 await 传播,Failed.cause 保留分类。
若供应方要求主线程调用 init,initializer 可以只在主线程提交轻量入口,后续重工作是否异步仍由 trace 验证。Dispatchers.IO 是 Kotlin 协程面向阻塞 I/O 的共享线程池;把整个 gateway 切到这里,也改不了内部实现不可见的 SDK 自己的线程行为。
4. 首屏广告怎样取舍
首屏广告、App Open 广告(打开 App 时展示)或首页首个广告位,都会把广告等待直接压在启动体验上。默认行为要先由产品定下来,我们再谈技术方案:
- SDK 未 ready 时跳过本次广告;
- 展示固定尺寸占位,内容 ready 后再填充;
- 等待一个很短的业务 deadline(最晚等待时刻),超时立即进入页面;
- 广告是业务入口本身时,使用独立且可取消的加载页,并把等待计入 TTFD/业务 ready(业务就绪时间)。
无限加载页等广告、新路径失败后立刻回主线程跑旧 SDK 的同步初始化,这两条路都要避开。若备用路径比正常路径更重,少量错误样本就可能集中到 P99(99% 分位耗时)、ANR(Application Not Responding,应用无响应) 和用户差评上。
广告容器还要避免素材出现后让正文突然移位。预留尺寸时使用产品确定的广告规格;跳过广告时及时移除占位。远程素材到达后触发的布局和图片解码,应纳入首屏帧耗时与内存 guardrail(实验不可越过的保护指标)。
5. Provider 与多进程边界
5.1 自动初始化发生在 Application 之前
广告 SDK 通过 Provider 自动初始化时,Application.onCreate() 里的进程分支和延迟开关已经来不及阻止 Provider onCreate()。我们要检查的是最终 merged manifest(App 与所有依赖合并后的最终清单):
- SDK 是否支持移除 Provider metadata 或切换手动初始化;
- Provider 属于主进程还是指定远程进程;
- authority 是否稳定且不冲突;
- App Startup initializer 是否仍由依赖库合并回来;
- 升级 SDK 后的 Manifest diff(前后差异)是否新增组件。
移除自动初始化必须走供应方支持的方式,直接删掉 Provider 再假设 SDK 仍可用,是行不通的。修改后要覆盖冷启动、通知/deep link、配置变更、进程重建和所有广告入口。
5.2 自有远程进程与 SDK Runtime 的差异
把广告 Service 或宿主组件声明到 android:process=":ads",得到的是 App 私有远程进程:
- 通常仍使用 App 的 UID(Linux 用户身份)与权限;
- SDK 能否在该进程工作取决于 SDK 自身实现与许可;
- UI、Activity Context(绑定 Activity 生命周期的上下文)与 WebView 可能要求主进程交互;
- Binder 调用、远程进程冷启动、独立堆内存和进程死亡恢复都由 App 负责;
- Provider 只在其所属进程安装,但
Application会在该进程创建。
这与历史 SDK sandbox 的独立 UID、受控 API 和独立 SDK package(安装包)模型不同。没有供应方明确支持和完整测试时,不要强行把内部实现不可见的广告 SDK 移到远程进程。
6. 失败与回退策略
| 失败 | 会话策略 | 启动约束 |
|---|---|---|
| Android 17 平台 sandbox 不受支持 | 不调用 deprecated load;使用受支持的嵌入式 SDK 或关闭能力 | 不尝试循环探测 |
| Android 14–16 sandbox disabled | 标记遗留平台路径不可用 | 不在首帧前重试 |
| 普通 SDK init 超时 | 页面占位、跳过或使用产品允许的其他广告源 | 调用方停止等待,不等于后台初始化已取消 |
| SDK init 抛错 | 会话级 Failed,按逐次延长间隔的退避策略与错误类型决定重试 | 禁止主线程同步执行备用路径 |
| SDK 进程/Binder 死亡 | 清理已失效的远程接口,遗留路径按可见页面需求恢复 | 重连与业务请求分开计时 |
| 配置或 SDK 缺失 | 关闭对应广告源并上报构建/配置错误 | 用户路径不负责下载修复 |
| 同意状态未满足 | 不发起受限制的初始化或请求,按产品合规策略展示 | 不用旧缓存绕过状态 |
业务页面只依赖 Disabled / Idle / Initializing / Ready / Failed 这组稳定状态,不直接认识某家 SDK 的错误码。网关保留原始错误码供诊断,再映射成跨 SDK 都能理解的业务分类。
7. 观测与验收
7.1 时间线
我们至少记录下面这些单调时间戳。单调时钟只向前推进,用户修改系统时间、网络校时都影响不到它,适合计算同一次启动内的耗时:
| 事件 | 解释 |
|---|---|
ad_gateway_trigger | 哪个页面/策略决定开始初始化 |
ad_sdk_init_call | 进入供应方 API 前 |
ad_sdk_init_callback | 成功或失败回调 |
ad_first_request_call | 第一个广告请求 |
ad_first_response | 填充、无填充或错误 |
ad_first_render_ready | 素材达到可提交 UI 的状态 |
ad_first_impression | 按供应方定义产生一次广告曝光 |
这些事件与 TTID(首次显示时间)、TTFD、首屏 frame(UI 帧) 和首个操作放在同一条 session timeline(本次启动时间线)里。初始化回调、广告请求、渲染三段耗时要分开记录;合并成一个 ad_ready 数字,就说不清慢在 SDK 初始化、网络还是 UI 了。

图:用同一单调时间基准关联会话,分别记录触发、调用、回调、响应、渲染 Ready 与曝光。
7.2 资源与稳定性
| 维度 | 指标 |
|---|---|
| 主线程 | init 调用同步耗时、首帧前 Binder/I/O、长时间占用线程的任务 |
| CPU | App/远程进程 CPU 时间、处于 Runnable(可运行、等待 CPU)状态的线程竞争、初始化线程数 |
| 内存 | 主进程与远程进程 PSS、Java/native heap、WebView/Bitmap 峰值 |
| 帧 | TTID 后首屏 slow/frozen frame(明显延迟或长时间停住的帧)、广告填充时布局和解码 |
| 稳定性 | init 错误、超时、ANR、Crash(崩溃)、Binder death(远程接口因进程退出而失效)、重试次数 |
| 业务 | 广告 ready、填充、展示、跳过、占位时长与首个广告可用率 |
| 数据质量 | 采样率、回调缺失、会话终止、SDK/配置版本 |
PSS(Proportional Set Size)按比例分摊进程间共享的内存页,RSS(Resident Set Size)则把每个进程驻留的共享页都计入。所以单看主进程 PSS 下降,还证明不了整体内存改善;若采用供应方支持的远程进程,要看各进程合计 PSS,RSS 直接相加会把共享页重复计算。
7.3 A/B 条件
A/B 实验是随机把条件相同的用户分到基准组与改动组,再对比两组结果。启动类型也要固定:cold 表示需要创建 App 进程,warm 表示保留部分状态但仍需重建一部分启动路径,hot 表示进程和目标 Activity 仍在内存中。
广告初始化实验至少固定:
- 相同 App 与 SDK 版本;
- 相同同意状态、广告位和服务端配置;
- cold/warm/hot、安装状态与入口;
- 设备档位、Android 版本和渠道;
- 相同超时、占位和跳过策略。
实验要同时观察启动、稳定性和广告业务保护指标。首帧变快但广告填充骤降,或收入改善但 ANR/P99(99% 的样本不超过的耗时) 上升,都要产品与技术一起决定,单个指标说了不算。
8. 迁移清单
清单保留代码和交付系统里的原名:
- runtime-enabled SDK:为旧 SDK Runtime 改造的 SDK;
- ASB:Android SDK Bundle 交付包;
<uses-sdk-library>:App 声明所需 SDK 包的 Manifest 元素;- Binder DTO:跨进程传递的数据对象;
- endpoint:服务端地址;
- rate limit:调用频率上限。
从旧 Privacy Sandbox 业务 API 退出
-
搜索
TopicsManager、AdSelectionManager、CustomAudienceManager与MeasurementManager的调用点、权限、配置和依赖。 - 用远程开关停止新请求,页面布局和广告填充不再等待这些结果。
- 区分已废弃(deprecated)、不支持(unsupported)、已关闭(disabled)、权限/安全拒绝(security)、频率受限(rate limit)、网络错误、调用方本地超时与取消;永久废弃错误不进入重试队列。
- 盘点 registration、bidding(竞价)、trusted-data(竞价所需可信数据) 与 reporting endpoint(报告地址) 的剩余流量,确认旧后台任务和数据库记录的删除范围。
- 只记录受控的功能名、调用位置、平台/Ad Services extension 版本、结果分类、端到端时间和备用路径;不上传 topic、audience、竞价信号或完整 registration URI。
- 替代广告、归因和标识方案分别完成合规评审,不把 GAID 当作默认回退。
从 Android 14–16 SDK Runtime 迁移到 Android 17
-
搜索
SdkSandboxManager、SandboxedSdkProvider、SandboxedSdk和*Compat使用点。 -
记录 runtime-enabled SDK、ASB、Manifest
<uses-sdk-library>与商店交付依赖。 - API 37 明确关闭平台 sandbox 路径,不仅检查类是否存在。
-
删除
loadSdk()的首帧前等待和同步备用路径。 - 与 SDK 供应方确认 Android 17 支持的嵌入式/替代版本。
- 移除或冻结 deprecated AndroidX alpha19,验证构建产物不再携带无用 DEX/assets。
- 把 Binder DTO 转换为当前 SDK 接口时,保留数据版本和错误映射。
- 对比 merged manifest,确认替代 SDK 新增的 Provider 和组件。
- 分批放量时观察 TTID、TTFD、帧耗时、ANR/Crash、PSS 和广告保护指标。
- 为旧 Android 14–16 用户保留受控迁移窗口和可回滚版本。
Android 17 广告 SDK 启动检查清单
- merged manifest 与传递依赖已经纳入检查。
- 自动初始化可以关闭时,使用供应方支持的手动路径。
-
Application只注册轻量网关,不等待 SDK ready。 - 页面并发 init 被合并成一个会话任务。
- SDK 要求的调用线程已按对应版本的官方文档验证。
- 首帧后初始化经过低端设备 CPU/帧竞争验证。
- 超时、取消、重试和会话级失败的含义清楚。
- 失败不会触发主线程同步执行旧 SDK 备用路径。
- UI 占位、跳过和布局变化有产品规则。
- 数据、同意状态和上传时机有显式契约。
- 主进程与任何远程进程资源合并统计。
- SDK 升级有版本、负责人、分批放量开关和可供回滚的构建产物。
小结
Android 17 已终止 SDK sandbox 的平台支持,旧的 loadSdk() 启动隔离方案只剩下维护 Android 14–16 遗留产品这个用途。API 37 新代码应停止探测和调用已废弃的平台路径,AndroidX 向后兼容库也接替不了长期维护职责。
广告 SDK 的启动问题随之回到常规工程:检查最终 Manifest,控制 Provider 和 Application 的工作,合并并发初始化,明确首帧、TTFD 与广告可用时间的关系,处理失败与重试,并把主线程、帧、内存、稳定性和广告指标放进同一次实验。隔离能力退场不影响这些原则,团队要做的是重新选择受支持的 SDK 交付和运行方式。
参考资料
- Android 17
SdkSandboxManager.java:API 37 deprecation 与 no-oploadSdk()路径。 - Android 17
SandboxedSdkProvider.java:Provider deprecation 与历史onLoadSdk()边界。 - Android 17 SDK sandbox flags:no-op 与 API deprecation flag。
- Privacy Sandbox technology retirement announcement:2025 年 10 月 17 日的退场决定与后续分阶段移除说明。
- Privacy Sandbox feature status:Android 各项技术当前的 phaseout 状态。
- Android 17
TopicsManager.java:无论服务结果如何均返回废弃异常的客户端路径。 - Android 17
AdSelectionManager.java:广告选取入口的废弃标记与回调转换。 - Android 17
CustomAudienceManager.java:受众加入、离开等入口的废弃回调。 - Android 17
MeasurementManager.java:Attribution Measurement 的 soft removal 注释与过渡调用路径。 SdkSandboxManagerAPI reference:API 37 deprecated 和“不再受支持”的公开说明。SandboxedSdkProviderAPI reference:历史加载约束与 API 37 deprecation。- SDK Runtime overview:Android 14–16 设计目标、进程与通信模型。
- SDK Runtime backward compatibility:旧版 in-app ClassLoader 兼容模型。
- AndroidX Privacy Sandbox SDK Runtime releases:alpha19 deprecation 与停止更新说明。