Meta Audience Network + AdMob 双平台集成完整指南

35 阅读21分钟

一、架构选择:聚合平台(Mediation)

同时接入多个广告网络,强烈推荐使用广告聚合(Mediation)平台,而不是自己手动管理切换逻辑。

1.1 主流聚合平台对比

聚合平台说明推荐度
Google AdMob MediationAdMob 官方内置聚合,支持 Meta AN 作为第三方适配器⭐⭐⭐⭐⭐
AppLovin MAX独立聚合平台,支持广泛广告网络,实时竞价能力强⭐⭐⭐⭐⭐
ironSource LevelPlay游戏领域主流,已与 Unity Ads 合并⭐⭐⭐⭐
Mintegral / TopOn / TradPlus国内出海常用,支持国内外主流网络⭐⭐⭐⭐

本指南重点讲解最主流的两种方案:

  1. Google AdMob Mediation(以 AdMob 为主,Meta AN 做竞价补充)
  2. 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 MediationAdMob 官方内置,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 Target13.0
Google Mobile Ads SDK12.0.0+(推荐最新)
Meta Audience Network SDK6.21.0
Meta Adapter6.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,它会自动拉取 FBAudienceNetwork SDK,不需要额外单独引入。

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 后台创建广告位
  1. 登录 Meta Business Suite → Monetization Manager
  2. 创建 Property → 选择 iOS 平台
  3. Mediation Platform 选择 "AdMob"
  4. 为每种格式(Banner / Interstitial / Rewarded)创建 Placement
  5. 记录每个 Placement ID(格式如 123456789_987654321
步骤 2:AdMob 后台添加 Meta 竞价
  1. 登录 AdMob Console
  2. 导航到 Mediation → Mediation Groups
  3. 创建或编辑一个 Mediation Group
  4. Bidding 区域,点击 Add Ad Sources → Meta Audience Network
  5. 输入 Meta 的 Placement ID
  6. 保存

💡 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
  1. MAX → Manage → Ad Units → 创建 Ad Unit
  2. 选择格式(Banner / Interstitial / Rewarded)
  3. Bidding 区域启用:
    • Google Bidding and Google AdMob → 填入 AdMob 的 Ad Unit ID
    • Meta Audience Network → 填入 Meta 的 Placement ID
  4. 保存

两个网络都通过 实时竞价(Bidding) 参与,MAX 会自动选择出价最高的网络展示广告


四、两种方案对比

特性方案一:AdMob Mediation方案二:AppLovin MAX
聚合主体Google AdMobAppLovin MAX
竞价公平性AdMob 自家广告可能有优势更公平透明,所有网络平等竞争
接入复杂度⭐ 简单(已用 AdMob 的项目)⭐⭐ 中等(需额外注册 AppLovin)
支持网络数量约 20+约 25+
收益报告AdMob 后台AppLovin Dashboard(更详细)
A/B 测试有限内置强大 A/B 测试
广告质量审核Google Ad ReviewMAX 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 或添加测试设备
崩溃:GADApplicationIdentifierAdMob 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%
使用实时竞价优于传统 WaterfalleCPM 提升 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改动最小,生态成熟
学习了解原理手动管理仅作学习参考

关键要点回顾

  1. 一定要使用聚合平台,不要手动管理多个 SDK
  2. 优先使用 Bidding(实时竞价) 而非 Waterfall(瀑布流)
  3. 接入 3 个以上竞价网络,竞争越多收益越高
  4. 隐私合规三步走:GDPR 同意 → ATT 授权 → 各 SDK 设置
  5. 使用测试 ID 开发,上线前切换为生产 ID
  6. 预加载策略:广告关闭后立即预加载下一个
  7. 频次控制:避免过度展示导致用户流失或政策违规
  8. 定期更新 SDK:各广告网络持续优化,新版本通常带来更高收益

预期收益参考(仅供参考,受地区/品类/用户质量影响极大)

广告格式美国市场 eCPM 参考中国/亚洲市场 eCPM 参考
Banner0.5 0.5 ~ 3.00.1 0.1 ~ 1.0
Interstitial5.0 5.0 ~ 20.01.0 1.0 ~ 8.0
Rewarded Video10.0 10.0 ~ 40.03.0 3.0 ~ 15.0
MREC1.0 1.0 ~ 5.00.3 0.3 ~ 2.0
App Open5.0 5.0 ~ 15.01.0 1.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.1ATT 弹窗描述不清或存在误导使用清晰、诚实的 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 Networkssupport.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 GitHubgithub.com/AppLovin/Ap…
Google UMP SDKdevelopers.google.com/admob/ump/i…

以上就是在 iOS 应用中同时集成 Google AdMobMeta Audience Network 的完整指南。总结核心建议:

  1. 优先选择聚合方案(AdMob Mediation 或 AppLovin MAX),避免手动管理
  2. 隐私合规必须放在最优先级——GDPR → ATT → SDK 初始化
  3. 接入 3+ 个 Bidding 网络是提升收益的最有效手段
  4. 使用测试 ID 开发,上线前严格按照检查清单逐项确认