Trusted Arbiter

在 iOS 应用中比较 CloudX 出价与受支持的第三方出价

Trusted Arbiter 会比较已加载的 CloudX 出价和受支持的第三方出价,并返回被选中的平台。CloudX iOS SDK 3.4.0 支持 CloudX、Unity LevelPlay 和 PubMatic 出价输入。CloudX iOS SDK 3.5.0 及更高版本还支持发布商传入的自定义出价输入。

支持的广告格式

Trusted Arbiter 与广告格式无关:它接收任何已加载的 CloudX 广告,并将其与传入的第三方出价进行比较,与格式无关。横幅广告、MREC、插屏广告和激励视频广告均受支持。

  • 插屏广告和激励视频广告(全屏格式)遵循本页后续展示的分步和控制器模式。
  • 横幅广告和 MREC(视图格式)需要下方横幅广告和 MREC 仲裁一节中描述的额外处理,因为落败出价的视图绝不能被加入视图层级,并且刷新周期需要手动协调。

基础 API

从已加载的广告创建出价候选项,然后传给仲裁器。

// cloudXAd 是 CloudX 加载回调中的 CLXAd 对象。
// levelPlayAdInfo 是 Unity LevelPlay 的广告信息对象。
// pubMaticPrice 和 pubMaticPartnerName 来自 PubMatic/OpenWrap 出价对象。
CLXArbiterBid *cloudXBid = [CLXArbiterBid cloudXBidWithAd:cloudXAd];

CLXArbiterBid *levelPlayBid =
    [CLXArbiterBid levelPlayBidWithNetworkName:levelPlayAdInfo.adNetwork
                                       revenue:levelPlayAdInfo.revenue.doubleValue
                                     precision:levelPlayAdInfo.precision];

CLXArbiterBid *pubMaticBid =
    [CLXArbiterBid pubMaticBidWithPrice:pubMaticPrice
                            partnerName:pubMaticPartnerName
                                 extras:nil];

CLXArbiterConfiguration *configuration =
    [CLXArbiterConfiguration configurationWithBids:@[cloudXBid, levelPlayBid, pubMaticBid]];

[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
    NSLog(@"Selected platform: %@", result.platform.name);
}];

CLXArbiterBid.cloudX 接收 CloudX 加载回调中的 CLXAd 对象。CLXArbiterBid.levelPlay 接收 Unity LevelPlay 广告信息值。CLXArbiterBid.pubMatic 接收 PubMatic OpenWrap 出价价格和可选的合作伙伴名称。extras 映射在 LevelPlay 和 PubMatic 出价中均为可选参数,partnerName 在 PubMatic 出价中为可选参数。完成回调在主线程上执行,因此你可以直接在其中展示广告或更新 UI。

result.platform 在选中平台时为 CLXArbiterPlatform.cloudXlevelPlaypubMatic;当无法选出获胜平台时(例如未传入任何出价),则为 CLXArbiterPlatform.none

AdMob 与 Google Ad Manager

CloudX 会将已加载的 CloudX 广告与已加载的 AdMob 或 Google Ad Manager 广告进行比较。AdMob 和 Ad Manager 是两个独立的需求来源,因此二者可以同时参与同一次仲裁。此方式适用于发布商自行管理的聚合配置,包括商业上称为 AdMob Pro 的账户;SDK 不提供单独的 AdMob Pro API。

Google 需求方通常不会在展示前透露已加载广告的价格,因此仲裁器无法获得可与 CloudX 出价比较的 pre-bid 价格。CloudX 会根据同类广告单元过往的表现估算出价,因此你无需自行提供价格。不需要任何 pre-bid 定价 API。

如果你的 AdMob 账户能够在 pre-bid 阶段提供展示级收益数据,也可以自行提供该精确价格来代替使用估算值——参见下方使用 pre-bid ILRD 手动输入价格

将 Google 付费事件回传给 CloudX(必需)

转发 Google 的付费事件是 Trusted Arbiter 接入 AdMob 与 Ad Manager 的必需环节,而不是可选的分析功能。CloudX 无法得知一次 Google 广告展示实际赚到了多少钱,其用于出价的估算值完全来自你回传的真实成交价格。

当通过仲裁获胜的 AdMob 或 Ad Manager 广告展示完成后,请把 Google 的展示级收入数据——即传给广告 paidEventHandlerGADAdValue——通过 reportRevenueData(_:) 转发进 CloudX SDK。AdMob 广告使用 CLXRevenuePlatformAdMob,Ad Manager 广告使用 CLXRevenuePlatformGAM。如果缺少这一回传,CloudX 将无法学习到你广告单元的真实成交价格,后续的仲裁估算质量会随之下降。

在 iOS 上,GADAdValue.value 是以货币单位表示的 NSDecimalNumber,可直接传入,无需换算。不要除以 1,000,000——只有 Android 和 Unity 版 Google Mobile Ads SDK 才以微单位(micros)上报广告价值。

func revenuePrecision(from precision: GADAdValuePrecision) -> CLXRevenuePrecision {
    switch precision {
    case .precise: return .exact
    case .estimated: return .estimated
    case .publisherProvided: return .publisherDefined
    default: return .undefined
    }
}

// Attach once to each Google ad you loaded as an arbiter candidate.
adMobInterstitial.paidEventHandler = { [weak adMobInterstitial] adValue in
    let servedBy = adMobInterstitial?.responseInfo.loadedAdNetworkResponseInfo
    let data = CLXRevenueData.revenueData(
        platform: .adMob,   // .gam for an Ad Manager ad
        revenue: adValue.value.doubleValue,
        adFormat: "interstitial"
    ) { builder in
        builder.currencyCode = adValue.currencyCode
        builder.precision = revenuePrecision(from: adValue.precision)
        builder.networkName = servedBy?.adSourceName
        builder.adUnitId = adMobAdUnitId
        builder.thirdPartyAdPlacementId = servedBy?.adSourceInstanceName
    }

    CloudXCore.shared.reportRevenueData(data)
}

每个广告对象只需挂载一次处理器。对于横幅广告和 MREC,AdMob 会在同一个视图自动刷新时重新触发付费事件,因此只挂载一次即可持续上报每一次刷新后的展示。完整的上报设置与全部字段列表参见发布商上报收入数据

用你已加载广告的广告单元 ID 创建出价:

// AdMob 广告单元。networkName 为可选参数;如果你知道获胜的广告来源,请传入它,
// 例如 responseInfo.loadedAdapterResponseInfo?.adSourceName。
let adMobBid = CLXArbiterBid.adMob(
    adUnitId: adMobAdUnitId,
    networkName: adMobNetworkName ?? "admob",
    manualRevenuePerImpressionUSD: nil,
    extras: [:]
)

// Ad Manager 广告单元 ID 的格式为 /NNNNNNN/placement/name。
let adManagerBid = CLXArbiterBid.gam(adUnitId: "/21775744923/example/interstitial")

let configuration = CLXArbiterConfiguration.configuration(
    bids: [CLXArbiterBid.cloudX(ad: cloudXAd), adMobBid, adManagerBid],
    builderBlock: nil
)

CloudXCore.shared.arbiter(with: configuration) { result in
    switch result.platform.name {
    case CLXArbiterPlatform.cloudX.name:
        cloudXInterstitial.show(from: viewController)
    case CLXArbiterPlatform.adMob.name:
        adMobInterstitial.present(fromRootViewController: viewController)
    case CLXArbiterPlatform.gam.name:
        adManagerInterstitial.present(fromRootViewController: viewController)
    default:
        break
    }
}

获胜的 Google 出价会报告自己的平台——CLXArbiterPlatform.adMobCLXArbiterPlatform.gam。你不再需要像处理自定义出价那样,通过 result.platformName 来区分这两个来源。

传入空白的广告单元 ID,出价对象仍会正常构建,不会导致应用崩溃,但它不具备可用的身份信息:既不会被定价,也会被服务器拒绝。

使用 pre-bid ILRD 手动输入价格

部分 AdMob 账户可以在 pre-bid 阶段提供展示级收益数据(ILRD):已加载广告的 GADAdValue 在加载时即可获取,早于广告展示。这是一项受账户控制的历史能力,请与你的 Google 客户团队确认你的账户是否已启用。在展示之前就已知的精确单次展示价格,是自行提供价格优于 CloudX 估算值的唯一场景。

将 pre-bid 广告价值作为 manualRevenuePerImpressionUSD 传入,它会覆盖估算值:

// GADAdValue.value 是已按货币单位表示的 NSDecimalNumber,直接原样传入即可,不要做任何除法。
let adMobBid = CLXArbiterBid.adMob(
    adUnitId: adMobAdUnitId,
    networkName: adMobNetworkName ?? "admob",
    manualRevenuePerImpressionUSD: preBidAdValue.value,
    extras: [:]
)

该数值的处理方式:

  • 0.0 是一个真实的价格。它表示该出价价值为零——而不是价格缺失。
  • 负值和非有限值不是有效价格,因此会被视为缺失并记录日志。
  • 空白的广告单元 ID 会导致手动价格被完全丢弃,因为没有身份信息的出价无法通过校验。

manualRevenuePerImpressionUSD 是单次展示的美元收益,而不是 CPM。请换算你的价格来源报告的数值:

  • AdMob 广告价值 在 iOS 上无需缩放。GADAdValue.value 是已按货币单位表示的 NSDecimalNumber,因此数值 0.005 就对应单次展示 0.005,直接原样传入即可——既不要除以 1,000,也不要除以 1,000,000。只有 Android 和 Unity 版 Google Mobile Ads SDK 才以微单位(micros)报告广告价值。
  • 非美元金额 必须先换算为美元。

分步示例:在 CloudX 与 LevelPlay 之间仲裁

本演练展示了应读取哪个 Unity LevelPlay 回调,以及应将哪些值传入仲裁器。示例使用插屏广告,但相同的字段映射适用于任何广告格式——横幅广告和 MREC 有何不同请参见支持的广告格式

加载两个候选项

创建 CloudX 和 LevelPlay 插屏广告,设置各自的 delegate,并为每个平台启动加载。

self.cloudXInterstitial = [[CloudXCore shared] createInterstitialWithAdUnitId:@"YOUR_CLOUDX_AD_UNIT_ID"];
self.cloudXInterstitial.delegate = self;
[self.cloudXInterstitial load];

self.levelPlayInterstitial = [[LPMInterstitialAd alloc] initWithAdUnitId:@"YOUR_LEVELPLAY_AD_UNIT_ID"];
self.levelPlayInterstitial.delegate = self;
[self.levelPlayInterstitial loadAd];

保存每个平台已加载的广告

LevelPlay 在其加载回调中提供 LPMAdInfo;CloudX 提供 CLXAd。请保存两者,下一步会从中读取仲裁输入。

// 属性:@property (nonatomic, strong) CLXAd *cloudXAd;
//       @property (nonatomic, strong) LPMAdInfo *levelPlayInfo;

// CLXInterstitialDelegate
- (void)didLoadAd:(CLXAd *)ad {
    self.cloudXAd = ad;
}

// LPMInterstitialAdDelegate
- (void)didLoadAdWithAdInfo:(LPMAdInfo *)adInfo {
    self.levelPlayInfo = adInfo;
}

将值映射为出价

LPMAdInfo 读取 LevelPlay 字段,并传给 CLXArbiterBid.levelPlay。CloudX 出价直接接收 CLXAd。只提交实际加载成功的平台。

LPMAdInfo 字段类型CLXArbiterBid.levelPlay 参数
adNetworkNSString *networkName
revenueNSNumber *revenue(用 .doubleValue 解包)
precisionNSString *precision
NSMutableArray<CLXArbiterBid *> *bids = [NSMutableArray array];

if (self.cloudXAd) {
    [bids addObject:[CLXArbiterBid cloudXBidWithAd:self.cloudXAd]];
}

if (self.levelPlayInfo) {
    CLXArbiterBid *levelPlayBid =
        [CLXArbiterBid levelPlayBidWithNetworkName:self.levelPlayInfo.adNetwork
                                           revenue:self.levelPlayInfo.revenue.doubleValue
                                         precision:self.levelPlayInfo.precision];
    [bids addObject:levelPlayBid];
}

CLXArbiterConfiguration *configuration =
    [CLXArbiterConfiguration configurationWithBids:bids];

运行仲裁器

将 configuration 连同完成回调传给仲裁器。请在两个插屏广告都加载完成(或加载失败)后再运行:跟踪各自的加载回调和加载失败,并且只提交成功加载的候选项。完成回调在主线程上执行。

[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
    [self showWinner:result];
}];

展示获胜平台

result.platform.name 与平台常量比较,展示获胜平台的广告。CLXArbiterPlatform.none 表示未选出获胜平台,此时不展示广告,继续应用流程。

- (void)showWinner:(CLXArbiterResult *)result {
    NSString *platform = result.platform.name;
    if ([platform isEqualToString:CLXArbiterPlatform.cloudX.name]) {
        [self.cloudXInterstitial showFromViewController:self];
    } else if ([platform isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
        [self.levelPlayInterstitial showAdWithViewController:self placementName:nil];
    }
    // CLXArbiterPlatform.none: 无获胜平台;不展示广告
}

下方的 ArbiterInterstitialController 将上述步骤封装为一个可复用的组件,会在到达广告位之前提前准备好获胜平台。

插屏广告示例

这个插屏广告示例会在两个平台之间仲裁:CloudX 和 Unity LevelPlay。在到达广告位之前先准备好获胜平台:

  1. 并行加载 CloudX 和 LevelPlay。
  2. 等待两个平台都加载完成或加载失败。
  3. 只将已加载的候选项提交给 Trusted Arbiter。
  4. 缓存选中的平台。
  5. 到达广告位时,立即展示缓存的获胜广告。

如果两个平台都加载失败,开始新的加载周期。如果到达广告位时还没有准备好获胜平台,则继续应用流程,不展示广告。

ArbiterInterstitialController.swift
/// 提前准备好 Trusted Arbiter 的获胜平台,以便在到达广告位时立即展示插屏广告。
///
/// 并行加载 CloudX 和 LevelPlay 插屏广告,等待两者都加载完成或加载失败,
/// 将已加载的候选项提交给 CloudXCore.shared.arbiter,并将选中的
/// CLXArbiterPlatform 缓存到 nextWinner 中。
final class ArbiterInterstitialController: NSObject {
    protocol Listener: AnyObject {
        /// 当仲裁器为下一次展示选出平台后调用。
        func arbiterInterstitialController(
            _ controller: ArbiterInterstitialController,
            didPrepareWinner platform: CLXArbiterPlatform
        )
    }

    weak var listener: Listener?

    private let cloudXInterstitial: CLXInterstitial
    private let levelPlayInterstitial: LPMInterstitialAd
    private var cloudXAd: CLXAd?
    private var cloudXLoadDone = false
    private var levelPlayAdInfo: LPMAdInfo?
    private var levelPlayLoadDone = false
    private var nextWinner: CLXArbiterPlatform?

    init(cloudXInterstitial: CLXInterstitial, levelPlayInterstitial: LPMInterstitialAd) {
        self.cloudXInterstitial = cloudXInterstitial
        self.levelPlayInterstitial = levelPlayInterstitial
        super.init()
        self.cloudXInterstitial.delegate = self
        self.levelPlayInterstitial.setDelegate(self)
    }

    /// 为每个当前没有缓存广告的平台启动加载。
    func loadMissingAds() {
        if cloudXAd == nil { cloudXInterstitial.load() }
        if levelPlayAdInfo == nil { levelPlayInterstitial.loadAd() }
    }

    /// 展示已准备好的获胜广告,仅当确实发起了展示调用时返回 true。
    ///
    /// 当没有准备好获胜平台或缓存的广告已不可用时返回 false,
    /// 此时会启动一次新的加载周期。
    func showAtPlacement(from viewController: UIViewController, placementName: String? = nil) -> Bool {
        guard let platformName = nextWinner?.name else { return false }

        if platformName == CLXArbiterPlatform.cloudX.name {
            return showCloudX(from: viewController, placementName: placementName)
        }

        if platformName == CLXArbiterPlatform.levelPlay.name {
            return showLevelPlay(from: viewController, placementName: placementName)
        }

        return false
    }

    /// 在两个平台都完成后运行仲裁器,然后缓存获胜平台。
    ///
    /// 在两个加载都完成之前提前返回。如果两个平台都没有加载成功,则重启加载周期;
    /// 否则将已加载的候选项提交给 CloudXCore.shared.arbiter。
    private func maybePrepareWinner() {
        guard cloudXLoadDone, levelPlayLoadDone else { return }

        if cloudXAd == nil && levelPlayAdInfo == nil {
            cloudXLoadDone = false
            levelPlayLoadDone = false
            loadMissingAds()
            return
        }

        var bids: [CLXArbiterBid] = []
        if let cloudXAd {
            bids.append(CLXArbiterBid.cloudX(ad: cloudXAd))
        }

        if let levelPlayAdInfo {
            bids.append(CLXArbiterBid.levelPlay(
                networkName: levelPlayAdInfo.adNetwork,
                revenue: levelPlayAdInfo.revenue?.doubleValue ?? 0,
                precision: levelPlayAdInfo.precision
            ))
        }

        let configuration = CLXArbiterConfiguration.configuration(bids: bids, builderBlock: nil)
        CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
            guard let self else { return }
            nextWinner = result.platform
            listener?.arbiterInterstitialController(self, didPrepareWinner: result.platform)
        }
    }

    private func showCloudX(from viewController: UIViewController, placementName: String?) -> Bool {
        if cloudXInterstitial.isReady {
            if let placementName {
                cloudXInterstitial.show(from: viewController, placement: placementName, customData: nil)
            } else {
                cloudXInterstitial.show(from: viewController)
            }
            return true
        }

        clearCloudXAndLoadMissingAds()
        return false
    }

    private func showLevelPlay(from viewController: UIViewController, placementName: String?) -> Bool {
        if levelPlayInterstitial.isAdReady() {
            levelPlayInterstitial.showAd(viewController: viewController, placementName: placementName)
            return true
        }

        clearLevelPlayAndLoadMissingAds()
        return false
    }

    private func clearCloudXAndLoadMissingAds() {
        cloudXAd = nil
        cloudXLoadDone = false
        nextWinner = nil
        loadMissingAds()
    }

    private func clearLevelPlayAndLoadMissingAds() {
        levelPlayAdInfo = nil
        levelPlayLoadDone = false
        nextWinner = nil
        loadMissingAds()
    }
}

extension ArbiterInterstitialController: CLXInterstitialDelegate {
    func didLoad(_ ad: CLXAd) {
        cloudXAd = ad
        cloudXLoadDone = true
        maybePrepareWinner()
    }

    func didFailToLoadAd(_ adUnitId: String, error: CLXError) {
        cloudXAd = nil
        cloudXLoadDone = true
        maybePrepareWinner()
    }

    func didDisplay(_ ad: CLXAd) {}

    func didFailToDisplay(_ ad: CLXAd, error: CLXError) {
        clearCloudXAndLoadMissingAds()
    }

    func didHide(_ ad: CLXAd) {
        clearCloudXAndLoadMissingAds()
    }

    func didClick(_ ad: CLXAd) {}
}

extension ArbiterInterstitialController: LPMInterstitialAdDelegate {
    func didLoadAd(with adInfo: LPMAdInfo) {
        levelPlayAdInfo = adInfo
        levelPlayLoadDone = true
        maybePrepareWinner()
    }

    func didFailToLoadAd(withAdUnitId adUnitId: String, error: Error) {
        levelPlayAdInfo = nil
        levelPlayLoadDone = true
        maybePrepareWinner()
    }

    func didChangeAdInfo(_ adInfo: LPMAdInfo) {
        levelPlayAdInfo = adInfo
    }

    func didDisplayAd(with adInfo: LPMAdInfo) {}

    func didFailToDisplayAd(with adInfo: LPMAdInfo, error: Error) {
        clearLevelPlayAndLoadMissingAds()
    }

    func didCloseAd(with adInfo: LPMAdInfo) {
        clearLevelPlayAndLoadMissingAds()
    }

    func didClickAd(with adInfo: LPMAdInfo) {}
}

showAtPlacement(from:placementName:) 仅当确实发起了展示调用时返回 truedidChangeAdInfo(_:) 会在 LevelPlay 广告保持加载状态期间更新缓存的 LevelPlay 候选项值。

对于 PubMatic OpenWrap,使用 CLXArbiterBid.pubMatic(price:partnerName:extras:) 创建第三方出价。如果仲裁服务不可用,SDK 会在传入的受支持出价输入中回退选择可比较美元出价最高的平台。

横幅广告和 MREC 仲裁

横幅广告和 MREC 是基于视图的广告格式:每个候选广告网络一旦加载成功,就会立即将广告渲染到视图中,无论该视图最终是否会显示在屏幕上。Trusted Arbiter 选出获胜方的方式不变,但你需要承担全屏格式不需要的两项额外职责。

关闭自动刷新

Trusted Arbiter 需要完全掌控何时请求新的填充、何时切换正在展示的广告,因此必须关闭每个网络自身的刷新定时器:

  • 在 CloudX 控制台中关闭该广告单元的自动刷新。
  • 创建 CLXBannerAdView 后立即对其调用 stopAutoRefresh(参见横幅广告 (320x50))。
  • 对你所仲裁的其他每个网络的对应 API 也关闭自动刷新。

视图挂载

只有获胜出价对应的视图可以被加入视图层级。落败网络的横幅视图一旦被加入父视图(superview),仍会渲染并触发自己的展示事件,因此在某个非获胜视图赢得后续轮次之前(或除非它赢得后续轮次),必须让它保持在屏幕之外(不要对其调用 addSubview: / addSubview(_:))。这与标准横幅广告集成不同——标准集成会在创建视图时立即通过 addSubview 挂载;而在 Trusted Arbiter 中,视图不能在创建时挂载,只能在仲裁选出获胜方之后才挂载。

刷新周期

关闭自动刷新后,需要自行驱动整个周期:

  1. 并行发起加载,将已加载的候选项提交给仲裁器,并挂载获胜方的视图。
  2. 获胜广告的展示事件一旦触发,立即为该获胜网络发起新的加载。
  3. 保留未获胜网络已经加载好的广告,用于下一轮仲裁;只对上一轮未能填充的网络重新发起加载请求。
  4. 待未完成的加载响应全部返回后,再次运行仲裁器。
  5. 每 20-30 秒刷新一次正在展示的广告,每次替换为新的获胜视图。刷新间隔短于 20 秒会降低 CPM 表现。

示例

下方的 ArbiterBannerController 在 CloudX 和 LevelPlay 的横幅广告之间进行仲裁,任意时刻只挂载一个视图,并驱动上述刷新周期。

/// 以 20-30 秒为周期,在 CloudX 和 LevelPlay 的横幅广告之间进行仲裁。
///
/// 只挂载获胜出价对应的视图。未获胜的视图保持已加载但不挂载的状态,
/// 因此永远不会渲染或触发展示事件。当前展示的获胜广告触发展示事件后,
/// 会为该网络发起新的加载,并保留另一个网络已经加载好的广告,用于下一轮仲裁。
@interface ArbiterBannerController () <CLXBannerDelegate, CLXAdRevenueDelegate, LPMBannerAdViewDelegate>
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, weak) UIViewController *presentingViewController;
@property (nonatomic, strong) CLXBannerAdView *cloudXBanner;
@property (nonatomic, strong) LPMBannerAdView *levelPlayBanner;
@property (nonatomic, strong) CLXAd *cloudXAd;
@property (nonatomic, assign) BOOL cloudXLoadDone;
@property (nonatomic, strong) LPMAdInfo *levelPlayAdInfo;
@property (nonatomic, assign) BOOL levelPlayLoadDone;
@property (nonatomic, copy) NSString *attachedPlatformName;
@property (nonatomic, strong) NSTimer *refreshTimer;
@end

@implementation ArbiterBannerController

- (instancetype)initWithContainerView:(UIView *)containerView
                presentingViewController:(UIViewController *)presentingViewController
                       cloudXAdUnitId:(NSString *)cloudXAdUnitId
                    levelPlayAdUnitId:(NSString *)levelPlayAdUnitId {
    self = [super init];
    if (self) {
        _containerView = containerView;
        _presentingViewController = presentingViewController;

        _cloudXBanner = [[CloudXCore shared] createBannerWithAdUnitId:cloudXAdUnitId];
        _cloudXBanner.delegate = self;
        _cloudXBanner.revenueDelegate = self;
        [_cloudXBanner stopAutoRefresh];

        LPMBannerAdViewConfigBuilder *levelPlayConfigBuilder = [[LPMBannerAdViewConfigBuilder alloc] init];
        LPMBannerAdViewConfig *levelPlayConfig = [levelPlayConfigBuilder build];
        _levelPlayBanner = [[LPMBannerAdView alloc] initWithAdUnitId:levelPlayAdUnitId
                                                                config:levelPlayConfig];
        _levelPlayBanner.delegate = self;
        // LevelPlay 的自动刷新需要通过 LevelPlay 自身的配置(控制台/API)关闭。
    }
    return self;
}

/// 为每个当前没有已填充广告的网络启动加载。
- (void)loadMissingAds {
    if (!self.cloudXAd) { [self.cloudXBanner load]; }
    if (!self.levelPlayAdInfo) {
        [self.levelPlayBanner loadAdWithViewController:self.presentingViewController];
    }
}

/// 启动周期性的 20-30 秒刷新定时器。仅在首次加载周期开始后调用一次。
- (void)startRefreshTimer {
    [self.refreshTimer invalidate];
    self.refreshTimer = [NSTimer scheduledTimerWithTimeInterval:25.0
                                                          target:self
                                                        selector:@selector(runArbiterIfReady)
                                                        userInfo:nil
                                                         repeats:YES];
}

- (void)runArbiterIfReady {
    if (!self.cloudXLoadDone || !self.levelPlayLoadDone) { return; }

    NSMutableArray<CLXArbiterBid *> *bids = [NSMutableArray array];
    if (self.cloudXAd) {
        [bids addObject:[CLXArbiterBid cloudXBidWithAd:self.cloudXAd]];
    }
    if (self.levelPlayAdInfo) {
        [bids addObject:[CLXArbiterBid levelPlayBidWithNetworkName:self.levelPlayAdInfo.adNetwork
                                                            revenue:self.levelPlayAdInfo.revenue.doubleValue
                                                          precision:self.levelPlayAdInfo.precision]];
    }
    if (bids.count == 0) { return; }

    CLXArbiterConfiguration *configuration = [CLXArbiterConfiguration configurationWithBids:bids];
    [[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
        [self attachWinner:result.platform.name];
    }];
}

