国内厂商的系统级广告 SDK,开发者接还是不接
国内厂商的系统级广告 SDK,开发者接还是不接
上个月处理了一个线上 ANR,堆栈信息指向华为广告 SDK 的初始化线程。 targetSdkVersion 34,Android 14 机器,用户在冷启动时直接卡死五秒,系统弹出 "应用无响应"。去华为开发者后台翻了三个文档版本,发现他们 13.4.50 系列的 SDK 在 Android 14 上有个已知问题:如果设备同时开启了限制后台启动,SDK 的 ContentProvider 初始化会阻塞主线程等待一个跨进程服务的绑定结果,超时时间写死的是 5000ms。这 5000ms 刚好踩在了 Android 14 对前台服务启动更严格的阈值线上。解决方案是升级到 13.4.58,但升级之后包体积又涨了 2.3MB。这个时候我开始认真考虑一个问题:这些国内手机厂商的系统级广告 SDK,我们到底为什么要接?
系统级广告 SDK 和普通聚合广告差在哪
很多人把系统级广告 SDK 和穿山甲、优量汇、百青藤这些第三方聚合平台混为一谈,这其实是个巨大的误区。穿山甲再怎么强势,它本质上还是一个应用层 SDK,依靠的是你 App 自身的上下文权限,能拿到多少设备信息取决于你的应用授权。但华为鲸鸿动能、小米米盟、OPPO 广告联盟这些所谓的"系统级"方案,根子上是绑在厂商 ROM 里的服务框架。它们通常会在系统层面预装一个或多个月活级别的服务进程,你的 App 接入之后,并不是在跑自己的广告逻辑,而是通过 AIDL 或者广播去调用系统级服务的接口。
这个架构差异直接带来了几个坑。第一,版本耦合。你的 App targetSdk 34,但用户手机上的系统广告服务版本可能是两年前的,也可能是上周刚推送的内测版。API 契约在文档里写得天花乱坠,实际跑起来就是 NPE 或者 TransactionTooLargeException。第二,上下文隔离。穿山甲的激励视频广告失败了你至少能 catch 到一个明确的 AdError,代码是 40001 还是 10009 一目了然。但系统级 SDK 经常给你抛一个 "INTERNAL_ERROR" 或者 "SERVICE_UNAVAILABLE",然后你在 logcat 里看到系统服务进程的 DeadObjectException,这你找谁去?第三,也是最隐蔽的,这些 SDK 往往自带强烈的"渠道属性"。华为 SDK 在荣耀老机型上表现异常,小米 SDK 在 MIUI 14 的国际版上直接不初始化,OPPO 的 Realme 子品牌有时候识别成白牌机填充率归零,这些都不是应用层代码能修的。
我做过一次横向的初始化耗时测试。在同一台红米 K60 上,纯穿山甲 SDK 的初始化耗时平均 87ms;接入小米米盟 5.2.1 版本后,初始化耗时跳到了 340ms,其中 220ms 花在了 init() 方法里等待一个系统级 ContentProvider 的返回。这 220ms 是在主线程的。如果你项目在 Application.onCreate 里串行初始化了三四个这样的 SDK,启动时间直接爆炸。
包体积与启动损耗:肉眼可见的代价
国内厂商的 SDK 在体积控制上普遍做得极差。这不是我空口说,看依赖树就知道。华为 Ads SDK 13.x 系列引入了一系列 HMS Core 的间接依赖,什么 com.huawei.hms:base、com.huawei.hms:log、com.huawei.hms:dynamicability,表面上你只接了一个广告包,Gradle 给你拉下来七八个 aar。实际测试下来,一个原本 18MB 的 APK,接完华为、小米、OPPO 三家系统级广告 SDK 之后,体积涨到了 26MB 左右,增长的 8MB 里至少有一半是各家 HMS、MiPush、Heytap 的底座依赖。
这些底座依赖很多时候和你的业务完全无关。比如你为了接广告,被迫引入了华为的动态能力服务(Dynamic Ability),这个服务在广告 SDK 里的用途是"按需加载广告素材",但它的权限声明里包含了 com.huawei.permission.SECURE_DATA,这在 Google Play 的审核政策里属于高风险权限,需要额外的声明视频和说明文档。有些团队为了上 Google Play,不得不写 flavor 把华为广告整个切掉,然后维护两套代码分支,这纯粹是给自己找活干。
启动损耗更隐蔽。Android 14 之后,系统对 ContentProvider 的初始化顺序做了更严格的限制,如果你的 SDK 用 ContentProvider 做自动初始化(就是那种在 AndroidManifest 里声明 android:initOrder 的玩法),它会在 Application.onCreate 之前执行。华为广告 SDK 在某个版本里就是这么干的。后果是什么?你的 App 还没有进入自己的 Application 逻辑,主线程已经被广告 SDK 占住去扫描 OAID、读取系统属性、检查 HMS Core 版本。用户在桌面点击图标,系统已经开始计算启动时间,你的代码还没跑,黑屏或者白屏就已经出现了。这种损耗不会直接体现在你的启动埋点里,因为埋点通常打在 Application.attachBaseContext 之后,但用户感受是实打实的"这 App 怎么点了没反应"。
权限黑洞:Android 14 之后的合规噩梦
去年 Android 14 正式发布之后,国内厂商广告 SDK 的权限问题集体爆发。最典型的两个点:精确闹钟权限(SCHEDULE_EXACT_ALARM)和后台启动 Activity 的限制。
精确闹钟这个坑,华为广告 SDK 在 13.4 之前的版本里,为了做"定时广告刷新"或者"延迟归因上报",在内部调用了 AlarmManager.setExact()。Android 14 要求这个调用必须持有 SCHEDULE_EXACT_ALARM 权限,且用户可以在设置里随时撤销。如果你的应用 targetSdkVersion 34,没有声明这个权限,或者用户关掉了,调用会直接抛 SecurityException。但华为 SDK 没有在外层 catch 住这个异常,导致线上直接崩溃。他们的官方 issue 列表里这个问题被顶了两个月,修复版本 13.4.56 的 release note 里只写了一句话:"优化 Android 14 兼容性"。你根本不知道他们内部做了什么改动,因为 SDK 是混淆后的 aar,反编译出来全是 a.b.c。
后台启动 Activity 的限制更离谱。从 Android 10 开始,系统就开始限制后台应用启动前台 Activity,Android 14 进一步收紧。但广告 SDK 的核心商业模式之一就是"在合适的时机弹出一个全屏插屏或者激励视频"。系统级广告 SDK 理论上可以利用厂商特权绕过这个限制,因为它们跑在系统服务进程里。但问题在于,SDK 客户端代码并不总是知道自己是在前台还是后台。我见过一个案例:OPPO 广告 SDK 4.1.x 版本,在用户点击通知栏跳转到我们的 App 之后,广告回调触发了激励视频的加载,但由于生命周期判断失误,SDK 认为此时仍处于"后台状态",于是尝试通过系统级服务去"提权"启动 Activity,结果被 Android 14 的安全机制拦截,返回了一个 BackgroundActivityStartNotAllowedException。这个异常没有被正确传递给应用层,导致我们的激励视频回调一直处于 pending 状态,用户看完了前置条件(比如下载试玩)却拿不到奖励,客诉直接炸了。
隐私合规方面,这些 SDK 对 OAID 的读取策略也成问题。国家市场监管总局和工信部对 OAID 的规范已经出了几轮,要求明示收集规则、提供退出机制。但系统级广告 SDK 往往利用系统权限在应用授权之前就开始读取设备标识符。有些厂商的文档里甚至会"建议"你在隐私协议弹窗之前就初始化 SDK,美其名曰"预加载提升填充率"。这在合规审查里就是雷。去年某社交 App 被点名,原因之一就是"未经用户同意收集设备信息",根子上就是初始化时机问题。接这种 SDK,等于是在合规红线上走钢丝。
后台驻留与保活:你的 App 成了"帮凶"
系统级广告 SDK 另一个被忽视的副作用,是它们对你的应用保活策略的污染。
这些 SDK 通常需要维持一个长连接到厂商的推送或广告调度服务器,以便接收实时竞价(RTB)的指令或者预加载素材。它们实现这个长连接的手段,往往依赖于厂商自己的推送服务——比如华为 Push、小米 Push。这意味着你为了接广告,可能被迫同时接入厂商的推送 SDK。而推送 SDK 的保活机制是非常激进的:广播监听、JobScheduler 保活、账号同步、甚至利用系统级服务拉活。
结果就是,你的 App 日活明明只有五十万,但系统后台看到的活跃进程数可能是三百万。用户对"这 App 怎么又耗电了"的抱怨,最后不会怪到华为或者小米头上,而是你的 App 在应用商店里被打上"高耗电"的标签。vivo 的电量管理、OPPO 的内存清理,对这些非系统应用的推送进程下手是非常狠的,你的主进程可能被杀了,但广告 SDK 的推送进程还在尝试复活,触发系统更严厉的封杀策略,形成恶性循环。
我还遇到过更极端的情况。某次版本上线后,我们的崩溃率突然上涨,调查发现是小米广告 SDK 在后台启动了一个 WorkManager 任务去上报广告曝光日志,但这个任务触发了小米系统自带的"链式启动"限制,导致系统直接抛出 SecurityException: Unable to start service Intent。问题是这个崩溃发生在我们的应用进程里,因为小米 SDK 把上报逻辑绑定在了我们的 UID 下。换句话说,厂商系统的两个组件在打架,战场却是你的应用进程。
eCPM 的真相:分成比例与填充率的数学游戏
很多商务或者运营同学推动接系统级广告 SDK 的理由通常是:"厂商渠道 eCPM 高"。这句话需要拆解来看,而且拆完之后通常不太好看。
所谓 eCPM 高,很多时候是因为厂商系统掌握了更全的用户画像。比如华为知道这台手机的 HMS 账号年龄、性别、消费层级,小米知道用户的 MIUI 活跃时段和硬件换新周期,这些数据在 RTB 竞价时的确能卖出更高的价格。但问题是,这更高的价格有多少能流到你手里?
国内厂商广告平台的分成比例普遍不透明。穿山甲、优量汇虽然也被开发者吐槽,但至少后台能看到一个相对清晰的收入构成:曝光量、点击率、转化数、预估收益。而某些厂商平台的报表,给你一个"预估收益"数字,但扣量逻辑、返点政策、技术调优分成(就是你用了他们的高级功能要倒扣钱)全部黑箱。我曾对比过同一款工具类 App 在华为鲸鸿动能和穿山甲的激励视频收益,华为的后台 eCPM 显示比穿山甲高 40%,但实际打到账户里的钱,扣除各种"技术服务费"和"激励政策调整"之后,差距缩小到了 8%。为了这 8%,你要承受包体积、启动耗时、ANR 率上涨、合规风险,这笔账怎么算都不划算。
填充率的数据也需要打问号。系统级广告 SDK 经常会返回"系统自带广告"或者"厂商合作广告"来充填,这些广告的点击率极低,但 SDK 会在内部把它们标记为"已填充",计入填充率指标。你看着填充率 95%,很开心,但那一半是应用商店推荐、一半是系统主题推广,用户根本不点。更恶劣的情况是,某些厂商会返回"静默预下载"类型的广告素材,在 WiFi 环境下后台下载安装包,占用用户的存储空间,而你的 App 作为载体,还要背这个锅。
那些厂商不会写在文档里的"附加条款"
接系统级广告 SDK 还有一些隐形成本,厂商文档里要么一笔带过,要么根本不提。
第一个是审核反噬。各大厂商的应用商店都有自己的"绿色应用"认证或者标星机制,其中一项指标就是"广告体验"。讽刺的是,你接了他们自己的广告 SDK,如果展示频次控制得不好,反而可能被他们的商店审核降级。华为应用市场曾经有一段时间严查"自动弹窗广告",但华为广告 SDK 的插屏广告默认的展示间隔就是 0(即不限制),你需要手动设置 freqCap。很多开发者不知道这个细节,上线之后被华为自己的审核团队打回,要求整改广告逻辑。
第二个是数据回传的霸王条款。接入协议里往往有一行小字:为了优化广告效果,SDK 可能会匿名上传用户的应用列表、使用时长、点击热区。这个"匿名"在法律和技术上都很暧昧。Android 14 之后,Google 对 package visibility 的管控加强,但系统级 SDK 可以通过系统签名权限绕过这个限制,直接读取你手机里装了什么 App。这些数据去到哪里,用于什么模型训练,你没有知情权,也无法在隐私协议里向用户解释清楚。
第三个是退出成本。一旦你深度接入了某家厂商的广告 SDK,用了他们的瀑布流竞价、用了他们的聚合策略、甚至把收入结算账户都绑定了,后期想切到第三方平台,代码层面的解耦非常痛苦。它们的广告位 ID 体系和穿山甲、Mintegral 完全不兼容,API 设计也是各有各的古怪。华为的广告位要区分 "Splash" 和 "Roll",但枚举值命名是 AD_POS_ID_SPLASH 和 AD_POS_ID_ROLL,而小米对应的叫 SPLASH_AD 和 INTERSTITIAL_AD,这种命名混乱在大型项目里会导致大量胶水代码。更麻烦的是,有些 SDK 会在本地数据库或 SharedPreferences 里写状态,卸载 SDK 之后残留的垃圾数据可能导致崩溃。
替代方案与我的判断
那如果不接系统级广告 SDK,我们接什么?
对于中小开发者,穿山甲 + 优量汇 + Mintegral 的聚合方案已经能覆盖绝大多数预算。如果怕填充率不够,加个 AdMob 或者 Pangle 做兜底。这些平台虽然也有各自的问题(比如穿山甲的 SDK 体积也在膨胀),但至少它们是标准的三方库,跑在你的应用进程里,行为可预测、异常可 catch、版本可控。如果你确实看重某家厂商的高端用户群,完全可以只通过聚合平台的 DSP 竞价去触达,而不是把对方的 SDK 直接埋进自己代码里。
对于工具类、内容