阅读视图

发现新文章,点击刷新页面。

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 安装

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 AdMob AppLovin MAX
竞价公平性 AdMob 自家广告可能有优势 更公平透明,所有网络平等竞争
接入复杂度 ⭐ 简单(已用 AdMob 的项目) ⭐⭐ 中等(需额外注册 AppLovin)
支持网络数量 约 20+ 约 25+
收益报告 AdMob 后台 AppLovin Dashboard(更详细)
A/B 测试 有限 内置强大 A/B 测试
广告质量审核 Google Ad Review MAX Ad Review
费用 免费 免费
推荐场景 已深度使用 AdMob 新项目或追求最高收益

五、方案三:手动管理(不推荐但可行)

如果你有特殊原因不想使用聚合平台,可以手动管理两个 SDK 的降级逻辑:

5.1 安装两个 SDK

platform :ios, '13.0'

target 'YourApp' do
  use_frameworks!

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

5.2 手动广告管理器

import Foundation
import GoogleMobileAds
import FBAudienceNetwork

/// 广告管理器 - 手动聚合(降级逻辑)
/// ⚠️ 不推荐:仅作学习参考,生产环境请用聚合平台
class ManualAdManager: NSObject {

    static let shared = ManualAdManager()

    // MARK: - 配置

    private let admobInterstitialUnitID = "ca-app-pub-xxxxx/yyyyy"
    private let metaInterstitialPlacementID = "123456789_987654321"

    private let admobRewardedUnitID = "ca-app-pub-xxxxx/zzzzz"
    private let metaRewardedPlacementID = "123456789_111111111"

    // MARK: - 广告实例

    private var admobInterstitial: GADInterstitialAd?
    private var metaInterstitial: FBInterstitialAd?

    private var admobRewarded: GADRewardedAd?
    private var metaRewarded: FBRewardedVideoAd?

    // MARK: - 状态追踪

    private var isAdMobInterstitialReady = false
    private var isMetaInterstitialReady = false
    private var isAdMobRewardedReady = false
    private var isMetaRewardedReady = false

    // MARK: - 回调

    var onRewardEarned: ((_ amount: Int, _ type: String) -> Void)?
    var onInterstitialDismissed: (() -> Void)?

    private override init() {
        super.init()
    }

    // MARK: - ==================== 插屏广告 ====================

    /// 同时请求两个网络,谁先 ready 谁展示
    func loadInterstitial() {
        isAdMobInterstitialReady = false
        isMetaInterstitialReady = false

        loadAdMobInterstitial()
        loadMetaInterstitial()
    }

    // —— AdMob 插屏 ——

    private func loadAdMobInterstitial() {
        GADInterstitialAd.load(
            withAdUnitID: admobInterstitialUnitID,
            request: GADRequest()
        ) { [weak self] ad, error in
            guard let self = self else { return }
            if let error = error {
                print("❌ AdMob 插屏加载失败: \(error.localizedDescription)")
                return
            }
            print("✅ AdMob 插屏加载成功")
            self.admobInterstitial = ad
            self.admobInterstitial?.fullScreenContentDelegate = self
            self.isAdMobInterstitialReady = true
        }
    }

    // —— Meta 插屏 ——

    private func loadMetaInterstitial() {
        metaInterstitial = FBInterstitialAd(placementID: metaInterstitialPlacementID)
        metaInterstitial?.delegate = self
        metaInterstitial?.load()
    }

    /// 展示插屏:优先 AdMob → 降级 Meta → 两者都无则放弃
    func showInterstitial(from viewController: UIViewController) -> Bool {
        if isAdMobInterstitialReady, let ad = admobInterstitial {
            print("📺 展示 AdMob 插屏")
            ad.present(fromRootViewController: viewController)
            return true
        } else if isMetaInterstitialReady, let ad = metaInterstitial, ad.isAdValid {
            print("📺 展示 Meta 插屏")
            ad.show(fromRootViewController: viewController)
            return true
        } else {
            print("⚠️ 无可用插屏广告")
            return false
        }
    }

    // MARK: - ==================== 激励视频 ====================

    func loadRewarded() {
        isAdMobRewardedReady = false
        isMetaRewardedReady = false

        loadAdMobRewarded()
        loadMetaRewarded()
    }

    // —— AdMob 激励 ——

    private func loadAdMobRewarded() {
        GADRewardedAd.load(
            withAdUnitID: admobRewardedUnitID,
            request: GADRequest()
        ) { [weak self] ad, error in
            guard let self = self else { return }
            if let error = error {
                print("❌ AdMob 激励加载失败: \(error.localizedDescription)")
                return
            }
            print("✅ AdMob 激励加载成功")
            self.admobRewarded = ad
            self.admobRewarded?.fullScreenContentDelegate = self
            self.isAdMobRewardedReady = true
        }
    }

    // —— Meta 激励 ——

    private func loadMetaRewarded() {
        metaRewarded = FBRewardedVideoAd(placementID: metaRewardedPlacementID)
        metaRewarded?.delegate = self
        metaRewarded?.load()
    }

    /// 展示激励视频:优先 AdMob → 降级 Meta
    func showRewarded(from viewController: UIViewController) -> Bool {
        if isAdMobRewardedReady, let ad = admobRewarded {
            print("📺 展示 AdMob 激励视频")
            ad.present(fromRootViewController: viewController) { [weak self] in
                let reward = ad.adReward
                print("🎉 AdMob 奖励: \(reward.amount) \(reward.type)")
                self?.onRewardEarned?(reward.amount.intValue, reward.type)
            }
            return true
        } else if isMetaRewardedReady, let ad = metaRewarded, ad.isAdValid {
            print("📺 展示 Meta 激励视频")
            ad.show(fromRootViewController: viewController)
            return true
        } else {
            print("⚠️ 无可用激励视频")
            return false
        }
    }

    /// 检查是否有广告就绪
    var isInterstitialReady: Bool {
        return isAdMobInterstitialReady || isMetaInterstitialReady
    }

    var isRewardedReady: Bool {
        return isAdMobRewardedReady || isMetaRewardedReady
    }
}

// MARK: - ==================== AdMob Delegate ====================

extension ManualAdManager: GADFullScreenContentDelegate {

    func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
        print("✅ AdMob 全屏广告已关闭")
        isAdMobInterstitialReady = false
        isAdMobRewardedReady = false
        onInterstitialDismissed?()
        // 预加载下一个
        loadInterstitial()
        loadRewarded()
    }

    func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
        print("❌ AdMob 展示失败: \(error.localizedDescription)")
    }
}

// MARK: - ==================== Meta Interstitial Delegate ====================

