Mediation 广告聚合,变现收益到底差多少

Mediation 广告聚合,变现收益到底差多少

Mediation 广告聚合,变现收益到底差多少


Mediation 广告聚合,变现收益到底差多少


去年 Q3 我把一个 DAU 八十万左右的工具 App 从纯 AdMob 切到了 AdMob Mediation,接了 Meta Audience Network、Mintegral、Pangle 三家,瀑布流叠实时竞价混跑。商务那边的预期是 ARPU 至少提升三成,因为行业里到处都在说聚合能把填充率拉到 95% 以上,eCPM 自然水涨船高。结果跑了两个月,广告后台拉下来的数据把预期打了个对折:填充率确实从 87% 涨到了 96%,但扣完平台分成、汇率损耗和未填充请求的沉没成本,激励视频位等效 eCPM 只从 $1.45 涨到 $1.62,ARPU 提升勉强过 10%。更麻烦的是包体涨了 3MB,印尼区的 1 日留存掉了两个点。这让我开始认真算一笔细账——Mediation 的收益优势,在真实项目里到底被哪些环节吃掉了。


瀑布流后台配置:eCPM 底价不是越高越好


大部分人配 Waterfall 的时候,都是在 AdMob 后台给每个第三方 Network 填一个 eCPM 底价,平台按价格从高到低排序请求。听起来很直接,但问题就出在这个“填”字上。AdMob 后台让你填的是预期 eCPM,而不是实时市场价。我最初的做法是直接抄 Meta Audience Network 后台前七天的平均 eCPM,美国区填 $4.2,比 AdMob 历史均值高 15%。结果当天 Meta 的展示量暴涨,第二天看 Meta 后台,实际结算 eCPM 只有 $3.1。差额从哪来?Mediation 的排序依据是你手工填写的静态价格,而 Meta 内部的竞价逻辑是二价结算,加上 AdMob 还要抽成,实际到手的价格被压了一截。


更隐蔽的是国家粒度。AdMob 的 Waterfall 允许按国家分组,但很多人为了省事直接设一个 Global Floor。我手上有印尼和美国两个大流量区,如果拿美国的 $4.2 作为全球底价,印尼的请求根本不会走到 Meta,因为印尼区 Meta 的实际 eCPM 可能只有 $0.6,永远排不到队。反过来,如果按印尼价设全球底价,美国区 AdMob 的高价广告就会被低价 Meta 截流。我当时分国别拆成了六组 Line Item,每周根据上一周数据手动调价,调了一周就放弃了——数据延迟 24 到 48 小时,调完客户端还有本地缓存,生效慢得令人绝望。


这种静态底价的本质是用昨天的地图找今天的宝藏。Bidding 被发明出来就是为了解决这个问题,但 Bidding 也有自己的时间黑洞。


Bidding 超时的沉默成本


AdMob 在 21.0.0 之后大力推 Bidding,我把 Meta 和 Pangle 都切到了 Bidding,保留 Waterfall 兜底。理论上实时竞价每次请求都取各家当前最高价,收益应该优于静态瀑布流。但上线后我发现印尼区的填充率反而掉了 7%。抓了一台印尼测试机的完整 Logcat,过滤 I/AdsAds-Mediation 标签,看到大量这样的记录:


I/Ads: Bidding started for adapter: com.google.ads.mediation.pangle.PangleAdapter
I/Ads: Bidding completed for adapter: com.google.ads.mediation.pangle.PangleAdapter
W/Ads: Bidding timed out for adapter: com.google.ads.mediation.facebook.FacebookAdapter

Pangle 的 Bidding 在 500ms 内返回了,Meta 的超时阈值在当时的网络环境下被击穿,直接弃用。Mediation 接着去请求 Waterfall 里的下一家,但时间已经损耗了 1.5 秒。对于开屏广告位,用户如果在那 1.5 秒内点了跳过,广告还没来得及展示,这次请求就作废了。


我后来查 Google Mobile Ads SDK 的 release note,21.2.0 版本确实调整过一次 bidding timeout 策略,默认把移动网络的竞价超时从 1500ms 改成了动态计算。但实际在 Mintegral Bidding Adapter 16.4.0.0 版本配合 Android 10 的某些印尼设备上,这个动态计算并不稳定。Logcat 里能看到 bidding completion 的回调比 AdMob 内部计时器晚了 200ms,刚好被判定超时。那 7% 的填充率损失,对应到日活上就是几千次展示,按印尼区 $0.8 的 eCPM 算,一天几十美元悄无声息地没了。


想验证这个问题,只能在测试包里的 AdRequest 加上测试设备 ID:


