Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 行为迁移判断
TopicsTopicsManager 已废弃;Android 17 客户端无论服务返回成功或失败,都会向调用方返回废弃异常停止新查询,不再为它提前启动服务进程
Ad Selection / Custom Audience两个 manager 均已废弃;广告选取、加入/离开受众等入口会把结果改写为废弃异常移除设备端竞价与受众维护依赖
Attribution MeasurementMeasurementManager 已废弃,但注释明确处于 soft removal(先废弃、随后分阶段拒绝调用)停止新集成;把当前仍能成功的调用视作迁移窗口,不视作长期保证
SDK RuntimeAPI 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++)库与新增方法数
ManifestProvider、Service、Receiver、Activity、metadata(清单键值配置)、进程名与 exported
自动初始化Provider、AndroidX App Startup initializer(初始化器)、ContentProvider authority
线程启动线程、线程池大小、HandlerThread(自带消息循环的工作线程)、定时任务
I/OSharedPreferences、数据库、缓存目录、文件扫描与 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 工作,供应方必须提供可验证的取消协议。

页面等待有自己的时限,共享初始化由 App 持有

图:页面只得到等待结果,不得到可任意取消的共享任务;某个页面超时不会自动终止 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、长时间占用线程的任务
CPUApp/远程进程 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 交付和运行方式。

参考资料