该框架的设计初衷是集中管理第三方的广告服务,包括服务的环境配置,初始化,以及广告的加载与展示。

前言

该框架的设计初衷是集中管理第三方的广告服务,包括服务的环境配置,初始化,以及广告的加载与展示。

接入过程中,我们会试用相同的策略,管理不同的服务,所以需要抽象一套对服务的统一抽象协议。

服务协议

将集成第三方服务大致分为3个部分:环境配置,服务SDK参数配置,广告预载与展示。

平台服务协议

public protocol GHADPlatfromServiceDelegate: NSObjectProtocol {
    func didSetup(service: GHADPlatfromService, params: [String : Any])
}

public protocol GHADPlatfromService {
    var delegate: GHADPlatfromServiceDelegate? { get set }
    var setupParams: [String : Any] { get set }
    func setup() -> Bool
    func configUserIdentity(params: [String : Any]) -> Bool
    func checkMainBundleInfoValid() -> Bool
}

GHADPlatfromService协议用于管理第三方服务的环境配置,以及SDK服务的初始化。

广告服务协议

public protocol GHAdDelegate: NSObjectProtocol {
    func adDidLoad(_ ad: GHAd)
    func adDidFailToLoadAd(_ ad: GHAd, error: Error?)

    func adDidDisplay(_ ad: GHAd)
    func adDidHide(_ ad: GHAd)
    func adDidClick(_ ad: GHAd)
    func adDidFail(_ ad: GHAd, error: Error?)

    func adDidStartRewardedVideo(_ ad: GHAd)
    func adDidCompleteRewardedVideo(_ ad: GHAd)
    func adDidRewardUser(_ ad: GHAd, name: String, amount: Double)
}

public protocol GHAd {
    var delegate: GHAdDelegate? { get set }
    func load(params: [String: Any], completion: ((Bool, NSError?) -> Void)?)
    func show(params: [String: Any], rewardUser: ((_ ad: GHAd, _ name: String, _ amount: Double) -> Void)?, completion: ((Bool, NSError?) -> Void)?)
}

GHADPlatfromService协议用于桥接服务商SDK提供的API,以及广告声明周期的回调。

这个协议的作用是抹平不同服务商SDK之间的差异,统一广告管理逻辑。

激励类型视频广告

GHAppLovinRewardedAd 是对AppLovin服务商提供的激励类型视频的一个具体封装。

它遵循了GHAd协议,服务商提供的回调MARewardedAdDelegate桥接到GHAdDelegate

实现了失败自动重新加载,下一则广告预载,避免重复加载等策略。

import Foundation
import AppLovinSDK
import GHToolKit
import GHLogger

public class GHAppLovinRewardedAd: NSObject, GHAppLovinAd {

    public let platformServiceType: GHADServiceType = .appLovin
    public weak var delegate: GHAdDelegate?
    public weak var revenueDelegate: MAAdRevenueDelegate?
    public let adType: GHAppLovinAdType = .reward
    public var isLoading: Bool = false
    public var isReady: Bool { return ad.isReady }
    
    public var adUnitId: String
    public var retryAttempt = 0
    public var maxRetryCount = 0
    public var autoRetryLoadWhenFailed: Bool = false
    public var autoLoadNextAd: Bool = true

    var loadCompletion: ((Bool, NSError?) -> Void)?
    var showCompletion: ((Bool, NSError?) -> Void)?
    var rewardUserCompletion: ((_ ad: GHAd, _ name: String, _ amount: Double) -> Void)?

    lazy var ad: MARewardedAd = {
        guard let service = GHADManager.shared.platformService(type: .appLovin) as? GHAppLovinService,
           let sdk = service.sdk else
        {
            return MARewardedAd.shared(withAdUnitIdentifier: self.adUnitId)
        }
        
        let ad = MARewardedAd.shared(withAdUnitIdentifier: self.adUnitId, sdk: sdk)
        ad.delegate = self
        ad.revenueDelegate = self
        
        return ad
    }()

    @objc public init(adUnitId: String, delegate: GHAdDelegate? = nil) {
        self.delegate = delegate
        self.adUnitId = adUnitId
    }
}

public extension GHAppLovinRewardedAd {