AdRequest request = new AdRequest.Builder()
    .addTestDevice("TEST_DEVICE_ID_HASH")
    .build();

然后抓完整日志看 bidding 各阶段的耗时。文档不会告诉你的是,很多 Adapter 的 Bidding 初始化还要额外再走一次网络,如果用户第一次打开 App,Meta 的 Bidder Token 可能还没生成好,这次请求直接废掉。


Adapter 初始化顺序与主线程陷阱


接多家 Network 最大的性能冲击在启动阶段。官方文档示例通常把 MobileAds.initialize() 放在 Application.onCreate() 里,然后推荐你也把第三方的初始化塞进去。我在早期版本里写了类似这样的代码:


public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        MobileAds.initialize(this);
        AudienceNetworkAds.initialize(this);
        MintegralSDK.init(...)
    }
}

结果冷启动时间从 1.1 秒涨到 1.9 秒,Firebase 上 ANR 率从 0.12% 飙到 0.31%。Google Play 的 bad behavior 阈值是 0.47%,虽然还没踩线,但搜索排名权重已经开始波动。更致命的是,Meta Audience Network Adapter 6.11.0 在初始化时如果主线程被卡超过 2 秒,不会抛崩溃,而是静默失败。Logcat 里只有一条容易被忽略的 Warning:


W/com.facebook.ads: Initializing Audience Network SDK took too long

这意味着后续所有发给 Meta 的 AdRequest 都会直接失败,Mediation 默默降级到 AdMob 兜底。那几天美国区的 Meta 填充量莫名其妙跌了 40%,我排查了后台 eCPM、排查了素材审核状态,最后是在一台 Pixel 4 上反复清数据冷启动,才在日志里抓到这条警告。


解决方法是把非 AdMob 的初始化全部延后到第一个 Activity 完全渲染之后,甚至等开屏广告展示结束再异步去做。Google Mobile Ads SDK 从 21.0.0 开始明确要求 initialize 必须在主线程调用,如果你之前为了避卡把它扔进了后台线程:


new Thread(() -> MobileAds.initialize(appContext)).start();

21.0.0 之后会直接抛 IllegalStateException: Method initialize must be called on the main thread.。诡异的是,当你接了四五个第三方 Adapter 时,某些 Adapter 的初始化异常捕获会把主线程检查抛出的异常吞掉,App 不崩溃,但 AdMob 自身初始化失败,整个 Mediation 体系里 AdMob 变成未就绪状态。这时所有请求实际上只在第三方 Network 里打转,而它们的填充率远低于 AdMob,收益反而比纯 AdMob 还差。我升级 play-services-ads 从 20.6.0 到 21.5.0 时踩了这个坑,查 GitHub 上的 googleads-mobile-android-examples Issues 区才定位到是线程问题。


展示上报:谁触发了 Impression?


填充不等于变现。Mediation 里有个长期被忽视的灰色地带:展示上报到底归谁管。


AdMob 20.6.0 之前,如果第三方 Adapter 没有正确调用 reportAdImpression(),AdMob 后台不会记录这次展示,但第三方自己的后台可能已经算了展示并收了广告主的钱。开发者夹在中间,用户看了广告,第三方收了钱,AdMob 没给你分成。


我遇到的具体案例是 InMobi Adapter 10.1.0.0 接原生广告时,在 Android 12 设备上 UnifiedNativeAdMapper 没有自动桥接 recordImpression()。用户滑动信息流,广告卡片完全曝光,但 AdMob Mediation 层没收到展示回调。那个月原生广告位的收入比预期少了 18%,两边后台数据死活对不上。后来升级到 InMobi Adapter 10.5.0.0 才修复。如果你手写 Custom Event,这个坑更直接——你必须在适当位置手动触发:


mediationListener.onAdImpression();

漏掉这一行,就是纯纯白给。类似的,Meta 的原生 Banner 在某些版本里需要显式调用 setManualImpressionsEnabled(true),否则曝光不计费:


adView.setManualImpressionsEnabled(true);

这些细节分散在各个 Adapter 的 release note 里,没有一处统一文档。Mediation 表面上屏蔽了各平台的差异,实际上只是把差异埋到了更深的日志层级。


包体积与新兴市场的隐性税收


每加一个 Network,就要多扛一个 AAR。Meta Audience Network 6.12.0 约 800KB,Mintegral SDK + Adapter 约 1.2MB,Pangle 约 900KB,R8 压缩后总包体从 8MB 涨到 11MB。对于美国、日本这类高 ARPU 市场,用户不在乎多等两秒下载;但在印度、印尼、巴西,包体是硬门槛。


