// // MabInterstitialEngine.swift // Pods // // Created by 250102 on 2025/5/10. // import Foundation // 类型别名定义 typealias InterstitialAdObtainer = (_ adPlatform: AdPlatform, _ adConfig: AdConfig) -> GuruInterstitialAd? // InterstitialBidder 类实现 private class InterstitialBidder: CustomStringConvertible, MabBidder, FusionAdListener { // 静态属性和常量 private static let idPool = AtomicInteger(0) static let TAG = "InterstitialBidder" // 实例属性 private let engine: InterstitialAdEngine var config: AggregatorConfig private let ad: GuruInterstitialAd private let adUnitSpec: AdUnitSpec private let faUnitId: String var id: Int var revenue: Double = 0.0 private var fusionAd: FusionAd? private var loadedTimeInMillis: Int64? private var loaded: Bool { return loadedTimeInMillis != nil } var isExpired: Bool? { get { // 若配置为0 则永不过期 if config.cacheExpireInSecond == 0.0 { return false } guard let loadedTime = loadedTimeInMillis else { return nil } return (SystemClock.elapsedRealtime() - loadedTime) > Int64(config.cacheExpireInSecond * 1000) } } var isFilled: Bool { return loaded && !(isExpired ?? true) } func getFusionAd() -> FusionAd? { return fusionAd } // 私有属性 private let loadTimeOutToken = UUID() private let cacheExpireToken = UUID() private var loadStartTime: Int64? // 初始化方法 init( engine: InterstitialAdEngine, config: AggregatorConfig, ad: GuruInterstitialAd, adUnitSpec: AdUnitSpec, faUnitId: String ) { self.engine = engine self.config = config self.ad = ad self.adUnitSpec = adUnitSpec self.faUnitId = faUnitId self.id = InterstitialBidder.idPool.incrementAndGet() ad.listener = self } private func consumeLoadDuration() -> Int64? { guard let start = loadStartTime else { return nil } loadStartTime = nil return SystemClock.elapsedRealtime() - start } private var handler: StateMachine { return engine } func load(reason: RequestReason?) -> Bool { MabAnalyticEvents.faLoad(faUnitId: faUnitId, adType: engine.adType, aggregatorId: config.aggregatorId, adUnitId: adUnitSpec.adUnitId, requestReason: reason) let result = ad.load() if result { handler.removeCallbacksAndMessages(cacheExpireToken) loadedTimeInMillis = nil loadStartTime = SystemClock.elapsedRealtime() let timeout: Int64 if let configTimeout = config.loadTimeoutInSecond { timeout = Int64(configTimeout * 1000) } else { timeout = MabInterstitialEngine.BIDDING_LOAD_TIMEOUT_MILLIS } handler.postDelayed(runnable: { [weak self] in self?.performLoadTimeout() }, token: loadTimeOutToken, delayed: .milliseconds(Int(timeout))) } return result } func show(request: InterstitialShowRequest?) -> Bool { let result = ad.show(request) if result { handler.removeCallbacksAndMessages(cacheExpireToken) loadedTimeInMillis = nil } return result } func destroy() { handler.removeCallbacksAndMessages(loadTimeOutToken) handler.removeCallbacksAndMessages(cacheExpireToken) revenue = 0.0 ad.destroy() fusionAd = nil loadedTimeInMillis = nil loadStartTime = nil } // MARK: - FusionAdListener 方法 func onAdLoaded(ad: FusionAd) { logLoaded(ad) handler.removeCallbacksAndMessages(loadTimeOutToken) fusionAd = ad loadedTimeInMillis = SystemClock.elapsedRealtime() revenue = MabBidderUtils.adjustRevenue(ad: ad, floor: adUnitSpec.floor) engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_LOADED, obj: EventParams(ad: ad, id: id)) handler.removeCallbacksAndMessages(cacheExpireToken) let cacheTimeInMillis = Int64(config.cacheExpireInSecond * 1000) if cacheTimeInMillis > 0 { handler.postDelayed(runnable: { [weak self] in self?.performCacheExpired() }, token: cacheExpireToken, delayed: .milliseconds(Int(cacheTimeInMillis + 50))) } } func onAdDisplayed(ad: FusionAd) { engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_DISPLAYED, obj: EventParams(ad: ad, id: id)) } func onAdHidden(ad: FusionAd) { engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_HIDDEN, obj: EventParams(ad: ad, id: id)) } func onAdClicked(ad: FusionAd) { engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_CLICK, obj: EventParams(ad: ad, id: id)) } func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { let errorCode = loadFailedInfo.error?.errorCode ?? FusionErrorCodes.UNKNOWN.code logLoadFailed(errorCode) loadedTimeInMillis = nil handler.removeCallbacksAndMessages(loadTimeOutToken) engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_LOAD_FAILED, obj: EventParams(loadFailedInfo: loadFailedInfo, id: id) ) } func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { engine.sendMessage( what: MabInterstitialEngine.EVENT_ON_DISPLAY_FAILED, obj: EventParams(ad: ad, displayFailedInfo: error, id: id) ) } func onAdRevenuePaid(ad: FusionAd) { engine.sendMessage(what: MabInterstitialEngine.EVENT_REVENUE_PAID, obj: EventParams(ad: ad, id: id)) } // MARK: - 私有方法 private func performLoadTimeout() { ad.destroy() onAdLoadFailed( loadFailedInfo: LoadFailedInfo( engineId: ad.engineId, adPlatform: ad.adPlatform, adType: engine.adType, adUnitId: ad.adUnitId, error: LoadAdTimeoutError() ) ) } private func performCacheExpired() { engine.sendMessage(what: MabInterstitialEngine.EVENT_CACHE_EXPIRED, obj: EventParams(id: id)) } private func logLoaded(_ fusionAd: FusionAd) { guard let duration = consumeLoadDuration() else { Logger.w(tag: InterstitialBidder.TAG, message: "logLoaded but duration is nil, ignored") return } MabAnalyticEvents.faLoaded( faUnitId: faUnitId, adType: engine.adType, adSource: fusionAd.networkName, aggregatorId: config.aggregatorId, adUnitId: ad.adUnitId, durationInMillis: duration ) } private func logLoadFailed(_ errorCode: Int) { guard let duration = consumeLoadDuration() else { Logger.w(tag: InterstitialBidder.TAG, message: "logLoadFailed but duration is nil, ignored") return } MabAnalyticEvents.faFailed( faUnitId: faUnitId, adType: engine.adType, aggregatorId: config.aggregatorId, adUnitId: ad.adUnitId, durationInMillis: duration, errorCode: errorCode ) } var description: String { return "InterstitialBidder[\(aggregatorId)(\(adUnitSpec.adUnitId))]" } } // MabInterstitialEngine 类实现 internal class MabInterstitialEngine: InterstitialAdEngine { // MARK: - 常量定义 static let ACTION_LOAD = 1 static let ACTION_SHOW = 2 static let ACTION_DESTROY = 4 static let ACTION_RETRY = 5 static let EVENT_ON_LOADED = 100 static let EVENT_ON_LOAD_FAILED = 101 static let EVENT_ON_DISPLAYED = 102 static let EVENT_ON_DISPLAY_FAILED = 103 static let EVENT_ON_CLICK = 104 static let EVENT_ON_HIDDEN = 105 static let EVENT_REVENUE_PAID = 107 static let EVENT_SHOW_TIMEOUT = 109 static let EVENT_CACHE_EXPIRED = 110 static let BIDDING_LOAD_TIMEOUT_MILLIS: Int64 = 60_000 static let SHOW_TIMEOUT_MILLIS: Int64 = 120_000 // MARK: - 属性 private let faUnitId: String private let mabConfig: MabInterstitialConfig private let adObtainer: InterstitialAdObtainer private let auctioneer: MabAuctioneer // MARK: - 初始化方法 init( viewController: UIViewController, id: Int, faUnitId: String, mabConfig: MabInterstitialConfig, adObtainer: @escaping InterstitialAdObtainer ) { self.mabConfig = mabConfig self.adObtainer = adObtainer self.faUnitId = faUnitId // 创建拍卖器 weak var futureSelf: MabInterstitialEngine? = nil auctioneer = MabAuctioneer( engineId: id, faUnitId: faUnitId, aggregatorConfigs: mabConfig.aggregatorConfigs, bidderFactory: { config, adUnitSpec in guard let self = futureSelf else { return nil } return self.createBidder(config: config, adUnitSpec: adUnitSpec) } ) super.init(viewController: viewController, id: id, adType: AdType.Interstitial, strategyName: "Mab") futureSelf = self } public override func createIdleState() -> Idle { return IdleImpl(self) } public override func createLoadingState() -> Loading { return LoadingImpl(self) } public override func createLoadedState() -> Loaded { return LoadedImpl(self) } public override func createShowingState() -> Showing { return ShowingImpl(self) } // MARK: - 私有方法 private func createBidder( config: AggregatorConfig, adUnitSpec: AdUnitSpec ) -> InterstitialBidder? { let adAmzId = adUnitSpec.adAmazonSlotId let adPlatform = AdPlatform.fromName(config.adPlatform) guard let ad = adObtainer( adPlatform, AdConfig(engineId: id, adUnitId: adUnitSpec.adUnitId, adAmazonSlotId: adAmzId, requireDisableAutoRetries: true) ) else { return nil } return InterstitialBidder( engine: self, config: config, ad: ad, adUnitSpec: adUnitSpec, faUnitId: faUnitId ) } private func handleUnhandledMessage(_ msg: Message?) -> Bool { guard let what = msg?.what else { return false } switch what { case MabInterstitialEngine.ACTION_LOAD: logWarn("ACTION_LOAD on \(String(describing: currentState?.name))") adLoadFailed( loadFailedInfo: LoadFailedInfo( engineId: id, adPlatform: AdPlatform.fusion, adType: adType, adUnitId: "mab_interstitial", error: LoadAdRequestError(message: "unhandled ACTION_LOAD, current state is \(String(describing: currentState?.name))") ) ) return true case MabInterstitialEngine.ACTION_SHOW: adDisplayFailed(ad: InvalidFusionAd.interstitial(engineId: id), error: ShowAdRequestError()) return true case MabInterstitialEngine.ACTION_DESTROY: logWarn("Destroy may not supported in a Mab Engine, may lead memory leakages if you are trying to destroy one.") transitionTo(idleState) // destroy all ads return true case MabInterstitialEngine.EVENT_REVENUE_PAID: logWarn("revenue paid event unhandled!") case MabInterstitialEngine.EVENT_ON_LOADED: logWarn("event EVENT_ON_LOADED unhandled!") case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: logWarn("event EVENT_ON_LOAD_FAILED unhandled!") default: break } return false } private func reset() { auctioneer.reset() removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) } // MARK: - InterstitialAdEngine 方法实现 override func requestLoad() { sendMessage(what: MabInterstitialEngine.ACTION_LOAD) } override func requestShow(request: InterstitialShowRequest?) { sendMessage(what: MabInterstitialEngine.ACTION_SHOW, obj: request) } override func requestDestroy() { sendMessage(what: MabInterstitialEngine.ACTION_DESTROY) } override var supportedAdPlatforms: Set { return [AdPlatform.fusion] } // MARK: - 内部状态类 class IdleImpl: Idle { public let engine: MabInterstitialEngine init(_ engine: MabInterstitialEngine) { self.engine = engine } override func enter(from: IState?, params: Any?) { super.enter(from: from, params: params) self.engine.reset() } override func exit(to: IState?) { super.exit(to: to) } override func processMessage(_ msg: Message) -> Bool { let what = msg.what if what == MabInterstitialEngine.ACTION_LOAD { let result = engine.auctioneer.requestNext() if result != MabAuctioneer.RequestResult.noAvailableSource { engine.transitionTo(engine.loadingState) return true } else { engine.logError("No available source") engine.adLoadFailed( loadFailedInfo: LoadFailedInfo( engineId: engine.id, adPlatform: AdPlatform.fusion, adType: engine.adType, adUnitId: "mab_interstitial", error: LoadAdNotAvailableError() ) ) return true } } return engine.handleUnhandledMessage(msg) || super.processMessage(msg) } override var name: String { return super.name } } class LoadingImpl: Loading { public let engine: MabInterstitialEngine init(_ engine: MabInterstitialEngine) { self.engine = engine } private var retryCount = 0 override func enter(from: IState?, params: Any?) { super.enter(from: from, params: params) } override func exit(to: IState?) { super.exit(to: to) } override func processMessage(_ msg: Message) -> Bool { let what = msg.what switch what { case MabInterstitialEngine.ACTION_RETRY: retryCount += 1 engine.transitionTo(engine.idleState) engine.requestLoad() return true case MabInterstitialEngine.ACTION_LOAD: engine.logWarn("Already loading! Ignore load request") return true case MabInterstitialEngine.EVENT_ON_LOADED: if let eventParams = msg.obj as? EventParams { if eventParams.ad != nil { engine.auctioneer.onLoaded(bidderId: eventParams.id) engine.auctioneer.requestNext() if engine.auctioneer.hasWinner { engine.transitionTo(engine.loadedState, params: eventParams) } } } return true case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: guard let eventParams = msg.obj as? EventParams else { return true } engine.auctioneer.onLoadFailed(bidderId: eventParams.id) let result = engine.auctioneer.requestNext() if result == MabAuctioneer.RequestResult.noAvailableSource { let failedInfo = eventParams.loadFailedInfo ?? LoadFailedInfo( engineId: engine.id, adPlatform: AdPlatform.fusion, adType: engine.adType, adUnitId: "mab_interstitial", error: LoadAdNotAvailableError() ) engine.adLoadFailed(loadFailedInfo: failedInfo) let delay = min(max(pow(2.0, Double(retryCount)) as Double, 10), 300) engine.sendMessageDelayed(what: MabInterstitialEngine.ACTION_RETRY, delayMillis: Int(delay * 1000)) } return true default: break } return engine.handleUnhandledMessage(msg) || super.processMessage(msg) } } class LoadedImpl: Loaded { public let engine: MabInterstitialEngine init(_ engine: MabInterstitialEngine) { self.engine = engine } override func enter(from: IState?, params: Any?) { if let params = params as? EventParams, let ad = params.ad { self.engine.adLoaded(ad: ad) } else { self.engine.logWarn("Loaded state enter with invalid params") self.engine.transitionTo(self.engine.idleState) } super.enter(from: from, params: params) } override func exit(to: IState?) { super.exit(to: to) } override func processMessage(_ msg: Message) -> Bool { let what = msg.what switch what { case MabInterstitialEngine.ACTION_SHOW: let showRequest = msg.obj as? InterstitialShowRequest var showResult: Bool? = nil var finalWinner: InterstitialBidder? = nil while true { guard let winner = engine.auctioneer.pollMaxRevenue() else { break } if !winner.isFilled { continue } showResult = winner.show(request: showRequest) if showResult != true { continue } finalWinner = winner break } if let winner = finalWinner { engine.auctioneer.onShown(winner: winner) engine.transitionTo(engine.showingState) } else { let errorMessage = showResult == nil ? "requestShow while NO winner in candidates" : "Show Ad returns false" engine.adDisplayFailed( ad: InvalidFusionAd.interstitial(engineId: engine.id), error: ShowAdRequestError(message: errorMessage) ) let result = engine.auctioneer.requestNext() if result != MabAuctioneer.RequestResult.noAvailableSource { engine.transitionTo(engine.loadingState) } else { engine.transitionTo(engine.idleState) engine.requestLoad() } } return true case MabInterstitialEngine.EVENT_ON_LOADED: if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { engine.auctioneer.onLoaded(bidderId: eventParams.id) engine.auctioneer.requestNext() } return true case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: processLoadFailed(msg) return true case MabInterstitialEngine.EVENT_CACHE_EXPIRED: engine.auctioneer.evictCache() if !engine.auctioneer.hasWinner { engine.transitionTo(engine.idleState) engine.requestLoad() } return true default: break } return engine.handleUnhandledMessage(msg) || super.processMessage(msg) } private func processLoadFailed(_ msg: Message) { guard let eventParams = msg.obj as? EventParams else { return } engine.auctioneer.onLoadFailed(bidderId: eventParams.id) engine.auctioneer.requestNext() } } class ShowingImpl: Showing { public let engine: MabInterstitialEngine init(_ engine: MabInterstitialEngine) { self.engine = engine } override func enter(from: IState?, params: Any?) { let timeout: Int64 if let configTimeout = engine.mabConfig.showTimeoutInSecond { timeout = Int64(configTimeout * 1000) } else { timeout = MabInterstitialEngine.SHOW_TIMEOUT_MILLIS } engine.sendMessageDelayed(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT, delayed: .milliseconds(Int(timeout))) super.enter(from: from, params: params) } override func exit(to: IState?) { self.engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) super.exit(to: to) } override func processMessage(_ msg: Message) -> Bool { let what = msg.what switch what { case MabInterstitialEngine.EVENT_ON_DISPLAYED: if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { engine.adDisplayed(ad: ad) } else { engine.logWarn("Showing state enter with invalid params") } engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) return true case MabInterstitialEngine.EVENT_ON_CLICK: if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { engine.adClicked(ad: ad) } else { engine.logWarn("Showing state enter with invalid params") } engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) return true case MabInterstitialEngine.EVENT_ON_HIDDEN: if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { engine.adHidden(ad: ad) } else { engine.logWarn("Showing state enter with invalid params") } processRequestAfterConsumed() return true case MabInterstitialEngine.EVENT_ON_DISPLAY_FAILED: if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) } else { engine.logWarn("Showing state enter with invalid params") } processRequestAfterConsumed() return true case MabInterstitialEngine.EVENT_SHOW_TIMEOUT: engine.adDisplayFailed(ad: InvalidFusionAd.interstitial(engineId: engine.id), error: ShowAdTimeoutError()) processRequestAfterConsumed() return true case MabInterstitialEngine.EVENT_REVENUE_PAID: if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { engine.adRevenuePaid(ad: ad) } else { engine.logWarn("revenue paid event with invalid params") } return true case MabInterstitialEngine.EVENT_ON_LOADED: if let eventParams = msg.obj as? EventParams { engine.auctioneer.onLoaded(bidderId: eventParams.id) } return true case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: if let eventParams = msg.obj as? EventParams { engine.auctioneer.onLoadFailed(bidderId: eventParams.id) } return true default: break } return engine.handleUnhandledMessage(msg) || super.processMessage(msg) } /** * 消耗后重新填充请求队列 * 正常情况下执行完毕后,队列头部为自抬价聚合请求,后续为按priority降序排列的聚合 * 并开始一轮requestNext */ private func processRequestAfterConsumed() { engine.auctioneer.refillCandidatesQueue() let result = engine.auctioneer.requestNext() if let winner = engine.auctioneer.peekMaxRevenue() { engine.transitionTo( engine.loadedState, params: EventParams(ad: winner.getFusionAd()) ) } else if result != MabAuctioneer.RequestResult.noAvailableSource { engine.transitionTo(engine.loadingState) } else { engine.transitionTo(engine.idleState) engine.requestLoad() } } } } private class AtomicInteger { private var value: Int init(_ initialValue: Int) { self.value = initialValue } func get() -> Int { return value } func incrementAndGet() -> Int { value += 1 return value } } private class SystemClock { static func elapsedRealtime() -> Int64 { return Int64(ProcessInfo.processInfo.systemUptime * 1000) } static func elapsedRealtimeNanos() -> Int64 { return Int64(ProcessInfo.processInfo.systemUptime * 1_000_000_000) } }