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

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

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

1.1 主流聚合平台对比

聚合平台 说明 推荐度
Google AdMob Mediation AdMob 官方内置聚合,支持 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 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 安装

ruby 复制代码
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
bash 复制代码
pod install --repo-update

只需要添加 GoogleMobileAdsMediationFacebook,它会自动拉取 FBAudienceNetwork SDK,不需要额外单独引入。

2.3 Info.plist 配置

xml 复制代码
<!-- ① 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 初始化

swift 复制代码
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 初始化(推荐写法)

swift 复制代码
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()

swift 复制代码
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 聚合)

swift 复制代码
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 的授权行:

text 复制代码
# 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 安装

ruby 复制代码
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
bash 复制代码
pod install --repo-update

💡 只需安装适配器 Pod,它们会自动拉取对应的广告网络 SDK

3.2 Info.plist 配置

xml 复制代码
<!-- ① 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 初始化

swift 复制代码
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 初始化完成")
            // 此时可以开始加载广告
        }
    }
}
swift 复制代码
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 插屏广告

swift 复制代码
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 激励视频广告

swift 复制代码
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 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

ruby 复制代码
platform :ios, '13.0'

target 'YourApp' do
  use_frameworks!

  pod 'Google-Mobile-Ads-SDK'   # AdMob
  pod 'FBAudienceNetwork'        # Meta AN
end

5.2 手动广告管理器

swift 复制代码
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 手动方案的使用方式

swift 复制代码
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 合规

swift 复制代码
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 隐私合规设置

swift 复制代码
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)

swift 复制代码
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 中调用

swift 复制代码
// 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 测试(会被封号):

swift 复制代码
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 测试模式

swift 复制代码
#if DEBUG
// 添加测试设备(设备 IDFA 的哈希值,在控制台日志中查找)
FBAdSettings.addTestDevice("YOUR_DEVICE_HASH")

// 或者启用模拟器测试模式
FBAdSettings.addTestDevice(FBAdSettings.testDeviceHash())

// 设置测试广告类型(可选)
// FBAdSettings.setLogLevel(.log)
#endif

7.3 AppLovin MAX 调试工具

swift 复制代码
#if DEBUG
// 显示 MAX Mediation Debugger(可视化调试面板)
// 显示所有适配器状态、广告加载记录等
ALSdk.shared().showMediationDebugger()
#endif

💡 MAX Mediation Debugger 非常强大,可以一目了然看到:

  • 各适配器是否正确初始化
  • 各网络的竞价情况
  • 广告加载成功/失败详情

7.4 广告来源追踪(通用)

swift 复制代码
/// 统一的广告事件追踪器
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 广告展示策略

swift 复制代码
/// 广告频次控制器
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 各网络持续优化竞价算法 持续收益改善

九、项目文件结构建议

arduino 复制代码
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 广告配置文件(开发/生产切换)

swift 复制代码
// 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,以下是适配方式:

swift 复制代码
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 中使用

swift 复制代码
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 封装

swift 复制代码
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(推荐快速上手)

ruby 复制代码
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(推荐追求高收益)

ruby 复制代码
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

方案三:手动管理(不推荐)

ruby 复制代码
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 参考
Banner 0.5 0.5 ~ 0.5 3.0 0.1 0.1 ~ 0.1 1.0
Interstitial 5.0 5.0 ~ 5.0 20.0 1.0 1.0 ~ 1.0 8.0
Rewarded Video 10.0 10.0 ~ 10.0 40.0 3.0 3.0 ~ 3.0 15.0
MREC 1.0 1.0 ~ 1.0 5.0 0.3 0.3 ~ 0.3 2.0
App Open 5.0 5.0 ~ 5.0 15.0 1.0 1.0 ~ 1.0 6.0

⚠️ 以上数据仅为行业大致参考范围。实际 eCPM 受以下因素影响极大:

  • 用户地区(T1 国家如美/英/澳/加远高于其他地区)
  • App 品类(金融、教育类 > 工具类 > 游戏休闲类)
  • 用户质量(高留存用户 eCPM 更高)
  • 接入网络数量(3+ 个 Bidding 网络可提升 20~50%)
  • ATT 授权率(授权用户 eCPM 可比未授权高 30~80%)
相关推荐
茶底世界之下3 天前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
2501_916007473 天前
苹果商店iOS应用上架费用全面解析:开发者计划与审核费用计算
android·ios·小程序·https·uni-app·iphone·webview
2501_916008893 天前
如何实现iOS App代码混淆:SwiftShield使用指南
android·ios·小程序·https·uni-app·iphone·webview
2501_915909063 天前
移动端开发工具链怎么组合比较好,iOS、Android、跨端三套配置方案
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
2501_916008893 天前
怎么用 Godot 导出 iOS 应用并上架 App Store?签名字段该填什么?
android·ios·小程序·https·uni-app·iphone·webview
2501_915918413 天前
iOS编程用什么软件好?主流工具Xcode、AppCode、CodeRunner与新兴快蝎对比
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
终端安全笔记4 天前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
9765033354 天前
iOS 上架 4.3a 被拒【uniapp专讲】
flutter·ios·objective-c·uniapp·swift
黑化旺仔4 天前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan5 天前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple