import Foundation // AggregatorBid 私有结构体 private struct AggregatorBid { let config: AggregatorConfig /** * 是否进行自抬价。 * 在某个聚合show完成时,会向竞价队列头部加入一个此聚合当前 revenue * selfRaise 自抬价的bid * 在设置为true时,同时设置[floor]为revenue,基于此值进行自抬价 */ let useSelfRaise: Bool /** * 当前bid的固定底价,若为nil,则使用winners中最大的revenue, see [MabWinners.maxRevenue] * 单位为美元 每次展示 */ let floor: Double? /** * 本次参与竞价请求的原因 */ let reason: RequestReason? init(config: AggregatorConfig, useSelfRaise: Bool = false, floor: Double? = nil, reason: RequestReason? = nil) { self.config = config self.useSelfRaise = useSelfRaise self.floor = floor self.reason = reason } var raiseRate: Double { return useSelfRaise ? config.selfRaiseRate ?? config.raiseRate : config.raiseRate } } // 扩展属性 extension AggregatorConfig { var deferRetryUntilShown: Bool { return adPlatform == AdPlatform.adMob.name } } /** * 储存已填充的广告 */ private class MabWinners { private var winners: [String: T] = [:] private let onEvict: ((T) -> Void)? init(onEvict: ((T) -> Void)? = nil) { self.onEvict = onEvict } /** * 清除已过期的缓存 */ func evictCache() { let expiredKeys = winners.filter { (_, bidder) in if bidder.isExpired != false { onEvict?(bidder) return true } return false }.map { $0.key } for key in expiredKeys { winners.removeValue(forKey: key) } } /** * 添加一个已填充的广告 */ func add(bidder: T) { evictCache() winners[bidder.aggregatorId] = bidder } /** * 是否包含此聚合的已填充广告 */ func contains(aggregatorId: String) -> Bool { evictCache() return winners[aggregatorId] != nil } /** * 已填充广告内当前最高的填充价格,若无广告填充或全部过期则返回0 */ func maxRevenue() -> Double { evictCache() if winners.isEmpty { return 0.0 } return winners.values.map { $0.revenue }.max() ?? 0.0 } /** * 查看当前最高填充价格的广告 */ func peekMaxRevenue() -> T? { evictCache() return winners.values.max(by: { if($0.revenue == $1.revenue) { // 优先级数值越低(优先级越高)越排序靠后(升序排列) return $0.config.priority > $1.config.priority } return $0.revenue < $1.revenue }) } /** * 移出当前最高填充价格的广告 */ func pollMaxRevenue() -> T? { evictCache() guard let entry = peekMaxRevenue() else { return nil } return winners.removeValue(forKey: entry.aggregatorId) } /** * 是否存在已填充广告 */ func isEmpty() -> Bool { evictCache() return winners.isEmpty } /** * 清空所有填充广告 */ func clear() { winners.removeAll() } func buildStateLog() -> String { var result = "winners[" for winner in winners { result += "\(winner.key)(\(winner.value.revenue))," } result += "]" return result } } /** * 对多次load敏感的聚合(如AdMob)进行load重试控制,确保在首次load后(无论失败或成功),后续load与重新load必须在一次广告show之后才能进行。 * 换句话说-> "每次广告show完,此聚合重新获得一次load机会(不能累加)" */ private class RetryGate { // Swift没有AtomicInteger,所以使用一个类包装 private class AtomicInteger { private var value: Int init(_ initialValue: Int = 0) { self.value = initialValue } func get() -> Int { return value } func set(_ newValue: Int) { value = newValue } func incrementAndGet() -> Int { value += 1 return value } } private var version = AtomicInteger() // 每次ad show成功时更新一个版本 private var lastUsed: [String: AtomicInteger] = [:] func onAdShown() { _ = version.incrementAndGet() } /** * 对比重试版本与show版本,若不一致则代表有机会进行retry,若一致则代表需等待下次show完 */ func canRetry(aggregatorId: String) -> Bool { return getLastUsed(aggregatorId: aggregatorId).get() < version.get() } /** * 标记本次重试版本 */ func markRetryUsed(aggregatorId: String) { getLastUsed(aggregatorId: aggregatorId).set(version.get()) } private func getLastUsed(aggregatorId: String) -> AtomicInteger { if let lastUsed = lastUsed[aggregatorId] { return lastUsed } else { let atomicInteger = AtomicInteger(-1) lastUsed[aggregatorId] = atomicInteger return atomicInteger } } } private func selectAdUnitSpec( aggregatorConfig: AggregatorConfig, floor: Double, selfRaise: Bool = false ) -> AdUnitSpec? { let adUnitIds = aggregatorConfig.adUnitIds if adUnitIds.isEmpty { return nil } let targetFloor = floor * (selfRaise ? aggregatorConfig.selfRaiseRate ?? aggregatorConfig.raiseRate : aggregatorConfig.raiseRate) let firstGtIndex = adUnitIds.firstIndex { $0.floor >= targetFloor } if firstGtIndex == nil { return adUnitIds.last } if firstGtIndex == 0 { return adUnitIds.first } // 寻找最接近targetFloor的一个 let index = firstGtIndex! let nearestIndex = (abs(targetFloor - adUnitIds[index].floor) <= abs(adUnitIds[index - 1].floor - targetFloor)) ? index : index - 1 let selection = adUnitIds[nearestIndex] return selection } enum RequestReason: String { case INIT = "init" case FAIL = "fail" case SHOW = "show" case EXPIRATION = "exp" var statsName: String { return rawValue } } protocol MabBidder { var id: Int { get } var config: AggregatorConfig { get } var isExpired: Bool? { get } var revenue: Double { get } func load(reason: RequestReason?) -> Bool func destroy() } extension MabBidder { var aggregatorId: String { return config.aggregatorId } } // 静态方法转换为类函数 class MabBidderUtils { static func adjustRevenue(ad: FusionAd, floor: Double) -> Double { switch ad.adPlatform { case .max: return ad.revenue case .ironSource: return max(ad.revenue, floor) case .adMob: return max(ad.revenue, floor) default: return max(ad.revenue, floor) } } } class MabAuctioneer { enum RequestResult { case success case busy case noAvailableSource } private let tag: String private let faUnitId: String private let priorityAggregators: [AggregatorConfig] private var candidatesQueue: [AggregatorBid] = [] private let bidderFactory: (AggregatorConfig, AdUnitSpec) -> T? private var bidding: T? private var bidders: [String: T] = [:] private let winners: MabWinners private let retryGate = RetryGate() var hasWinner: Bool { return !winners.isEmpty() } init( engineId: Int, faUnitId: String, aggregatorConfigs: [AggregatorConfig], bidderFactory: @escaping (AggregatorConfig, AdUnitSpec) -> T? ) { self.tag = "MabAuctioneer@\(engineId)" self.faUnitId = faUnitId self.priorityAggregators = aggregatorConfigs.sorted(by: { $0.priority < $1.priority }) self.bidderFactory = bidderFactory self.winners = MabWinners() } private func logI(_ message: String) { Logger.i(tag: tag, message: message) } private func logD(_ message: String) { Logger.d(tag: tag, message: message) } private func logW(_ message: String) { Logger.w(tag: tag, message: message) } private func logE(_ message: String) { Logger.e(tag: tag, message: message) } private func logState() { logI("mab state: \(buildStateLog())") } private func obtainBidder( config: AggregatorConfig, adUnitSpec: AdUnitSpec ) -> T? { if let bidder = bidders[adUnitSpec.adUnitId] { return bidder } else { if let newBidder = bidderFactory(config, adUnitSpec) { bidders[adUnitSpec.adUnitId] = newBidder return newBidder } return nil } } func requestNext() -> RequestResult { logI("requestNext") let result = internalRequestNext() logI("requestNext result \(result)") logState() return result } private func internalRequestNext() -> RequestResult { if bidding != nil { return .busy } while !candidatesQueue.isEmpty { guard let bid = candidatesQueue.first else { break } candidatesQueue.removeFirst() if winners.contains(aggregatorId: bid.config.aggregatorId) { // 若当前bid已存在填充,则忽略 continue } guard let adUnitSpec = selectAdUnitSpec( aggregatorConfig: bid.config, floor: bid.floor ?? winners.maxRevenue(), selfRaise: bid.useSelfRaise ) else { continue } logD("selected \(adUnitSpec.adUnitId) with \(adUnitSpec.floorEcpm)") if let bidder = obtainBidder(config: bid.config, adUnitSpec: adUnitSpec) { if bidder.load(reason: bid.reason) { bidding = bidder return .success } } } return .noAvailableSource } func refillCandidatesQueue(reason: RequestReason? = nil) { logI("refilling candidates") logState() for aggregator in priorityAggregators { fillCandidate(AggregatorBid(config: aggregator, useSelfRaise: false, reason: reason)) } logI("candidates refilled") logState() } private func fillCandidate(_ bid: AggregatorBid) { let aggregator = bid.config // 已填充并未过期的,忽略 if winners.contains(aggregatorId: aggregator.aggregatorId) { logD("refilling - \(aggregator.aggregatorId) filled, skip") return } // 正在填充中的,忽略 if let currentBidding = bidding, currentBidding.aggregatorId == aggregator.aggregatorId { logW("refilling - \(aggregator.aggregatorId) is loading, skip") return } // 如果未满足某个聚合show完一次才能重试一次的条件,忽略。 if aggregator.deferRetryUntilShown { if !retryGate.canRetry(aggregatorId: aggregator.aggregatorId) { logD("refilling - \(aggregator.aggregatorId) deferred until next shown") return } retryGate.markRetryUsed(aggregatorId: aggregator.aggregatorId) } logD("refilling - \(aggregator.aggregatorId) added to queue") candidatesQueue.append(bid) } private func onCacheEvict(_ bidder: T) { // candidatesQueue.append(AggregatorBid(config: bidder.config, reason: .EXPIRATION)) } func reset() { logI("reset") for bidder in bidders.values { bidder.destroy() } bidding?.destroy() bidding = nil winners.clear() bidders.removeAll() candidatesQueue.removeAll() refillCandidatesQueue() } func onShown(winner: T) { logI("winner shown, \(winner)") logState() retryGate.onAdShown() // 消耗完后,将待填充聚合队列清空,等待hidden后重新填补队列。 candidatesQueue.removeAll() // 如果配置了自抬价字段,则进入自抬价逻辑 if winner.config.selfRaiseRate != nil { // 当前show成功,加入自抬价意图,自抬价如果失败则重新请求一轮 candidatesQueue.append( AggregatorBid( config: winner.config, useSelfRaise: true, floor: winner.revenue, reason: .SHOW ) ) } } func onLoaded(bidderId: Int) { logI("onLoaded \(bidderId)") if let bidding = bidding, bidding.id == bidderId { winners.add(bidder: bidding) self.bidding = nil } else { logW("onLoaded bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)") } logState() } func onLoadFailed(bidderId: Int) { logI("onLoadFailed \(bidderId)") if let bidder = bidding, bidder.id == bidderId { bidding = nil } else { logW("onLoadFailed bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)") } logState() } func pollMaxRevenue() -> T? { return winners.pollMaxRevenue() } func peekMaxRevenue() -> T? { return winners.peekMaxRevenue() } func evictCache() { winners.evictCache() } private func buildStateLog() -> String { var result = "faUnitId(\(faUnitId)), " result += winners.buildStateLog() result += ", " result += "bidding[" if let bidding = bidding { result += "\(bidding)" } else { result += "null" } result += "], " result += "candidates[" for bid in candidatesQueue { result += "\(bid.config.aggregatorId)(" result += "\(bid.raiseRate)" if bid.useSelfRaise { result += " self-raise" } result += "), " } result += "]" return result } }