    @objc func load(params: [String: Any] = [String : Any](), completion: ((Bool, NSError?) -> Void)? = nil) {
        let forceLoad = params[GHAppLovinServiceParamKey.forceLoad.rawValue] as? Bool
        guard let forceLoad = forceLoad, forceLoad else {
            guard !ad.isReady else {
                completion?(false, GHError.error(code: -1, userInfo: ["Message": "ad is already load"]))
                return
            }
            ad.load()
            isLoading = true
            if let exist = loadCompletion { exist(false, nil) }
            loadCompletion = completion
            return
        }
        ad.load()
        isLoading = true
        if let exist = loadCompletion { exist(false, nil) }
        loadCompletion = completion
    }

    @objc func show(params: [String: Any] = [String : Any](), rewardUser: ((_ ad: GHAd, _ name: String, _ amount: Double) -> Void)? = nil, completion: ((Bool, NSError?) -> Void)? = nil) {
        rewardUserCompletion = rewardUser
        if let exist = showCompletion { exist(false, nil) }
        showCompletion = completion
        if ad.isReady {
            ad.show()
            return
        }
        load { [weak self] success, error in
            guard let self = self else { return }
            if success {
                self.ad.show()
            } else {
                self.showCompletion?(false, error)
                self.showCompletion = nil
                self.rewardUserCompletion = nil
            }
        }
    }
}

// MARK: - MARewardedAdDelegate
extension GHAppLovinRewardedAd : MARewardedAdDelegate {

    public func didLoad(_ ad: MAAd) {
        isLoading = false
        logDebug("didLoad", tag: .ad)
        GHADLogEventManager.shared.adDidLoad(self)
        delegate?.adDidLoad?(self)
        loadCompletion?(true, nil)
        loadCompletion = nil
        retryAttempt = 0
    }

    public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
        isLoading = false
        let error = GHError.error(code: error.code.rawValue, userInfo: ["message": error.message, "networkMessage": error.mediatedNetworkErrorMessage])
        logWarn("didFailToLoadAd: \(String(describing: error))", tag: .ad)
        GHADLogEventManager.shared.adDidFailToLoadAd(self, error: error)
        delegate?.adDidFailToLoadAd?(self, error: error)
        loadCompletion?(false, error)
        loadCompletion = nil
        if autoRetryLoadWhenFailed {
            if maxRetryCount > 0, retryAttempt < maxRetryCount {
                retryAttempt += 1
                let delaySec = pow(2.0, min(6.0, Double(retryAttempt)))
                DispatchQueue.main.asyncAfter(deadline: .now() + delaySec) {
                    self.ad.load()
                }
            } else {
                retryAttempt = 0
            }
        }
    }

    public func didStartRewardedVideo(for ad: MAAd) {
        GHADLogEventManager.shared.adDidStartRewardedVideo(self)
        delegate?.adDidStartRewardedVideo?(self)
    }

    public func didCompleteRewardedVideo(for ad: MAAd) {
        GHADLogEventManager.shared.adDidCompleteRewardedVideo(self)
        delegate?.adDidCompleteRewardedVideo?(self)
    }

    public func didRewardUser(for ad: MAAd, with reward: MAReward) {
        logDebug("didRewardUser: \(String(describing: reward))", tag: .ad)
        let name = reward.label
        let amount = Double(reward.amount)
        GHADLogEventManager.shared.adDidRewardUser(self, name: name, amount: amount)
        delegate?.adDidRewardUser?(self, name: name, amount: amount)
        self.rewardUserCompletion?(self, name, amount)
        self.rewardUserCompletion = nil
    }

    public func didDisplay(_ ad: MAAd) {
        logDebug("didDisplay", tag: .ad)
        showCompletion?(true, nil)
        showCompletion = nil
        GHADLogEventManager.shared.adDidDisplay(self)
        delegate?.adDidDisplay?(self)
    }

    public func didHide(_ ad: MAAd) {
        logDebug("didHide", tag: .ad)
        GHADLogEventManager.shared.adDidHide(self)
        delegate?.adDidHide?(self)
        // Rewarded ad is hidden. Pre-load the next ad
        if autoLoadNextAd { self.ad.load() }
        rewardUserCompletion = nil
    }

    public func didClick(_ ad: MAAd) {
        logDebug("didClick", tag: .ad)
        GHADLogEventManager.shared.adDidClick(self)
        delegate?.adDidClick?(self)
    }

    public func didFail(toDisplay ad: MAAd, withError error: MAError) {
        let error = GHError.error(code: error.code.rawValue, userInfo: ["message": error.message, "networkMessage": error.mediatedNetworkErrorMessage])
        logWarn("didFail: \(String(describing: error))", tag: .ad)
        showCompletion?(false, error)
        showCompletion = nil
        rewardUserCompletion = nil
        GHADLogEventManager.shared.adDidFailToDisplay(self, error: error)
        delegate?.adDidFailToDisplay?(self, error: error)
        // Rewarded ad failed to display. We recommend loading the next ad
        if autoLoadNextAd { self.ad.load() }
    }
}