extension ManualAdManager: FBInterstitialAdDelegate {

    func interstitialAdDidLoad(_ interstitialAd: FBInterstitialAd) {
        print("✅ Meta 插屏加载成功")
        isMetaInterstitialReady = true
    }

    func interstitialAd(_ interstitialAd: FBInterstitialAd, didFailWithError error: Error) {
        print("❌ Meta 插屏加载失败: \(error.localizedDescription)")
        isMetaInterstitialReady = false
    }

    func interstitialAdDidClose(_ interstitialAd: FBInterstitialAd) {
        print("✅ Meta 插屏已关闭")
        isMetaInterstitialReady = false
        onInterstitialDismissed?()
        loadInterstitial()
    }

    func interstitialAdDidClick(_ interstitialAd: FBInterstitialAd) {
        print("👆 Meta 插屏被点击")
    }

    func interstitialAdWillLogImpression(_ interstitialAd: FBInterstitialAd) {
        print("👁️ Meta 插屏曝光")
    }
}

// MARK: - ==================== Meta Rewarded Delegate ====================

extension ManualAdManager: FBRewardedVideoAdDelegate {

    func rewardedVideoAdDidLoad(_ rewardedVideoAd: FBRewardedVideoAd) {
        print("✅ Meta 激励加载成功")
        isMetaRewardedReady = true
    }

    func rewardedVideoAd(_ rewardedVideoAd: FBRewardedVideoAd, didFailWithError error: Error) {
        print("❌ Meta 激励加载失败: \(error.localizedDescription)")
        isMetaRewardedReady = false
    }

    func rewardedVideoAdDidClose(_ rewardedVideoAd: FBRewardedVideoAd) {
        print("✅ Meta 激励视频已关闭")
        isMetaRewardedReady = false
        loadRewarded()
    }

    func rewardedVideoAdVideoComplete(_ rewardedVideoAd: FBRewardedVideoAd) {
        print("🎉 Meta 激励视频观看完成")
        // Meta 不像 AdMob 那样返回具体奖励信息,需要自行定义
        onRewardEarned?(1, "coin")
    }

    func rewardedVideoAdDidClick(_ rewardedVideoAd: FBRewardedVideoAd) {
        print("👆 Meta 激励视频被点击")
    }
}

5.3 手动方案的使用方式

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // 预加载广告
        ManualAdManager.shared.loadInterstitial()
        ManualAdManager.shared.loadRewarded()

        // 设置奖励回调
        ManualAdManager.shared.onRewardEarned = { amount, type in
            print("🎉 发放奖励: \(amount) \(type)")
            // 更新用户余额等
        }
    }

    /// 关卡结束后展示插屏
    func onLevelComplete() {
        _ = ManualAdManager.shared.showInterstitial(from: self)
    }

    /// 用户主动观看激励视频
    @IBAction func watchAdForReward(_ sender: UIButton) {
        let shown = ManualAdManager.shared.showRewarded(from: self)
        if !shown {
            // 提示用户稍后再试
            showAlert(message: "广告暂不可用,请稍后再试")
        }
    }
}

⚠️ 手动方案的缺点:

  • 无法实现真正的实时竞价(Bidding),只是简单的优先级降级
  • 需要自己维护两套 Delegate
  • 无法动态调整优先级和 eCPM 排序
  • 合规(GDPR/CCPA)需要分别处理
  • 新增广告网络时需要大量改代码

六、隐私合规处理(三种方案通用)

6.1 Google UMP(User Messaging Platform)- GDPR 合规

import UIKit
import UserMessagingPlatform

class ConsentManager {

    static let shared = ConsentManager()

    /// 在 SDK 初始化之前调用
    func requestConsentIfNeeded(from viewController: UIViewController, completion: @escaping () -> Void) {

        // ① 创建请求参数
        let parameters = UMPRequestParameters()

        // 调试时使用(正式发布移除)
        #if DEBUG
        let debugSettings = UMPDebugSettings()
        debugSettings.testDeviceIdentifiers = ["YOUR_TEST_DEVICE_HASHED_ID"]
        debugSettings.geography = .EEA // 模拟欧洲用户
        parameters.debugSettings = debugSettings
        #endif

        // ② 请求更新同意信息
        UMPConsentInformation.sharedInstance.requestConsentInfoUpdate(with: parameters) { error in
            if let error = error {
                print("❌ 同意信息更新失败: \(error.localizedDescription)")
                completion()
                return
            }

            // ③ 如果需要,展示同意表单
            UMPConsentForm.loadAndPresentIfRequired(from: viewController) { formError in
                if let formError = formError {
                    print("❌ 同意表单展示失败: \(formError.localizedDescription)")
                }

                // ④ 检查是否可以请求广告
                if UMPConsentInformation.sharedInstance.canRequestAds {
                    print("✅ 用户已授权,可以请求广告")
                }

                completion()
            }
        }
    }

    /// 检查是否可以请求个性化广告
    var canRequestAds: Bool {
        return UMPConsentInformation.sharedInstance.canRequestAds
    }
}

6.2 Meta 隐私合规设置

import FBAudienceNetwork

class MetaPrivacyHelper {

    /// 设置 GDPR 数据处理选项(欧洲用户)
    static func setGDPRConsent(granted: Bool) {
        // Meta 不在 IAB GVL 中,需要使用 Additional Consent
        // 如果用户未同意,应当限制数据使用
        if !granted {
            // 限制数据处理
            FBAdSettings.setDataProcessingOptions(["LDU"], country: 0, state: 0)
        } else {
            // 不限制
            FBAdSettings.setDataProcessingOptions([])
        }
    }

    /// 设置 CCPA 数据处理选项(加州用户)
    static func setCCPAOptOut(optedOut: Bool) {
        if optedOut {
            // 用户选择退出数据售卖
            FBAdSettings.setDataProcessingOptions(["LDU"], country: 1, state: 1000)
        } else {
            FBAdSettings.setDataProcessingOptions([])
        }
    }

    /// 设置 iOS 14+ 广告追踪状态
    static func setAdvertiserTracking(enabled: Bool) {
        FBAdSettings.setAdvertiserTrackingEnabled(enabled)
    }
}

6.3 完整的初始化流程(合规 → ATT → 广告 SDK)

import UIKit
import GoogleMobileAds
import FBAudienceNetwork
import AppTrackingTransparency
import UserMessagingPlatform

class AppStartupManager {

    static let shared = AppStartupManager()
    
    private var isAdsInitialized = false