我的数据是:包体从 8MB 涨到 11MB 后,印尼区 Google Play 的下载完成率从 78% 降到 71%。1 日留存从 41% 掉到 38%。这 3% 的留存差距,放到八十万 DAU 里,相当于每天少了两万多活跃用户。长期看,广告库存总量被包体积抵消了一部分。更麻烦的是权限。Meta SDK 在某些版本会要求 QUERY_ALL_PACKAGES,2023 年之后 Google Play 对这个权限审核极严,稍有不慎就是拒审或下架。一次三天下架的损失,可能直接抹掉半年 Mediation 带来的增量收益。


还有一个很少被讨论的点是 Gradle 依赖冲突。Meta 内部依赖的 OkHttp 版本如果和项目里的不一致,运行时可能爆出 NoSuchMethodError,但只在特定机型复现。我的做法是强制在 build.gradle 里统一 exclude:


implementation('com.google.ads.mediation:facebook:6.12.0.0') {
    exclude group: 'com.squareup.okhttp3'
}

这种排包操作本身就有风险,需要逐个版本验证,运维成本直线上升。


手上两组对照数据的真实差距


说点最直接的对比。我维护了同一代码基线打出的两个渠道包:A 包纯 AdMob,B 包 AdMob Mediation(Meta + Mintegral + Pangle,Waterfall 与 Bidding 混跑)。两个包买量渠道相同,DAU 都在四十万上下,跑了完整的两个月周期。


A 包(纯 AdMob):激励视频填充率 87%,eCPM $1.45,原生广告填充率 82%,整体 ARPU $0.0082。

B 包(Mediation):激励视频填充率 96%,加权 eCPM $1.62,原生广告填充率 94%,整体 ARPU $0.0091。


ARPU 绝对值提升约 11%。但 B 包因为多了三个 SDK 的初始化负载,冷启动 ANR 率从 0.12% 涨到 0.28%;包体增加导致印尼区留存下滑;加上每周需要花两个小时在 AdMob 后台调各家的 Floor Price 和 Bidding 开关。如果把留存损失折算成长期 DAU,再算上维护人力,这 11% 的涨幅并没有账面看起来那么美。


更现实的是,第三方平台的 eCPM 数据存在水分。Meta 后台显示的 eCPM 是毛价,扣掉平台分成和汇率波动,进到 AdMob 再统一结算时通常要打八折到八五折。Pangle 在部分国家的填充虽然高,但 eCPM 低到几乎可以忽略,只为了把那 5% 的未填充缺口补上,却要付出包体和崩溃率的代价。


那个藏在 SDK 21.5.0 里的加载策略变更


最后提一个具体版本变更。AdMob SDK 在 21.5.0 里改了一个内部行为:同一个广告位短时间内连续发起 loadAd(),如果前一次请求还在进行中,新的请求会被静默取消,返回 Error Code 3NO_FILL)。以前 20.x 版本的行为是并行持有多个请求。


这个变更对 Mediation 的影响被放大了。Waterfall 模式下,如果第一层 Meta 请求超时,Mediation 本来应该无缝_fallback_到第二层 AdMob。但 21.5.0 之后,如果你的代码在 onAdFailedToLoad 里立刻再次 loadAd,而 Mediation 内部实际上还在等某个第三方的回调,这次新请求会被 SDK 层拒绝。Logcat 里只能看到:


W/Ads: Failed to load ad: 3

你不会意识到这是 Mediation 内部排队冲突,还以为是没填充。解决方法是给每个广告位加一个 300ms 到 500ms 的加载间隔,或者确保上一次 AdLoader 完全释放后再发起新请求。这个行为在官方文档里只用一句话带过,但实际线上表现就是填充率莫名其妙低了一截,尤其出现在用户快速切换页面、反复触发广告位的场景里。


收益差多少?纯技术角度看,Mediation 确实能压榨出最后一成左右的填充率,把等效 eCPM 抬个 10% 到 15%。但这笔钱不是白捡的。你需要持续维护瀑布流排序、监控每个 Adapter 的版本兼容性、处理 Bidding 超时、盯紧 ANR 和包体积,还要承担多平台合规的乘数级风险。对于中小型团队,如果 AdMob 纯跑已经能拿到 85% 以上的填充率,接入 Mediation 的边际收益可能远低于边际成本。只有在填充率真的存在明显短板、且有专人负责广告策略调优的时候,聚合才真正划算。否则那几行接入代码背后,藏着一个需要持续填坑的无底洞。

Android 的内存分页与大页,对 App 有什么影响 2026-07-23
kotlinx.serialization 的稳定性,生产环境敢用吗 2026-07-23

评论区