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
 | ||
|     }
 | ||
| }
 |