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);
}];// cloudXAd 是 CloudX 加载回调中的 CLXAd 对象。
// levelPlayAdInfo 是 Unity LevelPlay 的广告信息对象。
// pubMaticPrice 和 pubMaticPartnerName 来自 PubMatic/OpenWrap 出价对象。
let cloudXBid = CLXArbiterBid.cloudX(ad: cloudXAd)
let levelPlayBid = CLXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue?.doubleValue ?? 0,
precision: levelPlayAdInfo.precision
)
let pubMaticBid = CLXArbiterBid.pubMatic(
price: pubMaticPrice,
partnerName: pubMaticPartnerName,
extras: nil
)
let configuration = CLXArbiterConfiguration.configuration(
bids: [cloudXBid, levelPlayBid, pubMaticBid],
builderBlock: nil
)
CloudXCore.shared.arbiter(with: configuration) { result in
print("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.cloudX、levelPlay 或 pubMatic;当无法选出获胜平台时(例如未传入任何出价),则为 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 的展示级收入数据——即传给广告 paidEventHandler 的 GADAdValue——通过 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)
}static CLXRevenuePrecision *CLXRevenuePrecisionFromGAD(GADAdValuePrecision precision) {
switch (precision) {
case GADAdValuePrecisionPrecise: return CLXRevenuePrecision.exact;
case GADAdValuePrecisionEstimated: return CLXRevenuePrecision.estimated;
case GADAdValuePrecisionPublisherProvided: return CLXRevenuePrecision.publisherDefined;
case GADAdValuePrecisionUnknown: return CLXRevenuePrecision.undefined;
}
return CLXRevenuePrecision.undefined;
}
// Attach once to each Google ad you loaded as an arbiter candidate.
__weak GADInterstitialAd *weakAd = adMobInterstitial;
adMobInterstitial.paidEventHandler = ^(GADAdValue *adValue) {
GADAdNetworkResponseInfo *servedBy = weakAd.responseInfo.loadedAdNetworkResponseInfo;
CLXRevenueData *data =
[CLXRevenueData revenueDataWithPlatform:CLXRevenuePlatformAdMob // CLXRevenuePlatformGAM for Ad Manager
revenue:adValue.value.doubleValue
adFormat:@"interstitial"
builderBlock:^(CLXRevenueDataBuilder *builder) {
builder.currencyCode = adValue.currencyCode;
builder.precision = CLXRevenuePrecisionFromGAD(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
}
}CLXArbiterBid *adMobBid =
[CLXArbiterBid adMobBidWithAdUnitId:adMobAdUnitId
networkName:adMobNetworkName ?: @"admob"
manualRevenuePerImpressionUSD:nil
extras:@{}];
CLXArbiterBid *adManagerBid =
[CLXArbiterBid gamBidWithAdUnitId:@"/21775744923/example/interstitial"];
CLXArbiterConfiguration *configuration =
[CLXArbiterConfiguration configurationWithBids:@[
[CLXArbiterBid cloudXBidWithAd:cloudXAd],
adMobBid,
adManagerBid
]];
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
if ([result.platform.name isEqualToString:CLXArbiterPlatform.cloudX.name]) {
[cloudXInterstitial showFromViewController:viewController];
} else if ([result.platform.name isEqualToString:CLXArbiterPlatform.adMob.name]) {
[adMobInterstitial presentFromRootViewController:viewController];
} else if ([result.platform.name isEqualToString:CLXArbiterPlatform.gam.name]) {
[adManagerInterstitial presentFromRootViewController:viewController];
}
}];获胜的 Google 出价会报告自己的平台——CLXArbiterPlatform.adMob 或 CLXArbiterPlatform.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: [:]
)// GADAdValue.value 是已按货币单位表示的 NSDecimalNumber,直接原样传入即可,不要做任何除法。
CLXArbiterBid *adMobBid =
[CLXArbiterBid adMobBidWithAdUnitId: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];cloudXInterstitial = CloudXCore.shared.createInterstitial(adUnitId: "YOUR_CLOUDX_AD_UNIT_ID")
cloudXInterstitial?.delegate = self
cloudXInterstitial?.load()
levelPlayInterstitial = LPMInterstitialAd(adUnitId: "YOUR_LEVELPLAY_AD_UNIT_ID")
levelPlayInterstitial.setDelegate(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;
}var cloudXAd: CLXAd?
var levelPlayInfo: LPMAdInfo?
// CLXInterstitialDelegate
func didLoad(_ ad: CLXAd) {
cloudXAd = ad
}
// LPMInterstitialAdDelegate
func didLoadAd(with adInfo: LPMAdInfo) {
levelPlayInfo = adInfo
}将值映射为出价
从 LPMAdInfo 读取 LevelPlay 字段,并传给 CLXArbiterBid.levelPlay。CloudX 出价直接接收 CLXAd。只提交实际加载成功的平台。
LPMAdInfo 字段 | 类型 | CLXArbiterBid.levelPlay 参数 |
|---|---|---|
adNetwork | NSString * | networkName |
revenue | NSNumber * | revenue(用 .doubleValue 解包) |
precision | NSString * | 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];var bids: [CLXArbiterBid] = []
if let cloudXAd {
bids.append(CLXArbiterBid.cloudX(ad: cloudXAd))
}
if let levelPlayInfo {
bids.append(CLXArbiterBid.levelPlay(
networkName: levelPlayInfo.adNetwork,
revenue: levelPlayInfo.revenue?.doubleValue ?? 0,
precision: levelPlayInfo.precision
))
}
let configuration = CLXArbiterConfiguration.configuration(
bids: bids,
builderBlock: nil
)运行仲裁器
将 configuration 连同完成回调传给仲裁器。请在两个插屏广告都加载完成(或加载失败)后再运行:跟踪各自的加载回调和加载失败,并且只提交成功加载的候选项。完成回调在主线程上执行。
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
[self showWinner:result];
}];CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
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: 无获胜平台;不展示广告
}func showWinner(_ result: CLXArbiterResult) {
switch result.platform.name {
case CLXArbiterPlatform.cloudX.name:
cloudXInterstitial?.show(from: self)
case CLXArbiterPlatform.levelPlay.name:
levelPlayInterstitial.showAd(viewController: self, placementName: nil)
default:
break // CLXArbiterPlatform.none: 无获胜平台;不展示广告
}
}下方的 ArbiterInterstitialController 将上述步骤封装为一个可复用的组件,会在到达广告位之前提前准备好获胜平台。
插屏广告示例
这个插屏广告示例会在两个平台之间仲裁:CloudX 和 Unity LevelPlay。在到达广告位之前先准备好获胜平台:
- 并行加载 CloudX 和 LevelPlay。
- 等待两个平台都加载完成或加载失败。
- 只将已加载的候选项提交给 Trusted Arbiter。
- 缓存选中的平台。
- 到达广告位时,立即展示缓存的获胜广告。
如果两个平台都加载失败,开始新的加载周期。如果到达广告位时还没有准备好获胜平台,则继续应用流程,不展示广告。
/// 提前准备好 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:) 仅当确实发起了展示调用时返回 true。didChangeAdInfo(_:) 会在 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 中,视图不能在创建时挂载,只能在仲裁选出获胜方之后才挂载。
刷新周期
关闭自动刷新后,需要自行驱动整个周期:
- 并行发起加载,将已加载的候选项提交给仲裁器,并挂载获胜方的视图。
- 获胜广告的展示事件一旦触发,立即为该获胜网络发起新的加载。
- 保留未获胜网络已经加载好的广告,用于下一轮仲裁;只对上一轮未能填充的网络重新发起加载请求。
- 待未完成的加载响应全部返回后,再次运行仲裁器。
- 每 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/// 以 20-30 秒为周期,在 CloudX 和 LevelPlay 的横幅广告之间进行仲裁。
///
/// 只挂载获胜出价对应的视图。未获胜的视图保持已加载但不挂载的状态,
/// 因此永远不会渲染或触发展示事件。当前展示的获胜广告触发展示事件后,
/// 会为该网络发起新的加载,并保留另一个网络已经加载好的广告,用于下一轮仲裁。
final class ArbiterBannerController: NSObject {
private weak var containerView: UIView?
private weak var presentingViewController: UIViewController?
private let cloudXBanner: CLXBannerAdView
private let levelPlayBanner: LPMBannerAdView
private var cloudXAd: CLXAd?
private var cloudXLoadDone = false
private var levelPlayAdInfo: LPMAdInfo?
private var levelPlayLoadDone = false
private var attachedPlatformName: String?
private var refreshTimer: Timer?
init(containerView: UIView, presentingViewController: UIViewController, cloudXAdUnitId: String, levelPlayAdUnitId: String) {
self.containerView = containerView
self.presentingViewController = presentingViewController
cloudXBanner = CloudXCore.shared.createBanner(adUnitId: cloudXAdUnitId)
let levelPlayConfig = LPMBannerAdViewConfigBuilder().build()
levelPlayBanner = LPMBannerAdView(adUnitId: levelPlayAdUnitId, config: levelPlayConfig)
super.init()
cloudXBanner.delegate = self
cloudXBanner.revenueDelegate = self
cloudXBanner.stopAutoRefresh()
levelPlayBanner.setDelegate(self)
// LevelPlay 的自动刷新需要通过 LevelPlay 自身的配置(控制台/API)关闭。
}
/// 为每个当前没有已填充广告的网络启动加载。
func loadMissingAds() {
if cloudXAd == nil { cloudXBanner.load() }
if levelPlayAdInfo == nil, let presentingViewController {
levelPlayBanner.loadAd(with: presentingViewController)
}
}
/// 启动周期性的 20-30 秒刷新定时器。仅在首次加载周期开始后调用一次。
func startRefreshTimer() {
refreshTimer?.invalidate()
refreshTimer = Timer.scheduledTimer(withTimeInterval: 25.0, repeats: true) { [weak self] _ in
self?.runArbiterIfReady()
}
}
private func runArbiterIfReady() {
guard cloudXLoadDone, levelPlayLoadDone else { 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
))
}
guard !bids.isEmpty else { return }
let configuration = CLXArbiterConfiguration.configuration(bids: bids, builderBlock: nil)
CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
self?.attachWinner(result.platform.name)
}
}
/// 卸载上一个获胜方的视图,挂载新获胜方的视图,并在获胜网络的展示事件触发后为其发起新的加载。
private func attachWinner(_ platformName: String) {
cloudXBanner.removeFromSuperview()
levelPlayBanner.removeFromSuperview()
switch platformName {
case CLXArbiterPlatform.cloudX.name:
containerView?.addSubview(cloudXBanner)
attachedPlatformName = platformName
case CLXArbiterPlatform.levelPlay.name:
containerView?.addSubview(levelPlayBanner)
attachedPlatformName = platformName
default:
attachedPlatformName = nil
}
}
}
extension ArbiterBannerController: CLXBannerDelegate {
func didLoad(_ ad: CLXAd) {
cloudXAd = ad
cloudXLoadDone = true
runArbiterIfReady()
}
func didFailToLoadAd(_ adUnitId: String, error: CLXError) {
cloudXAd = nil
cloudXLoadDone = true
runArbiterIfReady()
}
}
extension ArbiterBannerController: CLXAdRevenueDelegate {
func didPayRevenue(for ad: CLXAd) {
guard attachedPlatformName == CLXArbiterPlatform.cloudX.name else { return }
cloudXAd = nil
cloudXLoadDone = false
cloudXBanner.load()
}
}
extension ArbiterBannerController: LPMBannerAdViewDelegate {
func didLoadAd(with adInfo: LPMAdInfo) {
levelPlayAdInfo = adInfo
levelPlayLoadDone = true
runArbiterIfReady()
}
func didFailToLoadAd(withAdUnitId adUnitId: String, error: Error) {
levelPlayAdInfo = nil
levelPlayLoadDone = true
runArbiterIfReady()
}
func didDisplayAd(with adInfo: LPMAdInfo) {
guard attachedPlatformName == CLXArbiterPlatform.levelPlay.name,
let presentingViewController else { return }
levelPlayAdInfo = nil
levelPlayLoadDone = false
levelPlayBanner.loadAd(with: presentingViewController)
}
}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
]];let customBid = CLXArbiterBid.custom(
platformName: "my_mediation_platform",
networkName: "winning_demand_source",
revenuePerImpressionUSD: 0.00125,
precision: .exact,
extras: ["ad_unit": "third-party-ad-unit-id"]
)
let configuration = CLXArbiterConfiguration.configuration(
bids: [CLXArbiterBid.cloudX(ad: cloudXAd), customBid],
builderBlock: nil
)当自定义出价获胜时,result.platform 为 CLXArbiterPlatform.custom,result.platformName 包含创建出价时传入的 platformName。revenuePerImpressionUSD 应传入单次展示的美元收益,而不是 CPM。使用 CLXArbiterPrecision.exact、estimated、publisherDefined 或 undefined 描述该收益值的精度。