    /// 完整的广告初始化流程:GDPR → ATT → Meta ATE → SDK 初始化
    func startAdInitialization(from viewController: UIViewController) {
        
        // ==================== 第 1 步:GDPR 同意 ====================
        print("📋 Step 1: 请求 GDPR 同意...")
        
        ConsentManager.shared.requestConsentIfNeeded(from: viewController) { [weak self] in
            guard let self = self else { return }
            
            // ==================== 第 2 步:ATT 权限 ====================
            print("📋 Step 2: 请求 ATT 权限...")
            
            self.requestATTPermission { trackingAuthorized in
                
                // ==================== 第 3 步:配置 Meta 隐私 ====================
                print("📋 Step 3: 配置 Meta 隐私设置...")
                
                FBAdSettings.setAdvertiserTrackingEnabled(trackingAuthorized)
                
                // 如果 GDPR 同意信息可用,配置 Meta 数据处理选项
                if ConsentManager.shared.canRequestAds {
                    FBAdSettings.setDataProcessingOptions([])
                } else {
                    FBAdSettings.setDataProcessingOptions(["LDU"], country: 0, state: 0)
                }
                
                // ==================== 第 4 步:初始化广告 SDK ====================
                print("📋 Step 4: 初始化广告 SDK...")
                
                self.initializeAdSDK()
            }
        }
    }
    
    private func requestATTPermission(completion: @escaping (Bool) -> Void) {
        if #available(iOS 14.5, *) {
            ATTrackingManager.requestTrackingAuthorization { status in
                DispatchQueue.main.async {
                    let authorized = (status == .authorized)
                    print("  ATT 状态: \(status.rawValue), 已授权: \(authorized)")
                    completion(authorized)
                }
            }
        } else {
            // iOS 14.5 以下默认可追踪
            completion(true)
        }
    }
    
    private func initializeAdSDK() {
        guard !isAdsInitialized else { return }
        isAdsInitialized = true
        
        // ====== 方案一:使用 AdMob Mediation ======
        GADMobileAds.sharedInstance().start { status in
            print("✅ AdMob SDK 初始化完成")
            
            for (adapter, adapterStatus) in status.adapterStatusesByClassName {
                print("  [\(adapter)] state=\(adapterStatus.state.rawValue), \(adapterStatus.description)")
            }
            
            // 初始化完成,发送通知让各页面开始加载广告
            NotificationCenter.default.post(name: .adsSDKInitialized, object: nil)
        }
        
        // ====== 方案二(替代):使用 AppLovin MAX ======
        /*
        let initConfig = ALSdkInitializationConfiguration(sdkKey: "YOUR_SDK_KEY") { builder in
            builder.mediationProvider = ALMediationProviderMAX
        }
        ALSdk.shared().initialize(with: initConfig) { sdkConfig in
            print("✅ AppLovin MAX SDK 初始化完成")
            NotificationCenter.default.post(name: .adsSDKInitialized, object: nil)
        }
        */
    }
}

// MARK: - 自定义通知名

extension Notification.Name {
    static let adsSDKInitialized = Notification.Name("adsSDKInitialized")
}

6.4 在 AppDelegate / SceneDelegate 中调用

// SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }
        
        let window = UIWindow(windowScene: windowScene)
        let rootVC = MainViewController()
        window.rootViewController = UINavigationController(rootViewController: rootVC)
        window.makeKeyAndVisible()
        self.window = window
    }
}

// MainViewController.swift
class MainViewController: UIViewController {
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // ⭐ 在主页面显示后启动广告初始化流程
        // 这样 GDPR 弹窗和 ATT 弹窗能正常展示
        AppStartupManager.shared.startAdInitialization(from: self)
        
        // 监听 SDK 初始化完成
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(onAdsReady),
            name: .adsSDKInitialized,
            object: nil
        )
    }
    
    @objc private func onAdsReady() {
        print("🚀 广告 SDK 已就绪,开始加载广告")
        // 在这里加载各种广告
    }
}

七、测试和调试

7.1 AdMob 测试广告 ID

在开发阶段,使用 Google 提供的官方测试 ID,不要使用真实广告 ID 测试(会被封号):

struct TestAdUnitIDs {
    // Google 官方测试 ID(安全使用,不会触发违规)
    static let admobBanner           = "ca-app-pub-3940256099942544/2934735716"
    static let admobInterstitial     = "ca-app-pub-3940256099942544/4411468910"
    static let admobRewarded         = "ca-app-pub-3940256099942544/1712485313"
    static let admobRewardedInterstitial = "ca-app-pub-3940256099942544/6978759866"
    static let admobNative           = "ca-app-pub-3940256099942544/3986624511"
    static let admobAppOpen          = "ca-app-pub-3940256099942544/5575463023"
}

7.2 Meta AN 测试模式

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

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

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

7.3 AppLovin MAX 调试工具

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

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

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

7.4 广告来源追踪(通用)

/// 统一的广告事件追踪器
class AdEventTracker {
    
    /// 记录广告展示来源
    static func trackImpression(
        adFormat: String,       // "banner" / "interstitial" / "rewarded"
        networkName: String,    // "AdMob" / "Meta" / "Google Bidding"
        revenue: Double? = nil, // 收益(如可用)
        adUnitID: String
    ) {
        print("""
        📊 广告曝光
          格式: \(adFormat)
          来源: \(networkName)
          收益: \(revenue.map { String(format: "%.6f", $0) } ?? "N/A")
          广告单元: \(adUnitID)
        """)
        
        // 发送到你的分析平台(Firebase / Amplitude / 自建等)
        // Analytics.logEvent("ad_impression", parameters: [...])
    }
    
    // —— AdMob 获取收益信息 ——
    static func trackAdMobRevenue(ad: GADFullScreenPresentingAd, adFormat: String) {
        // AdMob 收益追踪需要通过 paidEventHandler
        // 在加载成功后设置:
        // ad.paidEventHandler = { value in
        //     let revenue = value.value.doubleValue / 1_000_000 // 微单位转换
        //     trackImpression(adFormat: adFormat, networkName: "AdMob", revenue: revenue, adUnitID: "xxx")
        // }
    }
    
    // —— MAX 获取收益信息 ——
    static func trackMAXRevenue(ad: MAAd, adFormat: String) {
        let revenue = ad.revenue // MAX 直接提供收益值
        let networkName = ad.networkName
        trackImpression(
            adFormat: adFormat,
            networkName: networkName,
            revenue: revenue,
            adUnitID: ad.adUnitIdentifier
        )
    }
}

7.5 常见问题排查清单

问题 可能原因 解决方案
Meta AN 始终 No Fill 未通过 Meta 审核 / Placement ID 错误 确认 App 已在 Meta Business 审核通过
AdMob Adapter 未初始化 GADApplicationIdentifier 未配置 检查 Info.plist
ATT 弹窗不出现 viewDidLoad 中调用太早 改到 viewDidAppear 中调用
收益极低 仅一个网络参与竞争 接入更多网络(Bidding 竞争越多收益越高)
测试时展示真实广告 未添加测试设备 使用测试 ID 或添加测试设备
崩溃:GADApplicationIdentifier AdMob App ID 格式错误 格式应为 ca-app-pub-xxxx~yyyy
Meta SDK 初始化失败 iOS Deployment Target < 13.0 升级最低版本到 13.0
MAX Debugger 显示红色 适配器版本不兼容 更新所有 Pod 到最新版本

八、收益优化最佳实践

8.1 广告展示策略

/// 广告频次控制器
class AdFrequencyManager {
    
    static let shared = AdFrequencyManager()
    
    // 配置
    private let interstitialMinInterval: TimeInterval = 60      // 插屏最少间隔 60 秒
    private let maxInterstitialsPerSession = 10                  // 每次会话最多 10 个插屏
    private let rewardedCooldown: TimeInterval = 30              // 激励视频冷却 30 秒
    
    // 状态
    private var lastInterstitialTime: Date?
    private var sessionInterstitialCount = 0
    private var lastRewardedTime: Date?
    
    /// 检查是否可以展示插屏
    func canShowInterstitial() -> Bool {
        // 检查频率限制
        if let lastTime = lastInterstitialTime {
            let elapsed = Date().timeIntervalSince(lastTime)
            if elapsed < interstitialMinInterval {
                print("⏳ 插屏冷却中,还需 \(Int(interstitialMinInterval - elapsed)) 秒")
                return false
            }
        }
        
        // 检查会话上限
        if sessionInterstitialCount >= maxInterstitialsPerSession {
            print("🚫 已达到本次会话插屏上限")
            return false
        }
        
        return true
    }
    
    /// 记录插屏已展示
    func recordInterstitialShown() {
        lastInterstitialTime = Date()
        sessionInterstitialCount += 1
    }
    
    /// 检查是否可以展示激励视频
    func canShowRewarded() -> Bool {
        if let lastTime = lastRewardedTime {
            let elapsed = Date().timeIntervalSince(lastTime)
            if elapsed < rewardedCooldown {
                return false
            }
        }
        return true
    }
    
    /// 记录激励视频已展示
    func recordRewardedShown() {
        lastRewardedTime = Date()
    }
    
    /// 重置会话计数(App 启动或从后台恢复时调用)
    func resetSession() {
        sessionInterstitialCount = 0
    }
}

8.2 收益优化清单

优化项 说明 预期效果
接入 3+ 个 Bidding 网络 竞争越多出价越高 eCPM 提升 20~50%
使用实时竞价 优于传统 Waterfall eCPM 提升 10~30%
合理控制频次 避免用户疲劳和政策违规 长期收益稳定
预加载广告 关闭后立即预加载下一个 填充率接近 100%
ATT 优化弹窗文案 提高授权率 → 个性化广告收益更高 eCPM 提升 15~30%
Banner 自适应尺寸 使用 Adaptive Banner 替代固定尺寸 eCPM 提升 10~20%
定期更新 SDK 各网络持续优化竞价算法 持续收益改善

九、项目文件结构建议

YourApp/
├── Podfile
├── Info.plist
├── AppDelegate.swift
├── SceneDelegate.swift
│
├── Ads/
│   ├── Core/
│   │   ├── AppStartupManager.swift          // 完整初始化流程(GDPR→ATT→SDK)
│   │   ├── ConsentManager.swift             // GDPR / UMP 同意管理
│   │   ├── MetaPrivacyHelper.swift          // Meta 隐私合规
│   │   ├── AdFrequencyManager.swift         // 广告频次控制
│   │   └── AdEventTracker.swift             // 收益/事件追踪
│   │
│   ├── AdMobMediation/                      // 方案一:AdMob Mediation
│   │   ├── AdMobBannerManager.swift
│   │   ├── AdMobInterstitialManager.swift
│   │   └── AdMobRewardedManager.swift
│   │
│   ├── MAXMediation/                        // 方案二:AppLovin MAX
│   │   ├── MAXBannerManager.swift
│   │   ├── MAXInterstitialManager.swift
│   │   └── MAXRewardedManager.swift
│   │
│   └── Manual/                              // 方案三(不推荐)
│       └── ManualAdManager.swift
│
├── Config/
│   ├── AdConfig.swift                       // 广告 ID 配置(开发/生产)
│   └── TestAdUnitIDs.swift                  // 测试广告 ID
│
├── Views/
│   └── ...
└── ViewControllers/
    └── ...

9.1 广告配置文件(开发/生产切换)

// AdConfig.swift
import Foundation

struct AdConfig {
    
    // MARK: - 环境切换
    
    #if DEBUG
    static let isTestMode = true
    #else
    static let isTestMode = false
    #endif
    
    // MARK: - AdMob 配置
    
    struct AdMob {
        static var bannerID: String {
            isTestMode
                ? "ca-app-pub-3940256099942544/2934735716"       // 测试
                : "ca-app-pub-YOUR_REAL_PUB_ID/BANNER_ID"       // 生产
        }
        
        static var interstitialID: String {
            isTestMode
                ? "ca-app-pub-3940256099942544/4411468910"
                : "ca-app-pub-YOUR_REAL_PUB_ID/INTERSTITIAL_ID"
        }
        
        static var rewardedID: String {
            isTestMode
                ? "ca-app-pub-3940256099942544/1712485313"
                : "ca-app-pub-YOUR_REAL_PUB_ID/REWARDED_ID"
        }
    }
    
    // MARK: - Meta AN 配置
    
    struct Meta {
        static var bannerPlacementID: String {
            isTestMode
                ? "IMG_16_9_APP_INSTALL#YOUR_PLACEMENT_ID"       // 测试
                : "YOUR_REAL_PLACEMENT_ID"                        // 生产
        }
        
        static var interstitialPlacementID: String {
            isTestMode
                ? "IMG_16_9_APP_INSTALL#YOUR_PLACEMENT_ID"
                : "YOUR_REAL_PLACEMENT_ID"
        }
        
        static var rewardedPlacementID: String {
            isTestMode
                ? "VID_HD_16_9_46S_APP_INSTALL#YOUR_PLACEMENT_ID"
                : "YOUR_REAL_PLACEMENT_ID"
        }
    }
    
    // MARK: - AppLovin MAX 配置
    
    struct MAX {
        static let sdkKey = "YOUR_APPLOVIN_SDK_KEY"
        
        // MAX Ad Unit ID(在 AppLovin Dashboard 创建)
        static let bannerAdUnitID       = "YOUR_MAX_BANNER_UNIT"
        static let interstitialAdUnitID = "YOUR_MAX_INTERSTITIAL_UNIT"
        static let rewardedAdUnitID     = "YOUR_MAX_REWARDED_UNIT"
    }
}

十、SwiftUI 集成(额外补充)

如果你的项目使用 SwiftUI,以下是适配方式:

10.1 AdMob Banner 的 SwiftUI 封装

import SwiftUI
import GoogleMobileAds

struct AdMobBannerView: UIViewRepresentable {
    
    let adUnitID: String
    
    func makeUIView(context: Context) -> GADBannerView {
        let bannerView = GADBannerView(adSize: GADAdSizeBanner)
        bannerView.adUnitID = adUnitID
        bannerView.delegate = context.coordinator
        
        // 延迟获取 rootViewController(SwiftUI 环境需要这样做)
        DispatchQueue.main.async {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let rootVC = windowScene.windows.first?.rootViewController {
                bannerView.rootViewController = rootVC
                bannerView.load(GADRequest())
            }
        }
        
        return bannerView
    }
    
    func updateUIView(_ uiView: GADBannerView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, GADBannerViewDelegate {
        func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
            print("✅ [SwiftUI] Banner 加载成功")
        }
        
        func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
            print("❌ [SwiftUI] Banner 加载失败: \(error.localizedDescription)")
        }
    }
}

10.2 在 SwiftUI View 中使用

import SwiftUI

struct GameView: View {
    
    @StateObject private var adViewModel = AdViewModel()
    
    var body: some View {
        VStack {
            // 游戏内容
            Text("Your Game Content")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            // 底部 Banner 广告
            AdMobBannerView(adUnitID: AdConfig.AdMob.bannerID)
                .frame(height: 50)
        }
        .onAppear {
            adViewModel.loadInterstitial()
            adViewModel.loadRewarded()
        }
    }
}

// MARK: - 广告 ViewModel

class AdViewModel: ObservableObject {
    
    @Published var isInterstitialReady = false
    @Published var isRewardedReady = false
    
    private var interstitialAd: GADInterstitialAd?
    private var rewardedAd: GADRewardedAd?
    
    func loadInterstitial() {
        GADInterstitialAd.load(
            withAdUnitID: AdConfig.AdMob.interstitialID,
            request: GADRequest()
        ) { [weak self] ad, error in
            if let ad = ad {
                self?.interstitialAd = ad
                self?.isInterstitialReady = true
            }
        }
    }
    
    func showInterstitial() {
        guard isInterstitialReady,
              let ad = interstitialAd,
              let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let rootVC = windowScene.windows.first?.rootViewController else {
            return
        }
        
        ad.present(fromRootViewController: rootVC)
        isInterstitialReady = false
        
        // 展示后重新加载
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self?.loadInterstitial()
        }
    }
    
    func loadRewarded() {
        GADRewardedAd.load(
            withAdUnitID: AdConfig.AdMob.rewardedID,
            request: GADRequest()
        ) { [weak self] ad, error in
            if let ad = ad {
                self?.rewardedAd = ad
                self?.isRewardedReady = true
            }
        }
    }
    
    func showRewarded(onReward: @escaping (Int, String) -> Void) {
        guard isRewardedReady,
              let ad = rewardedAd,
              let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let rootVC = windowScene.windows.first?.rootViewController else {
            return
        }
        
        ad.present(fromRootViewController: rootVC) {
            let reward = ad.adReward
            onReward(reward.amount.intValue, reward.type)
        }
        
        isRewardedReady = false
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self?.loadRewarded()
        }
    }
}

10.3 AppLovin MAX 的 SwiftUI 封装

import SwiftUI
import AppLovinSDK

struct MAXBannerSwiftUIView: UIViewRepresentable {
    
    let adUnitID: String
    
    func makeUIView(context: Context) -> MAAdView {
        let adView = MAAdView(adUnitIdentifier: adUnitID)
        adView.delegate = context.coordinator
        adView.backgroundColor = .clear
        adView.loadAd()
        return adView
    }
    
    func updateUIView(_ uiView: MAAdView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, MAAdViewAdDelegate {
        func didLoad(_ ad: MAAd) {
            print("✅ [SwiftUI] MAX Banner 加载成功, 来源: \(ad.networkName)")
        }
        
        func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
            print("❌ [SwiftUI] MAX Banner 加载失败: \(error.message)")
        }
        
        func didClick(_ ad: MAAd) {}
        func didFail(toDisplay ad: MAAd, withError error: MAError) {}
        func didExpand(_ ad: MAAd) {}
        func didCollapse(_ ad: MAAd) {}
    }
}

// 使用方式
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello World")
                .frame(maxHeight: .infinity)
            
            MAXBannerSwiftUIView(adUnitID: AdConfig.MAX.bannerAdUnitID)
                .frame(height: 50)
        }
    }
}

十一、完整的 Podfile 汇总

根据你选择的方案,使用对应的 Podfile:

方案一:AdMob Mediation(推荐快速上手)

platform :ios, '13.0'

target 'YourApp' do
  use_frameworks!

  # AdMob SDK(聚合主体)
  pod 'Google-Mobile-Ads-SDK', '~> 12.0'

  # Meta Audience Network Mediation 适配器
  pod 'GoogleMobileAdsMediationFacebook'

  # GDPR 合规
  pod 'GoogleUserMessagingPlatform'

  # (可选)更多网络
  # pod 'GoogleMobileAdsMediationAppLovin'
  # pod 'GoogleMobileAdsMediationUnity'
end

方案二:AppLovin MAX(推荐追求高收益)

platform :ios, '13.0'

target 'YourApp' do
  use_frameworks!
  inhibit_all_warnings!

  # AppLovin MAX SDK(聚合主体)
  pod 'AppLovinSDK'

  # AdMob 适配器
  pod 'AppLovinMediationGoogleAdapter'

  # Meta AN 适配器
  pod 'AppLovinMediationFacebookAdapter'

  # (可选)更多网络 - 接入越多竞争越激烈收益越高
  # pod 'AppLovinMediationUnityAdsAdapter'
  # pod 'AppLovinMediationMintegralAdapter'
  # pod 'AppLovinMediationVungleAdapter'
  # pod 'AppLovinMediationIronSourceAdapter'
  # pod 'AppLovinMediationByteDanceAdapter'     # Pangle / TikTok
  # pod 'AppLovinMediationChartboostAdapter'
end

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

platform :ios, '13.0'

target 'YourApp' do
  use_frameworks!

  pod 'Google-Mobile-Ads-SDK', '~> 12.0'
  pod 'FBAudienceNetwork'
  pod 'GoogleUserMessagingPlatform'
end

十二、总结与推荐

最终推荐

场景 推荐方案 理由
新项目 / 追求最高收益 ⭐ AppLovin MAX 公平竞价、更多网络、详细报告
已有 AdMob 基础 / 快速接入 ⭐ AdMob Mediation 改动最小,生态成熟
学习了解原理 手动管理 仅作学习参考

关键要点回顾

  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 ~ 3.0 0.1 0.1 ~ 1.0
Interstitial 5.0 5.0 ~ 20.0 1.0 1.0 ~ 8.0
Rewarded Video 10.0 10.0 ~ 40.0 3.0 3.0 ~ 15.0
MREC 1.0 1.0 ~ 5.0 0.3 0.3 ~ 2.0
App Open 5.0 5.0 ~ 15.0 1.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.1 ATT 弹窗描述不清或存在误导 使用清晰、诚实的 NSUserTrackingUsageDescription 文案
Guideline 5.1.2 隐私标签与实际不符 根据所有接入 SDK 如实更新隐私标签
Guideline 2.3.2 广告遮挡 UI 或影响功能 确保 Banner 不遮挡按钮;插屏在合理时机展示
Guideline 3.1.1 激励视频绕过内购 激励视频只能奖励消耗型道具,不能替代订阅/永久解锁
Guideline 4.0 广告内容不当 启用 AdMob 或 MAX 的广告质量审核功能

13.3 ATT 弹窗最佳实践

// ❌ 不好的描述
"We need your permission to track you."

// ✅ 好的描述(清晰说明对用户的好处)
"此标识符将用于为您提供更相关的广告体验。您的数据不会用于其他目的。"

// ✅ 英文版
"This identifier will be used to deliver personalized ads to you. Your data will not be used for any other purpose."

提高 ATT 授权率的技巧:

/// 在弹出系统 ATT 弹窗之前,先展示一个自定义的预弹窗说明
class ATTPrePromptView: UIViewController {
    
    func showPrePrompt(from viewController: UIViewController, completion: @escaping () -> Void) {
        let alert = UIAlertController(
            title: "支持我们继续免费提供服务 🙏",
            message: """
            我们通过展示广告来维持应用免费。
            
            接下来系统会询问您是否允许追踪。
            如果您同意,我们能为您展示更相关的广告,
            同时帮助我们获得更好的收入来改进应用。
            
            您的选择不会影响广告数量。
            """,
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(title: "好的,继续", style: .default) { _ in
            completion()
        })
        
        alert.addAction(UIAlertAction(title: "暂时跳过", style: .cancel) { _ in
            completion()
        })
        
        viewController.present(alert, animated: true)
    }
}

💡 自定义预弹窗可将 ATT 授权率从 ~20% 提升到 ~40%+,直接影响广告收益。


十四、上线前的检查清单 ✅

### 📋 上线前广告集成检查清单

#### 基础配置
- [ ] Info.plist 中配置了 GADApplicationIdentifier(AdMob App ID)
- [ ] Info.plist 中配置了 NSUserTrackingUsageDescription
- [ ] Info.plist 中添加了所有必需的 SKAdNetworkItems
- [ ] AppLovinSdkKey 已配置(如使用 MAX)

#### SDK 初始化
- [ ] GDPR 同意流程在 SDK 初始化之前执行
- [ ] ATT 权限请求在 SDK 初始化之前执行
- [ ] Meta ATE 标志根据 ATT 结果正确设置
- [ ] 广告 SDK 初始化在 completionHandler 中确认成功

#### 广告实现
- [ ] 所有测试 ID 已替换为生产 ID
- [ ] 测试设备代码已移除或被 #if DEBUG 包裹
- [ ] 插屏广告有频次控制
- [ ] 广告关闭后有预加载逻辑
- [ ] 加载失败有指数退避重试
- [ ] 激励视频奖励逻辑在 didRewardUser 回调中处理

#### 隐私合规
- [ ] App Store Connect 隐私标签已更新
- [ ] GDPR 同意弹窗在欧洲地区正确显示
- [ ] CCPA 合规处理(如面向美国用户)
- [ ] Meta 数据处理选项根据用户同意状态设置

#### 后台配置
- [ ] AdMob 后台已创建所有 Ad Unit
- [ ] Meta AN 后台已创建所有 Placement
- [ ] AppLovin Dashboard 已配置所有 Ad Unit(如使用 MAX)
- [ ] Mediation 组配置正确,Bidding 已启用

#### 测试验证
- [ ] 三种广告格式(Banner/Interstitial/Rewarded)均能正常展示
- [ ] 在模拟器和真机上均测试通过
- [ ] 多次打开/关闭广告无崩溃
- [ ] 网络断开时不崩溃,恢复后能重新加载
- [ ] 内存泄漏检查通过(Instruments - Leaks)

#### 收益追踪
- [ ] 广告展示事件正确上报到分析平台
- [ ] 收益数据可在 AdMob / AppLovin 后台查看
- [ ] 不同网络的填充率和 eCPM 可分别追踪

十五、参考链接汇总

资源 链接
AdMob iOS 官方文档 developers.google.com/admob/ios/q…
AdMob Mediation 文档 developers.google.com/admob/ios/m…
Meta AN iOS 文档 developers.facebook.com/docs/audien…
AppLovin MAX iOS 文档 support.axon.ai/en/max/ios/…
MAX Mediated Networks support.axon.ai/en/max/ios/…
MAX Banner 文档 support.axon.ai/en/max/ios/…
MAX Interstitial 文档 support.axon.ai/en/max/ios/…
MAX Rewarded 文档 support.axon.ai/en/max/ios/…
SKAdNetwork 配置 support.axon.ai/en/max/ios/…
AppLovin MAX SDK GitHub github.com/AppLovin/Ap…
Google UMP SDK developers.google.com/admob/ump/i…

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

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

SKAdNetwork 6.0 深度实战:多窗口转化值(Conversion Value)建模与数据分层架构

摘要:随着 iOS 隐私政策的持续演进,SKAdNetwork (SKAN) 6.0 已成为移动营销衡量的新标准。本文将深入探讨 SKAN 6.0 的核心机制,重点解析如何针对三个转化窗口进行科学的转化值(CV)建模,并构建适配分层数据(Hierarchical Data)的归因架构,帮助高级 iOS 开发者与 AdTech 专家在隐私保护时代重构数据增长引擎。


一、 SKAN 6.0:从“黑盒”到“多维度透明”

SKAdNetwork 6.0(随 iOS 17.4+ 发布)在 4.0 的基础上进一步深化了隐私与效果的平衡。相比早期版本,SKAN 6.0 的核心进步在于通过多窗口回传(Multiple Postbacks)分层源标识符(Hierarchical Source IDs),提供了更长的生命周期观测能力和更灵活的数据粒度。

核心变化点:

  1. 三段式转化窗口
    • Window 1 (P1): 0-2 天,支持精细化(Fine-grained, 0-63)或粗略化(Coarse-grained)CV。
    • Window 2 (P2): 3-7 天,仅支持粗略化 CV。
    • Window 3 (P3): 8-35 天,仅支持粗略化 CV。
  2. 分层源标识符(Source ID):从 2 位扩展到 4 位,根据人群匿名度(Crowd Anonymity)阶梯式释放数据。
  3. 广告主域名(Advertising Domain):增强了网页到 App 归因的安全性与透明度。

二、 多窗口转化值(CV)建模策略

在 SKAN 6.0 中,CV 建模不再是单一维度的映射,而是一场关于“时间”与“价值”的博弈。

2.1 Window 1 (P1):精细化建模(0-63)

P1 决定了初始出价模型的准确性。建议采用“收入+行为”混合模型:

  • Bits 0-3 (Value 0-15):代表收入区间(e.g., 0,0, 0.99-$4.99, ...)。
  • Bits 4-5 (Value 16-63):代表关键转化行为(e.g., 完成新手引导、加入购物车、订阅尝试)。

2.2 Window 2 & 3 (P2/P3):粗略化建模(Low/Medium/High)

由于仅支持三个档位,建模应侧重于长期留存LTV 预测

  • Low: 用户仅启动过 App(维持活跃)。
  • Medium: 用户完成了中层转化(e.g., 累计在线时长 > 10min 或 完成 3 次关卡)。
  • High: 高价值行为(e.g., 再次复购或触发深度互动)。

2.3 锁窗机制(LockWindow)的应用

开发者可以通过 lockWindow() 提前锁定当前的转化窗口,以缩短数据回传的延迟。 实战建议:当用户触发了预期的最高价值行为(如首充)后立即锁窗,以最快速度将数据反馈给投放渠道。


三、 适配分层数据(Hierarchical Data)的架构设计

SKAN 6.0 的数据产出取决于“人群匿名度”。这种不确定性要求服务端架构具备极强的鲁棒性。

3.1 数据分层接收流程

  1. 捕获原始回传:服务端需能够处理不同粒度的 JSON。
  2. 映射解析层:根据 source-identifier 的位数(2/3/4位)决定关联的广告层级(Campaign vs Ad Group vs Creative)。
  3. 延迟修正模型:利用 Apple 定义的时间随机延迟(Window 1: 24-48h; Window 2/3: 24-144h)进行数据对齐。

3.2 代码示例:更新转化值与锁定窗口(Swift)

import StoreKit

func updateSKANConversion(revenue: Double, isDeepConversion: Bool) {
    let cvValue = calculateFineGrainedCV(revenue) // 自定义映射逻辑
    let coarseValue: SKAdNetwork.CoarseConversionValue = revenue > 10 ? .high : .medium
    
    if #available(iOS 16.1, *) {
        SKAdNetwork.updatePostbackConversionValue(cvValue, coarseValue: coarseValue) { error in
            if let error = error {
                print("SKAN Update Failed: \(error.localizedDescription)")
            }
        }
        
        // 如果是关键高价值行为,锁定窗口以加速回传
        if isDeepConversion {
            SKAdNetwork.updatePostbackConversionValue(cvValue, coarseValue: coarseValue, lockWindow: true) { error in
                // 处理回调
            }
        }
    }
}

四、 总结与最佳实践

  1. 组合建模:利用 P1 优化 CPI/tROAS,利用 P2/P3 观测用户留存。
  2. 阈值监控:实时监控 postback 中的数据粒度,若频繁出现低位 Source ID,说明样本量不足以触发隐私阈值,需调整投放预算集中度。
  3. 混合归因:将 SKAN 数据与自建的概率性归因(Probabilistic Attribution)进行交叉校验,构建更完整的用户画像。

Swift 6 严格并发检查:@Sendable 与 Actor 隔离的深度解析

摘要: Swift 6 引入了严格的并发检查机制,旨在消除数据竞争,提升多线程编程的安全性与可维护性。本文将深入探讨 @Sendable 协议的本质与应用场景,以及 Actor 隔离模型如何成为构建并发安全代码的基石。我们将通过代码示例和架构图,剖析这些新特性如何帮助 iOS 开发者避免常见的并发陷阱,并提供平滑迁移到 Swift 6 并发模型的实践指导。

1. 引言:并发编程的挑战与 Swift 6 的应对

在现代移动应用开发中,并发编程无处不在,从 UI 响应、网络请求到数据处理,合理利用多核处理器能显著提升用户体验。然而,并发也带来了诸多挑战,如数据竞争(Data Race)、死锁(Deadlock)和优先级反转(Priority Inversion),这些问题往往难以调试,导致应用崩溃或行为异常。

Swift 社区长期致力于解决这些问题。从 Swift 5.5 引入的 async/await 结构化并发,到 Swift 6 升级为默认启用的严格并发检查 (Strict Concurrency Checking),都体现了 Swift 在保证性能的同时,极大提升并发安全性的决心。

本文将聚焦 Swift 6 核心的两个概念:@Sendable 协议和 Actor 隔离模型。它们共同构筑了 Swift 安全并发的基石。

2. 理解 @Sendable:类型安全传递的契约

2.1 @Sendable 的核心作用

@Sendable 是 Swift 6 中引入的一个标记协议 (Marker Protocol),它声明了一个类型或函数是可以在并发上下文之间安全传递的。这里的“安全传递”意味着该类型的值在从一个并发域(如 Task 或 Actor)发送到另一个并发域时,不会引发数据竞争。

具体来说,满足 @Sendable 要求的类型必须满足以下条件之一:

  1. 值类型 (Value Type):如 structenum,它们默认是可复制的,每个并发域都有其独立的副本,因此是 Sendable 的。
  2. 不可变引用类型 (Immutable Reference Type):如果一个 class 的所有存储属性都是 let 常量,且自身是 final 的,它也是 Sendable 的。
  3. 遵循 Sendable 的容器类型:如 Array<Element>Dictionary<Key, Value>,只要其 ElementKey/Value 遵循 Sendable,自身也遵循 Sendable
  4. 无状态或带有 Actor 隔离状态的闭包:闭包捕获的变量必须是 Sendable 的,或者闭包本身是 async 且标记为 @Sendable

2.2 为什么需要 @Sendable

考虑以下经典的竞态条件场景:

class Counter {
    var value = 0

    func increment() {
        value += 1
    }
}

let counter = Counter()

// ❌ 潜在的数据竞争
Task {
    for _ in 0..<1000 {
        counter.increment()
    }
}

Task {
    for _ in 0..<1000 {
        counter.increment()
    }
}

在 Swift 6 严格并发模式下,编译器会立刻对 counter 这个非 Sendable 的引用类型在多个 Task 中被共享和修改的情况发出警告甚至错误。

@Sendable 的设计哲学:不是通过运行时锁或信号量来强制同步,而是通过编译时检查,确保只有那些本质上安全共享的数据类型才能跨并发边界传递从而在源头上预防数据竞争。

2.3 @Sendable 闭包与函数

函数和闭包也可以是 @Sendable 的。一个 @Sendable 的闭包意味着它捕获的所有值都必须是 @Sendable 的,或者它没有捕获任何可变状态。

// Sendable 闭包示例
func processData(@Sendable _ handler: @escaping ([Int]) async -> Void) {
    Task {
        let data = [1, 2, 3] // 假设数据是 Sendable 的
        await handler(data)
    }
}

processData { numbers in
    // numbers 是一个 Sendable 类型 ([Int]),安全
    print("Processing numbers: \(numbers)")
}

3. Actor 隔离:并发安全的首选模型

3.1 Actor 的核心概念

Actor 是 Swift 并发模型中一种强大的隔离机制 (Isolation Mechanism)。它将数据和操作封装在一个独立的并发执行单元中,确保:

  • 状态隔离:Actor 内部的可变状态只能由 Actor 自身的方法直接访问和修改。
  • 单线程访问:在任何时刻,只有一个任务能够执行 Actor 的代码。这意味着 Actor 内部不需要手动加锁,因为它天然是线程安全的。

当外部任务需要与 Actor 交互时,必须通过 await 关键字异步调用其方法。这强制了所有对 Actor 状态的访问都经过 Actor 的“信箱”,确保了消息的顺序性。

actor BankAccount {
    private var balance: Double

    init(initialBalance: Double) {
        self.balance = initialBalance
    }

    func deposit(amount: Double) {
        balance += amount
        print("Deposited \(amount). New balance: \(balance)")
    }

    func withdraw(amount: Double) {
        if balance >= amount {
            balance -= amount
            print("Withdrew \(amount). New balance: \(balance)")
        } else {
            print("Insufficient funds to withdraw \(amount). Current balance: \(balance)")
        }
    }

    func getBalance() -> Double {
        return balance
    }
}

// 使用 Actor
let account = BankAccount(initialBalance: 1000)

Task {
    await account.deposit(amount: 200)
}

Task {
    await account.withdraw(amount: 150)
}

Task {
    let currentBalance = await account.getBalance()
    print("Final balance: \(currentBalance)")
}

在上述例子中,即使 depositwithdraw 被并发调用,Actor 机制也能保证它们按顺序执行,避免了 balance 的数据竞争。

3.2 Actor 隔离图解

为了更好地理解 Actor 的工作原理,我们可以用一个 Mermaid 流程图来表示:


graph TD
    A[外部并发任务 A] -->|异步调用 withdraw(150)| ActorQueue(Actor 消息队列)
    B[外部并发任务 B] -->|异步调用 deposit(200)| ActorQueue
    C[外部并发任务 C] -->|异步调用 getBalance()| ActorQueue

    ActorQueue -->|按顺序执行| ActorCore(BankAccount Actor 核心)
    ActorCore -->|修改 balance| ActorState[Actor 内部状态 (balance)]
    ActorCore --&gt; D{返回结果给 Task C}

解释:

  • 多个外部并发任务可以同时向 Actor 发送消息(调用方法)。
  • 这些消息进入 Actor 内部的队列,Actor 会按顺序逐一处理。
  • 在 Actor 核心处理消息时,它拥有对内部状态的独占访问权,因此无需额外的锁。
  • 当 Actor 完成操作并有结果需要返回时(如 getBalance()),它会通过 await 机制将结果传递回调用者。

3.3 MainActor:主线程隔离

Swift UI 和 UIKit 这样的框架,其 UI 更新操作必须在主线程上执行。Swift 引入了 MainActor 这个全局 Actor 来解决这个问题。

任何标记为 @MainActor 的函数、属性或类,都保证其操作在主线程上执行。

@MainActor
class UIUpdater {
    var message: String = "" {
        didSet {
            // 这个属性的修改和 didSet 都会在主线程上执行
            print("UI Updated: \(message)")
        }
    }

    func updateMessage(with text: String) {
        // 这个方法也会在主线程上执行
        self.message = text
    }
}

let updater = UIUpdater()

func fetchData() async {
    let result = await performNetworkRequest() // 假设这是一个耗时操作
    
    // 异步切换到 MainActor,确保 UI 更新安全
    await MainActor.run {
        updater.updateMessage(with: "Data loaded: \(result)")
    }
}

Task {
    await fetchData()
}

在 Swift 6 严格并发模式下,如果一个非 @MainActor 的异步函数尝试直接修改 @MainActor 隔离的属性或调用其方法,编译器会发出警告或错误,强制你使用 await MainActor.run { ... } 进行安全的线程切换。

4. Swift 6 严格并发检查的实际影响与迁移

Swift 6 默认开启严格并发检查,这意味着过去一些“看似无害”的并发代码现在会被编译器捕获。这无疑会增加短期内的编译错误,但从长远来看,它极大地提升了代码的质量和可靠性。

迁移建议:

  1. 逐步启用:对于大型项目,可以先在模块级别启用,逐步推广。
  2. 理解错误:当出现关于 @Sendable 或 Actor 隔离的编译错误时,不要盲目添加 nonisolated@unchecked Sendable。深入理解编译器报错的意图,思考如何重构代码以满足并发安全。
  3. 拥抱 Actor:将共享的可变状态封装在 Actor 中是解决数据竞争最 Swift-idiomatic 的方式。
  4. 谨慎使用 nonisolated@unchecked Sendable:这两个是逃逸舱口,只在明确知道其行为,并能保证外部同步的情况下使用,否则会破坏 Swift 的并发安全性保证。

5. 结论

Swift 6 的严格并发检查是 Swift 语言发展的一个里程碑,它通过 @Sendable 和 Actor 隔离,为开发者提供了前所未有的编译时并发安全保证。虽然迁移过程可能需要投入一定精力,但最终会收获更健壮、更易于维护的并发代码。作为资深 iOS 开发者,掌握并应用这些新特性,是构建高性能、高质量应用的必经之路。


参考资料:

❌