545 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
	
		
		
			
		
	
	
			545 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
	
|  | 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<T: MabBidder> { | |||
|  |     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<T: MabBidder> { | |||
|  |     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<T> | |||
|  |     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<T>() | |||
|  |     } | |||
|  |      | |||
|  |     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 | |||
|  |     } | |||
|  | } |