一、架构选择:聚合平台(Mediation)
同时接入多个广告网络,强烈推荐使用广告聚合(Mediation)平台,而不是自己手动管理切换逻辑。
1.1 主流聚合平台对比
| 聚合平台 | 说明 | 推荐度 |
|---|---|---|
| Google AdMob Mediation | AdMob 官方内置聚合,支持 Meta AN 作为第三方适配器 | ⭐⭐⭐⭐⭐ |
| AppLovin MAX | 独立聚合平台,支持广泛广告网络,实时竞价能力强 | ⭐⭐⭐⭐⭐ |
| ironSource LevelPlay | 游戏领域主流,已与 Unity Ads 合并 | ⭐⭐⭐⭐ |
| Mintegral / TopOn / TradPlus | 国内出海常用,支持国内外主流网络 | ⭐⭐⭐⭐ |
本指南重点讲解最主流的两种方案:
- Google AdMob Mediation(以 AdMob 为主,Meta AN 做竞价补充)
- AppLovin MAX(独立聚合,AdMob + Meta AN 并行竞价)
二、方案一:Google AdMob Mediation + Meta Audience Network(推荐)
这是最直接的方案——AdMob 作为聚合主体,Meta AN 通过 Bidding(实时竞价) 参与竞争。好的,已经获取到所有关键信息。下面为您撰写完整详细的集成指南。
Meta Audience Network + AdMob 双平台集成完整指南(iOS/Swift)
一、架构选择:为什么需要聚合(Mediation)?
手动管理两个广告 SDK 的加载、展示、降级逻辑非常繁琐且容易出错。使用 广告聚合平台 可以:
- ✅ 自动让多个广告网络实时竞价(Bidding),最大化收益
- ✅ 统一 API 调用,不需要为每个广告网络写单独代码
- ✅ 自动处理 No Fill 降级(A 网络没广告则自动切换到 B)
- ✅ 统一管理隐私合规
1.1 主流聚合平台对比
| 聚合平台 | 特点 | 适合场景 | 推荐度 |
|---|---|---|---|
| Google AdMob Mediation | AdMob 官方内置,Meta AN 做竞价适配器 | 已使用 AdMob 的项目,最简单 | ⭐⭐⭐⭐⭐ |
| AppLovin MAX | 独立聚合,两者并行竞价,公正透明 | 追求最高 eCPM 的游戏类应用 | ⭐⭐⭐⭐⭐ |
| ironSource LevelPlay | 与 Unity 合并,游戏领域强势 | Unity 游戏或已使用 ironSource | ⭐⭐⭐⭐ |
| TopOn / TradPlus | 国内出海常用,支持国内外主流网络 | 出海应用同时接国内外广告 | ⭐⭐⭐⭐ |
💡 本指南重点讲解最主流的方案:Google AdMob Mediation + Meta AN(方案一) 和 AppLovin MAX(方案二)
二、方案一:Google AdMob Mediation + Meta Audience Network
核心思路: AdMob 作为主聚合,Meta AN 通过 Bidding 适配器参与实时竞价竞争
2.1 版本要求
| 条件 | 最低版本 |
|---|---|
| iOS Deployment Target | 13.0 |
| Google Mobile Ads SDK | 12.0.0+(推荐最新) |
| Meta Audience Network SDK | 6.21.0 |
| Meta Adapter | 6.21.0.0 |
| Xcode | 最新版本 |
⚠️ Meta AN 自 2021 年起 只支持 Bidding(实时竞价),不再支持 Waterfall
2.2 CocoaPods 安装
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
# ① Google Mobile Ads SDK(AdMob 主体)
pod 'Google-Mobile-Ads-SDK'
# ② Meta Audience Network Mediation Adapter(自动包含 FBAudienceNetwork SDK)
pod 'GoogleMobileAdsMediationFacebook'
end
pod install --repo-update
只需要添加
GoogleMobileAdsMediationFacebook,它会自动拉取FBAudienceNetworkSDK,不需要额外单独引入。
2.3 Info.plist 配置
<!-- ① AdMob App ID(必须) -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
<!-- ② App Tracking Transparency 权限说明(iOS 14.5+ 必须) -->
<key>NSUserTrackingUsageDescription</key>
<string>此标识符将用于向您投放个性化广告</string>
<!-- ③ SKAdNetwork 标识符(AdMob + Meta 都需要) -->
<key>SKAdNetworkItems</key>
<array>
<!-- Google -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<!-- Meta -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<!-- ... 完整列表参见 Google 和 Meta 官方文档 -->
</array>
2.4 AppDelegate 初始化
import UIKit
import GoogleMobileAds
import FBAudienceNetwork
import AppTrackingTransparency
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ⏱ 延迟请求 ATT 权限(建议在首页 viewDidAppear 中调用更好)
// 但必须在广告请求之前完成
return true
}
}
2.5 ATT 权限请求 + SDK 初始化(推荐写法)
import UIKit
import GoogleMobileAds
import FBAudienceNetwork
import AppTrackingTransparency
class MainViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
requestATTThenInitializeAds()
}
private func requestATTThenInitializeAds() {
if #available(iOS 14.5, *) {
// ① 先请求 ATT 权限
ATTrackingManager.requestTrackingAuthorization { [weak self] status in
DispatchQueue.main.async {
// ② 根据结果设置 Meta ATE 标志
// 注意:SDK 6.15.0+ 在 iOS 17+ 会自动读取 ATT 状态
// 但 iOS 14.5 ~ 16.x 仍需要手动设置
switch status {
case .authorized:
FBAdSettings.setAdvertiserTrackingEnabled(true)
case .denied, .restricted:
FBAdSettings.setAdvertiserTrackingEnabled(false)
case .notDetermined:
FBAdSettings.setAdvertiserTrackingEnabled(false)
@unknown default:
break
}
// ③ ATT 完成后再初始化 Google Mobile Ads SDK
self?.initializeGoogleAds()
}
}
} else {
// iOS 14.5 以下直接初始化
initializeGoogleAds()
}
}
private func initializeGoogleAds() {
// Google Mobile Ads SDK 初始化(会同时初始化所有 Mediation Adapter)
GADMobileAds.sharedInstance().start { status in
print("✅ AdMob SDK 初始化完成")
// 打印各 Adapter 状态
let adapterStatuses = status.adapterStatusesByClassName
for (adapter, status) in adapterStatuses {
print(" Adapter: \(adapter), State: \(status.state.rawValue), Desc: \(status.description)")
}
}
}
}
⚠️ 关键顺序:ATT 权限 → 设置 Meta ATE → 初始化 GADMobileAds
Google AdMob Mediation 初始化时会自动初始化 Meta AN SDK 适配器,不需要单独调用
FBAudienceNetworkAds.initialize()
2.6 Banner 广告(通过 AdMob 聚合)
import UIKit
import GoogleMobileAds
class BannerViewController: UIViewController, GADBannerViewDelegate {
private var bannerView: GADBannerView!
override func viewDidLoad() {
super.viewDidLoad()
setupBanner()
}
private func setupBanner() {
// 使用 AdMob 的 Ad Unit ID(在 AdMob 后台配置了 Meta Mediation 的广告单元)
bannerView = GADBannerView(adSize: GADAdSizeBanner) // 320×50
bannerView.adUnitID = "ca-app-pub-xxxxx/yyyyy" // ⬅️ AdMob Ad Unit ID
bannerView.rootViewController = self
bannerView.delegate = self
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
bannerView.load(GADRequest())
}
// MARK: - GADBannerViewDelegate
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
print("✅ Banner 加载成功")
// 可通过 bannerView.responseInfo 查看是哪个网络填充的
if let adNetworkClassName = bannerView.responseInfo?.loadedAdNetworkResponseInfo?.adNetworkClassName {
print(" 填充来源: \(adNetworkClassName)")
// 如果是 Meta 填充,会显示 GADMediationAdapterFacebook
}
}
func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
print("❌ Banner 加载失败: \(error.localizedDescription)")
}
func bannerViewDidRecordImpression(_ bannerView: GADBannerView) {
print("👁️ Banner 曝光")
}
func bannerViewDidRecordClick(_ bannerView: GADBannerView) {
print("👆 Banner 点击")
}
}
2.7 Interstitial 插屏广告(通过 AdMob 聚合)
import UIKit
import GoogleMobileAds
class InterstitialViewController: UIViewController, GADFullScreenContentDelegate {
private var interstitialAd: GADInterstitialAd?
override func viewDidLoad() {
super.viewDidLoad()
loadInterstitialAd()
}
/// 提前加载插屏广告
func loadInterstitialAd() {
GADInterstitialAd.load(
withAdUnitID: "ca-app-pub-xxxxx/yyyyy", // ⬅️ AdMob Ad Unit ID
request: GADRequest()
) { [weak self] ad, error in
if let error = error {
print("❌ 插屏广告加载失败: \(error.localizedDescription)")
return
}
print("✅ 插屏广告加载成功")
self?.interstitialAd = ad
self?.interstitialAd?.fullScreenContentDelegate = self
// 查看填充来源
if let adNetwork = ad?.responseInfo.loadedAdNetworkResponseInfo?.adNetworkClassName {
print(" 填充来源: \(adNetwork)")
}
}
}
/// 在合适时机展示
func showInterstitialAd() {
if let ad = interstitialAd {
ad.present(fromRootViewController: self)
} else {
print("⚠️ 广告尚未就绪")
}
}
// MARK: - GADFullScreenContentDelegate
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
print("❌ 展示失败: \(error.localizedDescription)")
loadInterstitialAd() // 重新加载
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("✅ 插屏广告已关闭")
loadInterstitialAd() // ⭐ 关闭后预加载下一个
}
func adDidRecordImpression(_ ad: GADFullScreenPresentingAd) {
print("👁️ 插屏广告曝光")
}
}
2.9 AdMob 后台配置 Meta AN Mediation(关键步骤)
在 AdMob 后台完成以下配置,才能让 Meta AN 参与竞价:
步骤 1:Meta 后台创建广告位
- 登录 Meta Business Suite → Monetization Manager
- 创建 Property → 选择 iOS 平台
- Mediation Platform 选择 "AdMob"
- 为每种格式(Banner / Interstitial / Rewarded)创建 Placement
- 记录每个 Placement ID(格式如
123456789_987654321)
步骤 2:AdMob 后台添加 Meta 竞价
- 登录 AdMob Console
- 导航到 Mediation → Mediation Groups
- 创建或编辑一个 Mediation Group
- 在 Bidding 区域,点击 Add Ad Sources → Meta Audience Network
- 输入 Meta 的 Placement ID
- 保存
💡 AdMob 会自动与 Meta 进行实时竞价(Bidding),不需要设置 eCPM 手动排序
步骤 3:配置 app-ads.txt
在您的开发者网站根目录添加 app-ads.txt 文件,包含 AdMob 和 Meta 的授权行:
# Google AdMob
google.com, pub-xxxxxxxxxxxxxxxx, DIRECT, f08c47fec0942fa0
# Meta Audience Network
facebook.com, xxxxxxxxxxxxxxxxx, RESELLER, c3e20eee3f780d68
三、方案二:AppLovin MAX 聚合(独立聚合平台)
核心思路: MAX 作为独立聚合,AdMob 和 Meta AN 同为竞价参与者,更加公平透明好的,已经获取到了所有需要的信息。以下是完整的后续内容:
3.1 CocoaPods 安装
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
inhibit_all_warnings!
# ① AppLovin MAX SDK(聚合主体)
pod 'AppLovinSDK'
# ② Google AdMob 适配器(自动包含 Google Mobile Ads SDK)
pod 'AppLovinMediationGoogleAdapter'
# ③ Meta Audience Network 适配器(自动包含 FBAudienceNetwork SDK)
pod 'AppLovinMediationFacebookAdapter'
end
pod install --repo-update
💡 只需安装适配器 Pod,它们会自动拉取对应的广告网络 SDK
3.2 Info.plist 配置
<!-- ① AppLovin SDK Key -->
<key>AppLovinSdkKey</key>
<string>YOUR_APPLOVIN_SDK_KEY</string>
<!-- ② AdMob App ID(Google Adapter 需要) -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
<!-- ③ ATT 权限描述 -->
<key>NSUserTrackingUsageDescription</key>
<string>此标识符将用于向您投放个性化广告</string>
<!-- ④ SKAdNetwork 标识符(AppLovin + Google + Meta 都需要) -->
<key>SKAdNetworkItems</key>
<array>
<!-- AppLovin -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ludvb6z3bs.skadnetwork</string>
</dict>
<!-- Google -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<!-- Meta -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<!-- ... 完整列表从各平台文档获取 -->
</array>
3.3 SDK 初始化
import UIKit
import AppLovinSDK
import FBAudienceNetwork
import AppTrackingTransparency
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ① 请求 ATT 权限(延迟到首页更好,此处简化演示)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.requestATTAndInitialize()
}
return true
}
private func requestATTAndInitialize() {
if #available(iOS 14.5, *) {
ATTrackingManager.requestTrackingAuthorization { [weak self] status in
DispatchQueue.main.async {
// ② 设置 Meta ATE 标志
switch status {
case .authorized:
FBAdSettings.setAdvertiserTrackingEnabled(true)
default:
FBAdSettings.setAdvertiserTrackingEnabled(false)
}
// ③ 初始化 AppLovin MAX SDK
self?.initializeMAX()
}
}
} else {
initializeMAX()
}
}
private func initializeMAX() {
// SDK Key 可在 AppLovin Dashboard → Account → General → Keys 找到
let initConfig = ALSdkInitializationConfiguration(sdkKey: "YOUR_SDK_KEY") { builder in
builder.mediationProvider = ALMediationProviderMAX
// (可选)如果需要测试特定广告单元
// builder.testDeviceAdvertisingIdentifiers = ["YOUR_IDFA"]
}
ALSdk.shared().initialize(with: initConfig) { sdkConfig in
print("✅ AppLovin MAX SDK 初始化完成")
// 此时可以开始加载广告
}
}
}
3.4 Banner 广告
import UIKit
import AppLovinSDK
class MAXBannerViewController: UIViewController, MAAdViewAdDelegate {
private var adView: MAAdView!
override func viewDidLoad() {
super.viewDidLoad()
createBannerAd()
}
private func createBannerAd() {
// Ad Unit ID 在 AppLovin Dashboard → MAX → Ad Units 创建
adView = MAAdView(adUnitIdentifier: "YOUR_AD_UNIT_ID")
adView.delegate = self
// Banner 尺寸:iPhone 50pt / iPad 90pt
let height: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 90 : 50
let width: CGFloat = UIScreen.main.bounds.width
adView.frame = CGRect(
x: 0,
y: view.bounds.height - height - view.safeAreaInsets.bottom,
width: width,
height: height
)
adView.backgroundColor = .clear
view.addSubview(adView)
// 加载广告(Banner 默认自动刷新)
adView.loadAd()
}
// MARK: - MAAdViewAdDelegate
func didLoad(_ ad: MAAd) {
print("✅ Banner 加载成功, 来源: \(ad.networkName)")
// ad.networkName 会显示 "Google Bidding and Google AdMob" 或 "Meta Audience Network"
}
func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
print("❌ Banner 加载失败: \(error.message)")
}
func didClick(_ ad: MAAd) {
print("👆 Banner 点击")
}
func didFail(toDisplay ad: MAAd, withError error: MAError) {
print("❌ Banner 展示失败")
}
func didExpand(_ ad: MAAd) {
print("📐 Banner 展开")
}
func didCollapse(_ ad: MAAd) {
print("📐 Banner 折叠")
}
deinit {
adView.delegate = nil
adView.removeFromSuperview()
}
}
3.5 Interstitial 插屏广告
import UIKit
import AppLovinSDK
class MAXInterstitialViewController: UIViewController, MAAdDelegate {
private var interstitialAd: MAInterstitialAd!
private var retryAttempt = 0
override func viewDidLoad() {
super.viewDidLoad()
createInterstitialAd()
}
private func createInterstitialAd() {
interstitialAd = MAInterstitialAd(adUnitIdentifier: "YOUR_AD_UNIT_ID")
interstitialAd.delegate = self
interstitialAd.load()
}
/// 在合适时机展示
func showInterstitialAd() {
if interstitialAd.isReady {
interstitialAd.show()
} else {
print("⚠️ 插屏广告尚未就绪")
}
}
// MARK: - MAAdDelegate
func didLoad(_ ad: MAAd) {
print("✅ 插屏加载成功, 来源: \(ad.networkName)")
retryAttempt = 0
}
func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
print("❌ 插屏加载失败: \(error.message)")
// ⭐ 指数退避重试(最大 64 秒)
retryAttempt += 1
let delaySec = pow(2.0, min(6.0, Double(retryAttempt)))
DispatchQueue.main.asyncAfter(deadline: .now() + delaySec) { [weak self] in
self?.interstitialAd.load()
}
}
func didDisplay(_ ad: MAAd) {
print("📺 插屏已展示")
}
func didHide(_ ad: MAAd) {
print("✅ 插屏已关闭")
// ⭐ 关闭后预加载下一个
interstitialAd.load()
}
func didClick(_ ad: MAAd) {
print("👆 插屏被点击")
}
func didFail(toDisplay ad: MAAd, withError error: MAError) {
print("❌ 插屏展示失败")
interstitialAd.load()
}
}
3.6 Rewarded 激励视频广告
import UIKit
import AppLovinSDK
class MAXRewardedViewController: UIViewController, MARewardedAdDelegate {
private var rewardedAd: MARewardedAd!
private var retryAttempt = 0
override func viewDidLoad() {
super.viewDidLoad()
createRewardedAd()
}
private func createRewardedAd() {
rewardedAd = MARewardedAd.shared(withAdUnitIdentifier: "YOUR_AD_UNIT_ID")
rewardedAd.delegate = self
rewardedAd.load()
}
/// 用户主动触发观看
@IBAction func watchAdTapped(_ sender: UIButton) {
if rewardedAd.isReady {
rewardedAd.show()
} else {
print("⚠️ 激励视频尚未就绪")
}
}
// MARK: - MAAdDelegate
func didLoad(_ ad: MAAd) {
print("✅ 激励视频加载成功, 来源: \(ad.networkName)")
retryAttempt = 0
}
func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
print("❌ 激励视频加载失败: \(error.message)")
retryAttempt += 1
let delaySec = pow(2.0, min(6.0, Double(retryAttempt)))
DispatchQueue.main.asyncAfter(deadline: .now() + delaySec) { [weak self] in
self?.rewardedAd.load()
}
}
func didDisplay(_ ad: MAAd) {
print("📺 激励视频已展示")
}
func didHide(_ ad: MAAd) {
print("✅ 激励视频已关闭")
rewardedAd.load() // ⭐ 预加载下一个
}
func didClick(_ ad: MAAd) {
print("👆 激励视频被点击")
}
func didFail(toDisplay ad: MAAd, withError error: MAError) {
print("❌ 激励视频展示失败")
rewardedAd.load()
}
// MARK: - MARewardedAdDelegate
/// ⭐ 用户观看完成,发放奖励
func didRewardUser(for ad: MAAd, with reward: MAReward) {
print("🎉 用户获得奖励: \(reward.amount) \(reward.label)")
grantReward(amount: reward.amount, currency: reward.label)
}
private func grantReward(amount: Int, currency: String) {
// 发放奖励逻辑
print("发放 \(amount) \(currency)")
}
}
3.7 AppLovin MAX 后台配置
在 AppLovin Dashboard 中完成以下配置:
添加 AdMob 和 Meta AN
- MAX → Manage → Ad Units → 创建 Ad Unit
- 选择格式(Banner / Interstitial / Rewarded)
- 在 Bidding 区域启用:
- Google Bidding and Google AdMob → 填入 AdMob 的 Ad Unit ID
- Meta Audience Network → 填入 Meta 的 Placement ID
- 保存
两个网络都通过 实时竞价(Bidding) 参与,MAX 会自动选择出价最高的网络展示广告
四、两种方案对比
| 特性 | 方案一:AdMob Mediation | 方案二:AppLovin MAX |
|---|---|---|
| 聚合主体 | Google AdMob | AppLovin MAX |
| 竞价公平性 | AdMob 自家广告可能有优势 | 更公平透明,所有网络平等竞争 |
| 接入复杂度 | ⭐ 简单(已用 AdMob 的项目) | ⭐⭐ 中等(需额外注册 AppLovin) |
| 支持网络数量 | 约 20+ | 约 25+ |
| 收益报告 | AdMob 后台 | AppLovin Dashboard(更详细) |
| A/B 测试 | 有限 | 内置强大 A/B 测试 |
| 广告质量审核 | Google Ad Review | MAX Ad Review |
| 费用 | 免费 | 免费 |
| 推荐场景 | 已深度使用 AdMob | 新项目或追求最高收益 |
五、方案三:手动管理(不推荐但可行)
如果你有特殊原因不想使用聚合平台,可以手动管理两个 SDK 的降级逻辑:
5.1 安装两个 SDK
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
pod 'Google-Mobile-Ads-SDK' # AdMob
pod 'FBAudienceNetwork' # Meta AN
end
5.2 手动广告管理器
import Foundation
import GoogleMobileAds
import FBAudienceNetwork
/// 广告管理器 - 手动聚合(降级逻辑)
/// ⚠️ 不推荐:仅作学习参考,生产环境请用聚合平台
class ManualAdManager: NSObject {
static let shared = ManualAdManager()
// MARK: - 配置
private let admobInterstitialUnitID = "ca-app-pub-xxxxx/yyyyy"
private let metaInterstitialPlacementID = "123456789_987654321"
private let admobRewardedUnitID = "ca-app-pub-xxxxx/zzzzz"
private let metaRewardedPlacementID = "123456789_111111111"
// MARK: - 广告实例
private var admobInterstitial: GADInterstitialAd?
private var metaInterstitial: FBInterstitialAd?
private var admobRewarded: GADRewardedAd?
private var metaRewarded: FBRewardedVideoAd?
// MARK: - 状态追踪
private var isAdMobInterstitialReady = false
private var isMetaInterstitialReady = false
private var isAdMobRewardedReady = false
private var isMetaRewardedReady = false
// MARK: - 回调
var onRewardEarned: ((_ amount: Int, _ type: String) -> Void)?
var onInterstitialDismissed: (() -> Void)?
private override init() {
super.init()
}
// MARK: - ==================== 插屏广告 ====================
/// 同时请求两个网络,谁先 ready 谁展示
func loadInterstitial() {
isAdMobInterstitialReady = false
isMetaInterstitialReady = false
loadAdMobInterstitial()
loadMetaInterstitial()
}
// —— AdMob 插屏 ——
private func loadAdMobInterstitial() {
GADInterstitialAd.load(
withAdUnitID: admobInterstitialUnitID,
request: GADRequest()
) { [weak self] ad, error in
guard let self = self else { return }
if let error = error {
print("❌ AdMob 插屏加载失败: \(error.localizedDescription)")
return
}
print("✅ AdMob 插屏加载成功")
self.admobInterstitial = ad
self.admobInterstitial?.fullScreenContentDelegate = self
self.isAdMobInterstitialReady = true
}
}
// —— Meta 插屏 ——
private func loadMetaInterstitial() {
metaInterstitial = FBInterstitialAd(placementID: metaInterstitialPlacementID)
metaInterstitial?.delegate = self
metaInterstitial?.load()
}
/// 展示插屏:优先 AdMob → 降级 Meta → 两者都无则放弃
func showInterstitial(from viewController: UIViewController) -> Bool {
if isAdMobInterstitialReady, let ad = admobInterstitial {
print("📺 展示 AdMob 插屏")
ad.present(fromRootViewController: viewController)
return true
} else if isMetaInterstitialReady, let ad = metaInterstitial, ad.isAdValid {
print("📺 展示 Meta 插屏")
ad.show(fromRootViewController: viewController)
return true
} else {
print("⚠️ 无可用插屏广告")
return false
}
}
// MARK: - ==================== 激励视频 ====================
func loadRewarded() {
isAdMobRewardedReady = false
isMetaRewardedReady = false
loadAdMobRewarded()
loadMetaRewarded()
}
// —— AdMob 激励 ——
private func loadAdMobRewarded() {
GADRewardedAd.load(
withAdUnitID: admobRewardedUnitID,
request: GADRequest()
) { [weak self] ad, error in
guard let self = self else { return }
if let error = error {
print("❌ AdMob 激励加载失败: \(error.localizedDescription)")
return
}
print("✅ AdMob 激励加载成功")
self.admobRewarded = ad
self.admobRewarded?.fullScreenContentDelegate = self
self.isAdMobRewardedReady = true
}
}
// —— Meta 激励 ——
private func loadMetaRewarded() {
metaRewarded = FBRewardedVideoAd(placementID: metaRewardedPlacementID)
metaRewarded?.delegate = self
metaRewarded?.load()
}
/// 展示激励视频:优先 AdMob → 降级 Meta
func showRewarded(from viewController: UIViewController) -> Bool {
if isAdMobRewardedReady, let ad = admobRewarded {
print("📺 展示 AdMob 激励视频")
ad.present(fromRootViewController: viewController) { [weak self] in
let reward = ad.adReward
print("🎉 AdMob 奖励: \(reward.amount) \(reward.type)")
self?.onRewardEarned?(reward.amount.intValue, reward.type)
}
return true
} else if isMetaRewardedReady, let ad = metaRewarded, ad.isAdValid {
print("📺 展示 Meta 激励视频")
ad.show(fromRootViewController: viewController)
return true
} else {
print("⚠️ 无可用激励视频")
return false
}
}
/// 检查是否有广告就绪
var isInterstitialReady: Bool {
return isAdMobInterstitialReady || isMetaInterstitialReady
}
var isRewardedReady: Bool {
return isAdMobRewardedReady || isMetaRewardedReady
}
}
// MARK: - ==================== AdMob Delegate ====================
extension ManualAdManager: GADFullScreenContentDelegate {
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
print("✅ AdMob 全屏广告已关闭")
isAdMobInterstitialReady = false
isAdMobRewardedReady = false
onInterstitialDismissed?()
// 预加载下一个
loadInterstitial()
loadRewarded()
}
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
print("❌ AdMob 展示失败: \(error.localizedDescription)")
}
}
// MARK: - ==================== Meta Interstitial Delegate ====================
extension ManualAdManager: FBInterstitialAdDelegate {
func interstitialAdDidLoad(_ interstitialAd: FBInterstitialAd) {
print("✅ Meta 插屏加载成功")
isMetaInterstitialReady = true
}
func interstitialAd(_ interstitialAd: FBInterstitialAd, didFailWithError error: Error) {
print("❌ Meta 插屏加载失败: \(error.localizedDescription)")
isMetaInterstitialReady = false
}
func interstitialAdDidClose(_ interstitialAd: FBInterstitialAd) {
print("✅ Meta 插屏已关闭")
isMetaInterstitialReady = false
onInterstitialDismissed?()
loadInterstitial()
}
func interstitialAdDidClick(_ interstitialAd: FBInterstitialAd) {
print("👆 Meta 插屏被点击")
}
func interstitialAdWillLogImpression(_ interstitialAd: FBInterstitialAd) {
print("👁️ Meta 插屏曝光")
}
}
// MARK: - ==================== Meta Rewarded Delegate ====================
extension ManualAdManager: FBRewardedVideoAdDelegate {
func rewardedVideoAdDidLoad(_ rewardedVideoAd: FBRewardedVideoAd) {
print("✅ Meta 激励加载成功")
isMetaRewardedReady = true
}
func rewardedVideoAd(_ rewardedVideoAd: FBRewardedVideoAd, didFailWithError error: Error) {
print("❌ Meta 激励加载失败: \(error.localizedDescription)")
isMetaRewardedReady = false
}
func rewardedVideoAdDidClose(_ rewardedVideoAd: FBRewardedVideoAd) {
print("✅ Meta 激励视频已关闭")
isMetaRewardedReady = false
loadRewarded()
}
func rewardedVideoAdVideoComplete(_ rewardedVideoAd: FBRewardedVideoAd) {
print("🎉 Meta 激励视频观看完成")
// Meta 不像 AdMob 那样返回具体奖励信息,需要自行定义
onRewardEarned?(1, "coin")
}
func rewardedVideoAdDidClick(_ rewardedVideoAd: FBRewardedVideoAd) {
print("👆 Meta 激励视频被点击")
}
}
5.3 手动方案的使用方式
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 预加载广告
ManualAdManager.shared.loadInterstitial()
ManualAdManager.shared.loadRewarded()
// 设置奖励回调
ManualAdManager.shared.onRewardEarned = { amount, type in
print("🎉 发放奖励: \(amount) \(type)")
// 更新用户余额等
}
}
/// 关卡结束后展示插屏
func onLevelComplete() {
_ = ManualAdManager.shared.showInterstitial(from: self)
}
/// 用户主动观看激励视频
@IBAction func watchAdForReward(_ sender: UIButton) {
let shown = ManualAdManager.shared.showRewarded(from: self)
if !shown {
// 提示用户稍后再试
showAlert(message: "广告暂不可用,请稍后再试")
}
}
}
⚠️ 手动方案的缺点:
- 无法实现真正的实时竞价(Bidding),只是简单的优先级降级
- 需要自己维护两套 Delegate
- 无法动态调整优先级和 eCPM 排序
- 合规(GDPR/CCPA)需要分别处理
- 新增广告网络时需要大量改代码
六、隐私合规处理(三种方案通用)
6.1 Google UMP(User Messaging Platform)- GDPR 合规
import UIKit
import UserMessagingPlatform
class ConsentManager {
static let shared = ConsentManager()
/// 在 SDK 初始化之前调用
func requestConsentIfNeeded(from viewController: UIViewController, completion: @escaping () -> Void) {
// ① 创建请求参数
let parameters = UMPRequestParameters()
// 调试时使用(正式发布移除)
#if DEBUG
let debugSettings = UMPDebugSettings()
debugSettings.testDeviceIdentifiers = ["YOUR_TEST_DEVICE_HASHED_ID"]
debugSettings.geography = .EEA // 模拟欧洲用户
parameters.debugSettings = debugSettings
#endif
// ② 请求更新同意信息
UMPConsentInformation.sharedInstance.requestConsentInfoUpdate(with: parameters) { error in
if let error = error {
print("❌ 同意信息更新失败: \(error.localizedDescription)")
completion()
return
}
// ③ 如果需要,展示同意表单
UMPConsentForm.loadAndPresentIfRequired(from: viewController) { formError in
if let formError = formError {
print("❌ 同意表单展示失败: \(formError.localizedDescription)")
}
// ④ 检查是否可以请求广告
if UMPConsentInformation.sharedInstance.canRequestAds {
print("✅ 用户已授权,可以请求广告")
}
completion()
}
}
}
/// 检查是否可以请求个性化广告
var canRequestAds: Bool {
return UMPConsentInformation.sharedInstance.canRequestAds
}
}
6.2 Meta 隐私合规设置
import FBAudienceNetwork
class MetaPrivacyHelper {
/// 设置 GDPR 数据处理选项(欧洲用户)
static func setGDPRConsent(granted: Bool) {
// Meta 不在 IAB GVL 中,需要使用 Additional Consent
// 如果用户未同意,应当限制数据使用
if !granted {
// 限制数据处理
FBAdSettings.setDataProcessingOptions(["LDU"], country: 0, state: 0)
} else {
// 不限制
FBAdSettings.setDataProcessingOptions([])
}
}
/// 设置 CCPA 数据处理选项(加州用户)
static func setCCPAOptOut(optedOut: Bool) {
if optedOut {
// 用户选择退出数据售卖
FBAdSettings.setDataProcessingOptions(["LDU"], country: 1, state: 1000)
} else {
FBAdSettings.setDataProcessingOptions([])
}
}
/// 设置 iOS 14+ 广告追踪状态
static func setAdvertiserTracking(enabled: Bool) {
FBAdSettings.setAdvertiserTrackingEnabled(enabled)
}
}
6.3 完整的初始化流程(合规 → ATT → 广告 SDK)
import UIKit
import GoogleMobileAds
import FBAudienceNetwork
import AppTrackingTransparency
import UserMessagingPlatform
class AppStartupManager {
static let shared = AppStartupManager()
private var isAdsInitialized = false
/// 完整的广告初始化流程:GDPR → ATT → Meta ATE → SDK 初始化
func startAdInitialization(from viewController: UIViewController) {
// ==================== 第 1 步:GDPR 同意 ====================
print("📋 Step 1: 请求 GDPR 同意...")
ConsentManager.shared.requestConsentIfNeeded(from: viewController) { [weak self] in
guard let self = self else { return }
// ==================== 第 2 步:ATT 权限 ====================
print("📋 Step 2: 请求 ATT 权限...")
self.requestATTPermission { trackingAuthorized in
// ==================== 第 3 步:配置 Meta 隐私 ====================
print("📋 Step 3: 配置 Meta 隐私设置...")
FBAdSettings.setAdvertiserTrackingEnabled(trackingAuthorized)
// 如果 GDPR 同意信息可用,配置 Meta 数据处理选项
if ConsentManager.shared.canRequestAds {
FBAdSettings.setDataProcessingOptions([])
} else {
FBAdSettings.setDataProcessingOptions(["LDU"], country: 0, state: 0)
}
// ==================== 第 4 步:初始化广告 SDK ====================
print("📋 Step 4: 初始化广告 SDK...")
self.initializeAdSDK()
}
}
}
private func requestATTPermission(completion: @escaping (Bool) -> Void) {
if #available(iOS 14.5, *) {
ATTrackingManager.requestTrackingAuthorization { status in
DispatchQueue.main.async {
let authorized = (status == .authorized)
print(" ATT 状态: \(status.rawValue), 已授权: \(authorized)")
completion(authorized)
}
}
} else {
// iOS 14.5 以下默认可追踪
completion(true)
}
}
private func initializeAdSDK() {
guard !isAdsInitialized else { return }
isAdsInitialized = true
// ====== 方案一:使用 AdMob Mediation ======
GADMobileAds.sharedInstance().start { status in
print("✅ AdMob SDK 初始化完成")
for (adapter, adapterStatus) in status.adapterStatusesByClassName {
print(" [\(adapter)] state=\(adapterStatus.state.rawValue), \(adapterStatus.description)")
}
// 初始化完成,发送通知让各页面开始加载广告
NotificationCenter.default.post(name: .adsSDKInitialized, object: nil)
}
// ====== 方案二(替代):使用 AppLovin MAX ======
/*
let initConfig = ALSdkInitializationConfiguration(sdkKey: "YOUR_SDK_KEY") { builder in
builder.mediationProvider = ALMediationProviderMAX
}
ALSdk.shared().initialize(with: initConfig) { sdkConfig in
print("✅ AppLovin MAX SDK 初始化完成")
NotificationCenter.default.post(name: .adsSDKInitialized, object: nil)
}
*/
}
}
// MARK: - 自定义通知名
extension Notification.Name {
static let adsSDKInitialized = Notification.Name("adsSDKInitialized")
}
6.4 在 AppDelegate / SceneDelegate 中调用
// SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
let rootVC = MainViewController()
window.rootViewController = UINavigationController(rootViewController: rootVC)
window.makeKeyAndVisible()
self.window = window
}
}
// MainViewController.swift
class MainViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// ⭐ 在主页面显示后启动广告初始化流程
// 这样 GDPR 弹窗和 ATT 弹窗能正常展示
AppStartupManager.shared.startAdInitialization(from: self)
// 监听 SDK 初始化完成
NotificationCenter.default.addObserver(
self,
selector: #selector(onAdsReady),
name: .adsSDKInitialized,
object: nil
)
}
@objc private func onAdsReady() {
print("🚀 广告 SDK 已就绪,开始加载广告")
// 在这里加载各种广告
}
}
七、测试和调试
7.1 AdMob 测试广告 ID
在开发阶段,使用 Google 提供的官方测试 ID,不要使用真实广告 ID 测试(会被封号):
struct TestAdUnitIDs {
// Google 官方测试 ID(安全使用,不会触发违规)
static let admobBanner = "ca-app-pub-3940256099942544/2934735716"
static let admobInterstitial = "ca-app-pub-3940256099942544/4411468910"
static let admobRewarded = "ca-app-pub-3940256099942544/1712485313"
static let admobRewardedInterstitial = "ca-app-pub-3940256099942544/6978759866"
static let admobNative = "ca-app-pub-3940256099942544/3986624511"
static let admobAppOpen = "ca-app-pub-3940256099942544/5575463023"
}
7.2 Meta AN 测试模式
#if DEBUG
// 添加测试设备(设备 IDFA 的哈希值,在控制台日志中查找)
FBAdSettings.addTestDevice("YOUR_DEVICE_HASH")
// 或者启用模拟器测试模式
FBAdSettings.addTestDevice(FBAdSettings.testDeviceHash())
// 设置测试广告类型(可选)
// FBAdSettings.setLogLevel(.log)
#endif
7.3 AppLovin MAX 调试工具
#if DEBUG
// 显示 MAX Mediation Debugger(可视化调试面板)
// 显示所有适配器状态、广告加载记录等
ALSdk.shared().showMediationDebugger()
#endif
💡 MAX Mediation Debugger 非常强大,可以一目了然看到:
- 各适配器是否正确初始化
- 各网络的竞价情况
- 广告加载成功/失败详情
7.4 广告来源追踪(通用)
/// 统一的广告事件追踪器
class AdEventTracker {
/// 记录广告展示来源
static func trackImpression(
adFormat: String, // "banner" / "interstitial" / "rewarded"
networkName: String, // "AdMob" / "Meta" / "Google Bidding"
revenue: Double? = nil, // 收益(如可用)
adUnitID: String
) {
print("""
📊 广告曝光
格式: \(adFormat)
来源: \(networkName)
收益: \(revenue.map { String(format: "%.6f", $0) } ?? "N/A")
广告单元: \(adUnitID)
""")
// 发送到你的分析平台(Firebase / Amplitude / 自建等)
// Analytics.logEvent("ad_impression", parameters: [...])
}
// —— AdMob 获取收益信息 ——
static func trackAdMobRevenue(ad: GADFullScreenPresentingAd, adFormat: String) {
// AdMob 收益追踪需要通过 paidEventHandler
// 在加载成功后设置:
// ad.paidEventHandler = { value in
// let revenue = value.value.doubleValue / 1_000_000 // 微单位转换
// trackImpression(adFormat: adFormat, networkName: "AdMob", revenue: revenue, adUnitID: "xxx")
// }
}
// —— MAX 获取收益信息 ——
static func trackMAXRevenue(ad: MAAd, adFormat: String) {
let revenue = ad.revenue // MAX 直接提供收益值
let networkName = ad.networkName
trackImpression(
adFormat: adFormat,
networkName: networkName,
revenue: revenue,
adUnitID: ad.adUnitIdentifier
)
}
}
7.5 常见问题排查清单
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| Meta AN 始终 No Fill | 未通过 Meta 审核 / Placement ID 错误 | 确认 App 已在 Meta Business 审核通过 |
| AdMob Adapter 未初始化 | GADApplicationIdentifier 未配置 | 检查 Info.plist |
| ATT 弹窗不出现 | 在 viewDidLoad 中调用太早 | 改到 viewDidAppear 中调用 |
| 收益极低 | 仅一个网络参与竞争 | 接入更多网络(Bidding 竞争越多收益越高) |
| 测试时展示真实广告 | 未添加测试设备 | 使用测试 ID 或添加测试设备 |
崩溃:GADApplicationIdentifier | AdMob App ID 格式错误 | 格式应为 ca-app-pub-xxxx~yyyy |
| Meta SDK 初始化失败 | iOS Deployment Target < 13.0 | 升级最低版本到 13.0 |
| MAX Debugger 显示红色 | 适配器版本不兼容 | 更新所有 Pod 到最新版本 |
八、收益优化最佳实践
8.1 广告展示策略
/// 广告频次控制器
class AdFrequencyManager {
static let shared = AdFrequencyManager()
// 配置
private let interstitialMinInterval: TimeInterval = 60 // 插屏最少间隔 60 秒
private let maxInterstitialsPerSession = 10 // 每次会话最多 10 个插屏
private let rewardedCooldown: TimeInterval = 30 // 激励视频冷却 30 秒
// 状态
private var lastInterstitialTime: Date?
private var sessionInterstitialCount = 0
private var lastRewardedTime: Date?
/// 检查是否可以展示插屏
func canShowInterstitial() -> Bool {
// 检查频率限制
if let lastTime = lastInterstitialTime {
let elapsed = Date().timeIntervalSince(lastTime)
if elapsed < interstitialMinInterval {
print("⏳ 插屏冷却中,还需 \(Int(interstitialMinInterval - elapsed)) 秒")
return false
}
}
// 检查会话上限
if sessionInterstitialCount >= maxInterstitialsPerSession {
print("🚫 已达到本次会话插屏上限")
return false
}
return true
}
/// 记录插屏已展示
func recordInterstitialShown() {
lastInterstitialTime = Date()
sessionInterstitialCount += 1
}
/// 检查是否可以展示激励视频
func canShowRewarded() -> Bool {
if let lastTime = lastRewardedTime {
let elapsed = Date().timeIntervalSince(lastTime)
if elapsed < rewardedCooldown {
return false
}
}
return true
}
/// 记录激励视频已展示
func recordRewardedShown() {
lastRewardedTime = Date()
}
/// 重置会话计数(App 启动或从后台恢复时调用)
func resetSession() {
sessionInterstitialCount = 0
}
}
8.2 收益优化清单
| 优化项 | 说明 | 预期效果 |
|---|---|---|
| 接入 3+ 个 Bidding 网络 | 竞争越多出价越高 | eCPM 提升 20~50% |
| 使用实时竞价 | 优于传统 Waterfall | eCPM 提升 10~30% |
| 合理控制频次 | 避免用户疲劳和政策违规 | 长期收益稳定 |
| 预加载广告 | 关闭后立即预加载下一个 | 填充率接近 100% |
| ATT 优化弹窗文案 | 提高授权率 → 个性化广告收益更高 | eCPM 提升 15~30% |
| Banner 自适应尺寸 | 使用 Adaptive Banner 替代固定尺寸 | eCPM 提升 10~20% |
| 定期更新 SDK | 各网络持续优化竞价算法 | 持续收益改善 |
九、项目文件结构建议
YourApp/
├── Podfile
├── Info.plist
├── AppDelegate.swift
├── SceneDelegate.swift
│
├── Ads/
│ ├── Core/
│ │ ├── AppStartupManager.swift // 完整初始化流程(GDPR→ATT→SDK)
│ │ ├── ConsentManager.swift // GDPR / UMP 同意管理
│ │ ├── MetaPrivacyHelper.swift // Meta 隐私合规
│ │ ├── AdFrequencyManager.swift // 广告频次控制
│ │ └── AdEventTracker.swift // 收益/事件追踪
│ │
│ ├── AdMobMediation/ // 方案一:AdMob Mediation
│ │ ├── AdMobBannerManager.swift
│ │ ├── AdMobInterstitialManager.swift
│ │ └── AdMobRewardedManager.swift
│ │
│ ├── MAXMediation/ // 方案二:AppLovin MAX
│ │ ├── MAXBannerManager.swift
│ │ ├── MAXInterstitialManager.swift
│ │ └── MAXRewardedManager.swift
│ │
│ └── Manual/ // 方案三(不推荐)
│ └── ManualAdManager.swift
│
├── Config/
│ ├── AdConfig.swift // 广告 ID 配置(开发/生产)
│ └── TestAdUnitIDs.swift // 测试广告 ID
│
├── Views/
│ └── ...
└── ViewControllers/
└── ...
9.1 广告配置文件(开发/生产切换)
// AdConfig.swift
import Foundation
struct AdConfig {
// MARK: - 环境切换
#if DEBUG
static let isTestMode = true
#else
static let isTestMode = false
#endif
// MARK: - AdMob 配置
struct AdMob {
static var bannerID: String {
isTestMode
? "ca-app-pub-3940256099942544/2934735716" // 测试
: "ca-app-pub-YOUR_REAL_PUB_ID/BANNER_ID" // 生产
}
static var interstitialID: String {
isTestMode
? "ca-app-pub-3940256099942544/4411468910"
: "ca-app-pub-YOUR_REAL_PUB_ID/INTERSTITIAL_ID"
}
static var rewardedID: String {
isTestMode
? "ca-app-pub-3940256099942544/1712485313"
: "ca-app-pub-YOUR_REAL_PUB_ID/REWARDED_ID"
}
}
// MARK: - Meta AN 配置
struct Meta {
static var bannerPlacementID: String {
isTestMode
? "IMG_16_9_APP_INSTALL#YOUR_PLACEMENT_ID" // 测试
: "YOUR_REAL_PLACEMENT_ID" // 生产
}
static var interstitialPlacementID: String {
isTestMode
? "IMG_16_9_APP_INSTALL#YOUR_PLACEMENT_ID"
: "YOUR_REAL_PLACEMENT_ID"
}
static var rewardedPlacementID: String {
isTestMode
? "VID_HD_16_9_46S_APP_INSTALL#YOUR_PLACEMENT_ID"
: "YOUR_REAL_PLACEMENT_ID"
}
}
// MARK: - AppLovin MAX 配置
struct MAX {
static let sdkKey = "YOUR_APPLOVIN_SDK_KEY"
// MAX Ad Unit ID(在 AppLovin Dashboard 创建)
static let bannerAdUnitID = "YOUR_MAX_BANNER_UNIT"
static let interstitialAdUnitID = "YOUR_MAX_INTERSTITIAL_UNIT"
static let rewardedAdUnitID = "YOUR_MAX_REWARDED_UNIT"
}
}
十、SwiftUI 集成(额外补充)
如果你的项目使用 SwiftUI,以下是适配方式:
10.1 AdMob Banner 的 SwiftUI 封装
import SwiftUI
import GoogleMobileAds
struct AdMobBannerView: UIViewRepresentable {
let adUnitID: String
func makeUIView(context: Context) -> GADBannerView {
let bannerView = GADBannerView(adSize: GADAdSizeBanner)
bannerView.adUnitID = adUnitID
bannerView.delegate = context.coordinator
// 延迟获取 rootViewController(SwiftUI 环境需要这样做)
DispatchQueue.main.async {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
bannerView.rootViewController = rootVC
bannerView.load(GADRequest())
}
}
return bannerView
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, GADBannerViewDelegate {
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
print("✅ [SwiftUI] Banner 加载成功")
}
func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
print("❌ [SwiftUI] Banner 加载失败: \(error.localizedDescription)")
}
}
}
10.2 在 SwiftUI View 中使用
import SwiftUI
struct GameView: View {
@StateObject private var adViewModel = AdViewModel()
var body: some View {
VStack {
// 游戏内容
Text("Your Game Content")
.frame(maxWidth: .infinity, maxHeight: .infinity)
// 底部 Banner 广告
AdMobBannerView(adUnitID: AdConfig.AdMob.bannerID)
.frame(height: 50)
}
.onAppear {
adViewModel.loadInterstitial()
adViewModel.loadRewarded()
}
}
}
// MARK: - 广告 ViewModel
class AdViewModel: ObservableObject {
@Published var isInterstitialReady = false
@Published var isRewardedReady = false
private var interstitialAd: GADInterstitialAd?
private var rewardedAd: GADRewardedAd?
func loadInterstitial() {
GADInterstitialAd.load(
withAdUnitID: AdConfig.AdMob.interstitialID,
request: GADRequest()
) { [weak self] ad, error in
if let ad = ad {
self?.interstitialAd = ad
self?.isInterstitialReady = true
}
}
}
func showInterstitial() {
guard isInterstitialReady,
let ad = interstitialAd,
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return
}
ad.present(fromRootViewController: rootVC)
isInterstitialReady = false
// 展示后重新加载
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.loadInterstitial()
}
}
func loadRewarded() {
GADRewardedAd.load(
withAdUnitID: AdConfig.AdMob.rewardedID,
request: GADRequest()
) { [weak self] ad, error in
if let ad = ad {
self?.rewardedAd = ad
self?.isRewardedReady = true
}
}
}
func showRewarded(onReward: @escaping (Int, String) -> Void) {
guard isRewardedReady,
let ad = rewardedAd,
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return
}
ad.present(fromRootViewController: rootVC) {
let reward = ad.adReward
onReward(reward.amount.intValue, reward.type)
}
isRewardedReady = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.loadRewarded()
}
}
}
10.3 AppLovin MAX 的 SwiftUI 封装
import SwiftUI
import AppLovinSDK
struct MAXBannerSwiftUIView: UIViewRepresentable {
let adUnitID: String
func makeUIView(context: Context) -> MAAdView {
let adView = MAAdView(adUnitIdentifier: adUnitID)
adView.delegate = context.coordinator
adView.backgroundColor = .clear
adView.loadAd()
return adView
}
func updateUIView(_ uiView: MAAdView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, MAAdViewAdDelegate {
func didLoad(_ ad: MAAd) {
print("✅ [SwiftUI] MAX Banner 加载成功, 来源: \(ad.networkName)")
}
func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
print("❌ [SwiftUI] MAX Banner 加载失败: \(error.message)")
}
func didClick(_ ad: MAAd) {}
func didFail(toDisplay ad: MAAd, withError error: MAError) {}
func didExpand(_ ad: MAAd) {}
func didCollapse(_ ad: MAAd) {}
}
}
// 使用方式
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
.frame(maxHeight: .infinity)
MAXBannerSwiftUIView(adUnitID: AdConfig.MAX.bannerAdUnitID)
.frame(height: 50)
}
}
}
十一、完整的 Podfile 汇总
根据你选择的方案,使用对应的 Podfile:
方案一:AdMob Mediation(推荐快速上手)
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
# AdMob SDK(聚合主体)
pod 'Google-Mobile-Ads-SDK', '~> 12.0'
# Meta Audience Network Mediation 适配器
pod 'GoogleMobileAdsMediationFacebook'
# GDPR 合规
pod 'GoogleUserMessagingPlatform'
# (可选)更多网络
# pod 'GoogleMobileAdsMediationAppLovin'
# pod 'GoogleMobileAdsMediationUnity'
end
方案二:AppLovin MAX(推荐追求高收益)
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
inhibit_all_warnings!
# AppLovin MAX SDK(聚合主体)
pod 'AppLovinSDK'
# AdMob 适配器
pod 'AppLovinMediationGoogleAdapter'
# Meta AN 适配器
pod 'AppLovinMediationFacebookAdapter'
# (可选)更多网络 - 接入越多竞争越激烈收益越高
# pod 'AppLovinMediationUnityAdsAdapter'
# pod 'AppLovinMediationMintegralAdapter'
# pod 'AppLovinMediationVungleAdapter'
# pod 'AppLovinMediationIronSourceAdapter'
# pod 'AppLovinMediationByteDanceAdapter' # Pangle / TikTok
# pod 'AppLovinMediationChartboostAdapter'
end
方案三:手动管理(不推荐)
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
pod 'Google-Mobile-Ads-SDK', '~> 12.0'
pod 'FBAudienceNetwork'
pod 'GoogleUserMessagingPlatform'
end
十二、总结与推荐
最终推荐
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新项目 / 追求最高收益 | ⭐ AppLovin MAX | 公平竞价、更多网络、详细报告 |
| 已有 AdMob 基础 / 快速接入 | ⭐ AdMob Mediation | 改动最小,生态成熟 |
| 学习了解原理 | 手动管理 | 仅作学习参考 |
关键要点回顾
- 一定要使用聚合平台,不要手动管理多个 SDK
- 优先使用 Bidding(实时竞价) 而非 Waterfall(瀑布流)
- 接入 3 个以上竞价网络,竞争越多收益越高
- 隐私合规三步走:GDPR 同意 → ATT 授权 → 各 SDK 设置
- 使用测试 ID 开发,上线前切换为生产 ID
- 预加载策略:广告关闭后立即预加载下一个
- 频次控制:避免过度展示导致用户流失或政策违规
- 定期更新 SDK:各广告网络持续优化,新版本通常带来更高收益
预期收益参考(仅供参考,受地区/品类/用户质量影响极大)
| 广告格式 | 美国市场 eCPM 参考 | 中国/亚洲市场 eCPM 参考 |
|---|---|---|
| Banner | 3.0 | 1.0 |
| Interstitial | 20.0 | 8.0 |
| Rewarded Video | 40.0 | 15.0 |
| MREC | 5.0 | 2.0 |
| App Open | 15.0 | 6.0 |
⚠️ 以上数据仅为行业大致参考范围。实际 eCPM 受以下因素影响极大:
- 用户地区(T1 国家如美/英/澳/加远高于其他地区)
- App 品类(金融、教育类 > 工具类 > 游戏休闲类)
- 用户质量(高留存用户 eCPM 更高)
- 接入网络数量(3+ 个 Bidding 网络可提升 20~50%)
- ATT 授权率(授权用户 eCPM 可比未授权高 30~80%)
十三、App Store 审核注意事项
13.1 App 隐私标签(Privacy Nutrition Labels)
上架 App Store 时,需要在 App Store Connect 中如实填写隐私标签。接入广告 SDK 后,通常需要声明以下数据收集:
| 数据类型 | 是否收集 | 用途 | 是否关联用户 |
|---|---|---|---|
| 设备标识符 (IDFA) | ✅ | 第三方广告、分析 | 是(如用户授权 ATT) |
| 粗略位置 | ✅ | 第三方广告 | 否 |
| 使用数据(产品交互) | ✅ | 第三方广告、分析 | 否 |
| 诊断数据 | ✅ | 分析 | 否 |
| 广告数据 | ✅ | 第三方广告 | 是 |
💡 各 SDK 的隐私声明文档:
13.2 审核常见被拒原因及解决
| 被拒原因 | 描述 | 解决方案 |
|---|---|---|
| Guideline 5.1.1 | ATT 弹窗描述不清或存在误导 | 使用清晰、诚实的 NSUserTrackingUsageDescription 文案 |
| Guideline 5.1.2 | 隐私标签与实际不符 | 根据所有接入 SDK 如实更新隐私标签 |
| Guideline 2.3.2 | 广告遮挡 UI 或影响功能 | 确保 Banner 不遮挡按钮;插屏在合理时机展示 |
| Guideline 3.1.1 | 激励视频绕过内购 | 激励视频只能奖励消耗型道具,不能替代订阅/永久解锁 |
| Guideline 4.0 | 广告内容不当 | 启用 AdMob 或 MAX 的广告质量审核功能 |
13.3 ATT 弹窗最佳实践
// ❌ 不好的描述
"We need your permission to track you."
// ✅ 好的描述(清晰说明对用户的好处)
"此标识符将用于为您提供更相关的广告体验。您的数据不会用于其他目的。"
// ✅ 英文版
"This identifier will be used to deliver personalized ads to you. Your data will not be used for any other purpose."
提高 ATT 授权率的技巧:
/// 在弹出系统 ATT 弹窗之前,先展示一个自定义的预弹窗说明
class ATTPrePromptView: UIViewController {
func showPrePrompt(from viewController: UIViewController, completion: @escaping () -> Void) {
let alert = UIAlertController(
title: "支持我们继续免费提供服务 🙏",
message: """
我们通过展示广告来维持应用免费。
接下来系统会询问您是否允许追踪。
如果您同意,我们能为您展示更相关的广告,
同时帮助我们获得更好的收入来改进应用。
您的选择不会影响广告数量。
""",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "好的,继续", style: .default) { _ in
completion()
})
alert.addAction(UIAlertAction(title: "暂时跳过", style: .cancel) { _ in
completion()
})
viewController.present(alert, animated: true)
}
}
💡 自定义预弹窗可将 ATT 授权率从 ~20% 提升到 ~40%+,直接影响广告收益。
十四、上线前的检查清单 ✅
### 📋 上线前广告集成检查清单
#### 基础配置
- [ ] Info.plist 中配置了 GADApplicationIdentifier(AdMob App ID)
- [ ] Info.plist 中配置了 NSUserTrackingUsageDescription
- [ ] Info.plist 中添加了所有必需的 SKAdNetworkItems
- [ ] AppLovinSdkKey 已配置(如使用 MAX)
#### SDK 初始化
- [ ] GDPR 同意流程在 SDK 初始化之前执行
- [ ] ATT 权限请求在 SDK 初始化之前执行
- [ ] Meta ATE 标志根据 ATT 结果正确设置
- [ ] 广告 SDK 初始化在 completionHandler 中确认成功
#### 广告实现
- [ ] 所有测试 ID 已替换为生产 ID
- [ ] 测试设备代码已移除或被 #if DEBUG 包裹
- [ ] 插屏广告有频次控制
- [ ] 广告关闭后有预加载逻辑
- [ ] 加载失败有指数退避重试
- [ ] 激励视频奖励逻辑在 didRewardUser 回调中处理
#### 隐私合规
- [ ] App Store Connect 隐私标签已更新
- [ ] GDPR 同意弹窗在欧洲地区正确显示
- [ ] CCPA 合规处理(如面向美国用户)
- [ ] Meta 数据处理选项根据用户同意状态设置
#### 后台配置
- [ ] AdMob 后台已创建所有 Ad Unit
- [ ] Meta AN 后台已创建所有 Placement
- [ ] AppLovin Dashboard 已配置所有 Ad Unit(如使用 MAX)
- [ ] Mediation 组配置正确,Bidding 已启用
#### 测试验证
- [ ] 三种广告格式(Banner/Interstitial/Rewarded)均能正常展示
- [ ] 在模拟器和真机上均测试通过
- [ ] 多次打开/关闭广告无崩溃
- [ ] 网络断开时不崩溃,恢复后能重新加载
- [ ] 内存泄漏检查通过(Instruments - Leaks)
#### 收益追踪
- [ ] 广告展示事件正确上报到分析平台
- [ ] 收益数据可在 AdMob / AppLovin 后台查看
- [ ] 不同网络的填充率和 eCPM 可分别追踪
十五、参考链接汇总
| 资源 | 链接 |
|---|---|
| AdMob iOS 官方文档 | developers.google.com/admob/ios/q… |
| AdMob Mediation 文档 | developers.google.com/admob/ios/m… |
| Meta AN iOS 文档 | developers.facebook.com/docs/audien… |
| AppLovin MAX iOS 文档 | support.axon.ai/en/max/ios/… |
| MAX Mediated Networks | support.axon.ai/en/max/ios/… |
| MAX Banner 文档 | support.axon.ai/en/max/ios/… |
| MAX Interstitial 文档 | support.axon.ai/en/max/ios/… |
| MAX Rewarded 文档 | support.axon.ai/en/max/ios/… |
| SKAdNetwork 配置 | support.axon.ai/en/max/ios/… |
| AppLovin MAX SDK GitHub | github.com/AppLovin/Ap… |
| Google UMP SDK | developers.google.com/admob/ump/i… |
以上就是在 iOS 应用中同时集成 Google AdMob 和 Meta Audience Network 的完整指南。总结核心建议:
- 优先选择聚合方案(AdMob Mediation 或 AppLovin MAX),避免手动管理
- 隐私合规必须放在最优先级——GDPR → ATT → SDK 初始化
- 接入 3+ 个 Bidding 网络是提升收益的最有效手段
- 使用测试 ID 开发,上线前严格按照检查清单逐项确认