/// 卸载上一个获胜方的视图,挂载新获胜方的视图,并在获胜网络的展示事件触发后为其发起新的加载。
- (void)attachWinner:(NSString *)platformName {
    [self.cloudXBanner removeFromSuperview];
    [self.levelPlayBanner removeFromSuperview];

    if ([platformName isEqualToString:CLXArbiterPlatform.cloudX.name]) {
        [self.containerView addSubview:self.cloudXBanner];
        self.attachedPlatformName = platformName;
    } else if ([platformName isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
        [self.containerView addSubview:self.levelPlayBanner];
        self.attachedPlatformName = platformName;
    } else {
        self.attachedPlatformName = nil;
    }
}

#pragma mark - CLXBannerDelegate

- (void)didLoadAd:(CLXAd *)ad {
    self.cloudXAd = ad;
    self.cloudXLoadDone = YES;
    [self runArbiterIfReady];
}

- (void)didFailToLoadAd:(NSString *)adUnitId error:(CLXError *)error {
    self.cloudXAd = nil;
    self.cloudXLoadDone = YES;
    [self runArbiterIfReady];
}

#pragma mark - CLXAdRevenueDelegate

- (void)didPayRevenueForAd:(CLXAd *)ad {
    if ([self.attachedPlatformName isEqualToString:CLXArbiterPlatform.cloudX.name]) {
        self.cloudXAd = nil;
        self.cloudXLoadDone = NO;
        [self.cloudXBanner load];
    }
}

#pragma mark - LPMBannerAdViewDelegate

- (void)didLoadAdWithAdInfo:(LPMAdInfo *)adInfo {
    self.levelPlayAdInfo = adInfo;
    self.levelPlayLoadDone = YES;
    [self runArbiterIfReady];
}

- (void)didFailToLoadAdWithAdUnitId:(NSString *)adUnitId error:(NSError *)error {
    self.levelPlayAdInfo = nil;
    self.levelPlayLoadDone = YES;
    [self runArbiterIfReady];
}

- (void)didDisplayAdWithAdInfo:(LPMAdInfo *)adInfo {
    if ([self.attachedPlatformName isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
        self.levelPlayAdInfo = nil;
        self.levelPlayLoadDone = NO;
        [self.levelPlayBanner loadAdWithViewController:self.presentingViewController];
    }
}

@end

didPayRevenue(for:)(CloudX 的展示信号)和 didDisplayAd(with:)(LevelPlay 的展示信号)会触发当前已挂载网络的下一次加载;未获胜网络已经加载好的广告会保持不变,直到它赢得某一轮或被消耗为止。runArbiterIfReady() 既会被加载回调触发,也会被刷新定时器触发,因此只有当两个网络都已就绪时,某一轮才会真正替换已挂载的视图。

自定义出价输入

当需要让 Trusted Arbiter 比较 CloudX 与没有专用出价辅助方法的第三方平台时,可以使用 CLXArbiterBid.custom(...)

CLXArbiterBid *customBid =
    [CLXArbiterBid customBidWithPlatformName:@"my_mediation_platform"
                                 networkName:@"winning_demand_source"
                     revenuePerImpressionUSD:0.00125
                                   precision:CLXArbiterPrecision.exact
                                      extras:@{@"ad_unit": @"third-party-ad-unit-id"}];

CLXArbiterConfiguration *configuration =
    [CLXArbiterConfiguration configurationWithBids:@[
        [CLXArbiterBid cloudXBidWithAd:cloudXAd],
        customBid
    ]];

当自定义出价获胜时,result.platformCLXArbiterPlatform.customresult.platformName 包含创建出价时传入的 platformNamerevenuePerImpressionUSD 应传入单次展示的美元收益,而不是 CPM。使用 CLXArbiterPrecision.exactestimatedpublisherDefinedundefined 描述该收益值的精度。