// MARK: - MAAdRevenueDelegate

extension GHAppLovinRewardedAd: MAAdRevenueDelegate {
    
    public func didPayRevenue(for ad: MAAd) {
        delegate?.adDidPayRevenue?(self, maAd: ad)
    }
    
}

广告模块注册与使用

广告模块的使用分为3个步骤

  1. 注册平台服务,配置服务商Key、用户信息,并注册到GHADManager进行统一维护。
  2. 生成一个广告实例(通常广告使用单例模式管理),配置广告的加载策略。
  3. 使用广告的showAPI进行展示
/// 配置广告
class AdModule: NSObject, SpaceportModuleProtocol {
    var loaded = false
    static var rewardedAd: GHAppLovinRewardedAd?
    static func modulePriority() -> Int { return 2750 }

    func loadModule() {
        let servce = GHAppLovinService()
        let key = "xxx"
        servce.setupParams[GHAppLovinServiceParamKey.sdkKey.rawValue] = key
      
        GHADManager.shared.register(type: .appLovin, service: servce)
        GHADManager.shared.setupServices()

        let rewardedAd = GHAppLovinRewardedAd(adUnitId: "c3ae87d19c167b0b")
        rewardedAd.autoRetryLoadWhenFailed = true
        rewardedAd.maxRetryCount = 10
        rewardedAd.delegate = self
        Self.rewardedAd = rewardedAd
        preloadAd()
    }

    func applicationDidBecomeActive(notification: Notification) {
        preloadAd()
    }

    func preloadAd() {
        // 非VIP,预载广告
        if !PurchaseManager.isVIP() {
            Self.rewardedAd?.load()
        }
    }

    func unloadModule() { }
}

// MARK: - GHAdDelegate
extension AdModule: GHAdDelegate {
    func adDidLoad(_ ad: GHAD.GHAd) { }
    func adDidFailToLoadAd(_ ad: GHAD.GHAd, error: Error?) { }
    func adDidDisplay(_ ad: GHAD.GHAd) { }
    func adDidHide(_ ad: GHAD.GHAd) { }
    func adDidClick(_ ad: GHAD.GHAd) { }
    func adDidFail(_ ad: GHAD.GHAd, error: Error?) { }
    func adDidStartRewardedVideo(_ ad: GHAD.GHAd) { }
    func adDidCompleteRewardedVideo(_ ad: GHAD.GHAd) { }
    func adDidRewardUser(_ ad: GHAd, name: String, amount: Double) { }
}
// 展示广告
func showAd() {
  guard let ad = AdModule.rewardedAd else { return }
  ad.show { ad, name, amount in
      // 激励成功回调
			PEHUD.showRewardedADSuccess {}
	} completion: { success, error in
      // 展示完成回调
      if !success { PEHUD.showRewardedADFailed() }
  }  
}

总结

该聚合框架的重点会放在服务SDK的注册与管理,抹平不同SDK的差异,减少业务方接入成本与使用成本。

同时采用服务协议化注册,在实现统一API的基础上,满足业务方扩展定制化API的需求。

另外由于这是一个聚合框架,会包含所有服务商的SDK,当集成的服务商数量太多时,会明显影响包体大小。

后续使用过程中,应当按需组合SDK,避免这个问题。