哪个小可爱在偷偷的看我~~
背景
在一个流量变现的年代,app如何收益逐渐提上日程,如何将app日活变现呢?广告是一个很好的途径
在曝光量有限的前提下,如何能将流量高质量的变现?请往下看,此篇文章提供广告核心封装思路,如何简单对接多方三方广告
本人在长时间广告开发过程中,相对对广告有了一定的认知,经过大日活app广告实线,沉淀出一套广告封装思想,以下将为大家分享一下核心功能封装
功能为简版,暂不包含各策略,如多方广告投放,单一广告失败其他广告补充,广告可控系统,串行请求,并行请求,超时暂停,竞价模式,启动图、霸屏、插屏预加载等等各策略
目的为,在有限的曝光位,展示最高价值广告,最优展示
当然,大家也可以选择简简单单对接一个三方平台,VCat是广告逐渐调优沉淀出来的,有不合理的地方可提出疑问,下面开始详细介绍功能实现
####广告对外暴露调用方式
如果需要,对外回掉事件也通过AdBuilder进行回掉
private fun getAd() {
AdBuilder()
.setAdLoadListener(
object : SimpleAdLoadListener {
override fun onLoading(isLoading: Boolean) {
}
override fun onAdType(adCnType: AdVCatType) {
}
override fun onAdShow() {
}
override fun onNoAdOrDismissed() {
}
},
)
.setAdId(binding.etAdid.text.toString().trim())
.setController(binding.rootView)
.setAdFrom(AdFrom.FROM_BAPING_AD)
.build()
.loadAdInfo()
}
设计思路架构核心类 |
---|
一、 对外暴露通过AdBuilder进行通信 |
二、 核心类AdLoadNotice,此类为贯穿所有策略的节点 |
三、 代理类AdRequestManager,代理核心类AdLoadNotice,负责分发 |
四、 AdForAdId广告解析adid向下分发 |
五、 TestAdId无需接口更改,本地自测adid |
六、 VCatAdFactory工厂 |
七、 FactoryVCat生产者 |
八、 AdLoadVCat广告加载类 |
九、 AdRenderVCat广告渲染类 |
十、 AdStatistics统计类 |
简述类的由来
AdBuilder
,获取外部参数,集中获取,build模式——>
AdLoadNotice
,处理各种任务,线性串联,只负责串联,具体实现不用去管,可以优化成责任链模式——>
AdRequestManager
,分担AdLoadNotice
一部分任务,因为AdLoadNotice
类分发内容会越来越多,需要个秘书进行代理,如需修改任何节点,秘书进行切换即可,代理模式——>
AdForAdId
,此类只负责解析adid,新增、删减广告此类进行控制,此类角色为老板——>
TestAdId
,此类为测试类,不跟随接口返回内容走,角色为老板开的后门——>
VCatAdFactory
,此类为工厂,老板定好了需要生产啥,交与工厂进行生产,工厂模式——>
FactoryVCat
,此类为工厂的部门,工厂可以生产多种物件,此类只负责生产某种产品,状态模式——>
AdLoadVCat
,此类给工厂部门提供零件,专注于提供请求广告资源服务,策略模式——>
AdRenderVCat
,此类给工厂部门提供零件,专注于提供请求广告渲染服务,策略模式——>
AdStatistics
,此类给工厂部门提供零件,专注于提供请求广告统计服务,策略模式——>
注:所有任务做完,都会回掉给AdLoadNotice
,让它进行再次分发,所以它是流水线的命脉,后续会增加 TimeOutEvent超时机制
,PolicyManager请求模式管理者
,PolicySingle单独一个广告请求
,PolicyBing并行广告请求
,PolicyChuan串行广告请求
,AdHandlerEvent下发节点控制类
,竞价控制类
等等,都是会通过AdLoadNotice进行切换
广告设计思想
1、我们想要的是,广告类型不管怎么增加,我们不关心流程的实现,只关心我新增,和结果,所以功能实现要分隔,新增只在AdForAdId
中处理新增的adid
进行向下分发,结果只在FactoryVCat
工厂里进行新增的广告类型进行创建即可,新增的广告也不用关心其他的广告,不会有任何参杂,都是个体,没有任何关联
2、广告很多节点都是重复的,将功能抽象化,以抽象类的形式展现,调用也只调用抽象即可,不去关心子类,如IRenderUI
,ILoaded
,IStatistics
,OnAdFactory
等,多肽的思想进行构建
3、广告展示分为2步骤,请求和渲染,互不影响,他们的任务没有任何联系,唯一的关联是请求的结果需要交给渲染类进行渲染,但是这一步也没有关联,请求的结果是交付给AdLoadNotice
,AdLoadNotice
代理给AdRequestManager
,进行下发给渲染类,这样功能完全独立,如AdLoadCSJ
只负责穿山甲的所有类型广告请求,如启动图,banner
等等,请求都在我这里实现,而AdRenderCSJ
只负责渲染,不关心任何功能
4、广告避免不了和外界的交互,比如外界需要展示的广告类型,此时都是通过AdBuilder
进行通信,但是AdBuilder
不能过于庞大,它的任务只是负责基础信息,如需要更多操作,需要如CreateAdBuilder
继承 AdBuilder
,进行针对性扩展
5、为了和外界进行隔离,外界只给我们提供个容器即可,我们将自己的view添加到容器上,即实现了广告展示
广告流程图简版
项目结构如下
关键核心类AdLoadNotice
后续增加各种策略,都是由核心类进行分发,核心类不需要具体实现,可以理解为此类为路由转折点
class AdLoadNotice internal constructor(private var builder: AdBuilder) : OnFactoryInfoListener {
private var TAG = AD_TAG + builder.mAdFrom + "(" + builder.mAdFrom.adName + ") | "
private var mAdRequestManager: AdRequestManager? = null
init {
mAdRequestManager = AdRequestManager(builder)
}
override fun getBuilder(): AdBuilder {
return builder
}
/**
* 加载广告 adInfo 未传,内部进行解析
*/
override fun loadAdInfo(adInfo: VCatAdInfo?) {
VCatLog.d(TAG + "广告发起请求")
builder.setAdInfo(adInfo)
builder.mOnAdLoad?.onLoading(true)
loadAdWithType()
}
private fun loadAdWithType() {
AdForAdId(builder, { adType: AdCnType, sdkEvent: SDKEvent ->
builder.setAdCnType(adType)
builder.setSDKEvent(sdkEvent)
loadCnAd()
}, {
onNoAdNotice()
})
}
/**
* 广告发起 请求
*/
private fun loadCnAd() {
VCatLog.d(TAG + builder.mAdCnType.type + " | 广告发起 请求 ")
VCatAdFactory(this, builder).create { loaded, renderUI, statistics ->
builder.mOnAdLoad?.onAdType(builder.mAdCnType)
mAdRequestManager?.loadAd(loaded, renderUI, statistics)
}
}
override fun handlerEvent(it: Any?) {
mAdRequestManager?.handlerEvent(it)
}
override fun handlerStatistics(it: Any?) {
mAdRequestManager?.handlerStatistics(it)
}
/**
* 无广告下发
*/
override fun onNoAdNotice() {
VCatLog.d(TAG + "下发无广告或广告关闭状态")
builder.mOnAdLoad?.onLoading(false)
builder.mOnAdLoad?.onNoAdOrDismissed()
mAdRequestManager?.onDestroy()
}
fun onResume() {
mAdRequestManager?.onResume()
}
fun onPause() {
mAdRequestManager?.onPause()
}
fun onStop() {
mAdRequestManager?.onStop()
}
fun onDestroy() {
mAdRequestManager?.onDestroy()
}
}
根据步骤进行拆解
一、广告整体思路梳理
1、 广告所需展示到某块区域,我们可提供个容器,将请求到的广告view
,addView
到容器上,这样可以和页面进行隔离操作,只提供个容器即可
2、 广告所需参数,如context
,广告所需展示的位置,ViewGroup
容器,广告位所属的adid等,请求广告所需参数,AdBuilder
负责整合所需参数,进行下传处理
3、 为了广告可控,一般我们会接口控制广告是否投放,所以我们需要个自己的接口,为了三方广告adid
可变,即接口下发三方adid
,所以此接口是必要的,我们简单称之为firstApi
,即通过对应的广告位所属adid
,通过接口请求三方的adid
,用来请求三方
4、 广告分为2
种,一种是三方广告,另一种是自研广告,可以投放一些自己公布的信息,自研广告不是必须的,我们按照有的思路去解析,我们可将自研广告也当作三方广告处理
5、 自研广告投放时机问题,常规思路,先请求firstApi
如果下发的是自研广告,我们再请求自研Api
,此时我们自己的广告系统会请求2次,才能获取到广告信息,所以不是最优方案,可以优化为请求一次,fistApi
即下发自研广告资源,下发则优先渲染自研,无则下发三方adid
(也可为集合,之后会作为并行,串行请求所需)
6、 下发广告后,包含adid
等信息,此时我们不知道请求哪个三方,所以需要制定我们的规则,请求对应的三方广告,AdForAdId
负责处理此逻辑
7、 广告创建,可以作为工厂模式进行处理,VCatAdFactory
负责创建,策略模式处理创建哪一个
8、 单个广告举例,FactoryVCat
某个策略,AdLoadVCat
负责广告请求,AdRenderVCat
负责广告渲染,AdStatistics
负责广告统计
二、外部传递信息AdBuilder创建
AdBuilder
获取外界传递各项参数,此类贯穿全局,获取值从此类中提取,外部获取状态也可以通过此类进行监听,如OnAdLoadListener
拓展:如果需要额外回掉,比如霸屏,创建单独的Builder
如BaPingBuilder
继承 AdBuilder
,BaPingBuilder
独立增加额外的参数
open class AdBuilder {
/**
* 广告来源
*/
internal lateinit var mAdFrom: AdFrom
internal var mAdInfo: VCatAdInfo? = null
internal var mViewSize: Pair<Int, Int> = Pair(640, 320)
internal var mOnAdLoad: OnAdLoadListener? = null
internal var mController: ViewGroup? = null
internal var mContext: Context? = null
internal var mAdId: String = ""
internal lateinit var mAdCnType: AdCnType
internal var mSDKEvent: SDKEvent? = null
fun setAdId(adId: String): AdBuilder {
mAdId = adId
return this
}
fun setAdFrom(adFrom: AdFrom): AdBuilder {
mAdFrom = adFrom
return this
}
fun setAdLoadListener(onAdLoad: OnAdLoadListener?): AdBuilder {
mOnAdLoad = onAdLoad
return this
}
fun setViewSize(viewSize: Pair<Int, Int>): AdBuilder {
mViewSize = viewSize
return this
}
fun setController(controller: ViewGroup?): AdBuilder {
mController = controller
mContext = mController?.context
return this
}
fun setAdInfo(adInfo: VCatAdInfo?) {
mAdInfo = adInfo
}
fun setAdCnType(adCnType: AdCnType) {
mAdCnType = adCnType
}
fun setSDKEvent(sdkEvent: SDKEvent?) {
mSDKEvent = sdkEvent
}
fun build(): AdLoadNotice =
if (::mAdFrom.isInitialized) {
AdLoadNotice(this)
} else {
throw NullPointerException("adInfo or adFrom was null! 请检查是否设置!!!")
}
}
二、关键核心类AdLoadNotice,分步骤创建AdLoadNotice
此类为贯穿所有策略的节点,AdLoadNotice持有AdBuilder,loadAdInfo()为解析入口,下一步需要解析adid
override fun loadAdInfo(adInfo: VCatAdInfo?) {
VCatLog.d(TAG + "广告发起请求")
builder.setAdInfo(adInfo)
builder.mOnAdLoad?.onLoading(true)
loadAdWithType()
}
private fun loadAdWithType() {
AdForAdId(builder, { adType: AdCnType, sdkEvent: SDKEvent ->
builder.setAdCnType(adType)
builder.setSDKEvent(sdkEvent)
loadCnAd()
}, {
onNoAdNotice()
})
}
三、adid解析类AdForAdId,规则需要自己定制,下面简单展示下实现内容
class AdForAdId(
val builder: AdBuilder,
private val callBack: (adType: AdCnType, sdkEvent: SDKEvent) -> Unit,
private val onNoAdId: () -> Unit,
) {
private var TAG = AD_TAG + builder.mAdFrom + "(" + builder.mAdFrom.adName + ") | "
// 传入全部的 资源 VCat广告集合
private val mVCatList: MutableList<SDKEvent> = mutableListOf()
// 传入全部的 资源 穿山甲广告集合
private val mCSJList: MutableList<SDKEvent> = mutableListOf()
// 传入全部的 资源 广点通广告集合
private val mGDTList: MutableList<SDKEvent> = mutableListOf()
// 传入全部的 资源 百度广告集合
private val mBAIDUList: MutableList<SDKEvent> = mutableListOf()
init {
initAdInfo()
}
private fun isReqConfig(): Boolean {
return builder.mAdInfo == null
}
private fun initAdInfo() {
with(mutableListOf<SDKEvent>()) {
if (isReqConfig()) {
add(SDKEvent())
} else {
builder.mAdInfo?.sdks?.let { addAll(it) }
}
if (BuildConfig.DEBUG) {
TestAdId(builder) {
clear()
addAll(it)
}
}
adTypeEvent(this)
}
}
/**
* 初始化广告信息
*/
private fun adTypeEvent(sdks: List<SDKEvent>) {
mVCatList.clear()
mCSJList.clear()
mGDTList.clear()
if (isReqConfig()) {
mVCatList.add(sdks.first())
} else {
sdks.forEach { sdkEvent ->
if (sdkEvent.adid.isBlank()) {
return@forEach
}
when (sdkEvent.type) {
AdContent.csj -> {
mCSJList.add(sdkEvent)
}
AdContent.gdt -> {
mGDTList.add(sdkEvent)
}
AdContent.baidu -> {
mBAIDUList.add(sdkEvent)
}
}
}
}
adLogShow(sdks)
loadAdWithType()
}
private fun loadAdWithType() {
when {
mVCatList.size > 0 -> {
callBack.invoke(AdCnType.TYPE_VCAT, mVCatList.first())
}
mCSJList.size > 0 -> {
callBack.invoke(AdCnType.TYPE_CSJ, mCSJList.first())
}
mGDTList.size > 0 -> {
callBack.invoke(AdCnType.TYPE_GDT, mGDTList.first())
}
mBAIDUList.size > 0 -> {
callBack.invoke(AdCnType.TYPE_BAIDU, mBAIDUList.first())
}
else -> {
onNoAdId.invoke()
}
}
}
/**
* 打印广告日志
*/
private fun adLogShow(sdks: List<SDKEvent>) {
if (isReqConfig()) {
Timber.d(TAG + "配置接口请求:adid->" + builder.mAdId)
} else {
Timber.d(TAG + "打印所有集合:" + sdks)
Timber.d(
TAG + "广告SDK返回数据 集合所有 type:" +
sdks.stream().map(SDKEvent::type).collect(Collectors.toList())
)
Timber.d(
TAG + "广告SDK返回数据 集合所有 adid:" +
sdks.stream().map(SDKEvent::adid).collect(Collectors.toList())
)
}
}
}
四、广告开始发起请求,VCatAdFactory工厂模式
private fun loadCnAd() {
VCatLog.d(TAG + builder.mAdCnType.type + " | 广告发起 请求 ")
VCatAdFactory(this, builder).create { loaded, renderUI, statistics ->
builder.mOnAdLoad?.onAdType(builder.mAdCnType)
mAdRequestManager?.loadAd(loaded, renderUI, statistics)
}
}
class VCatAdFactory(val listener: OnFactoryInfoListener, val builder: AdBuilder) {
fun create(callback: (loaded: Loaded, renderUI: RenderUI, statistics: Statistics?) -> Unit) {
when (builder.mAdCnType) {
AdCnType.TYPE_VCAT -> {
FactoryVCat(listener, builder)
}
AdCnType.TYPE_CSJ -> {
FactoryCSJ(listener, builder)
}
AdCnType.TYPE_GDT -> {
FactoryGDT(listener, builder)
}
AdCnType.TYPE_BAIDU -> {
FactoryBaiDu(listener, builder)
}
}.apply {
callback.invoke(onCreateLoader(), onCreateRender(), onCreateStatistics())
}
}
}
五、策略模式创建各种广告,简单以穿山甲为例
interface OnAdFactory {
fun onCreateLoader(): Loaded
fun onCreateRender(): RenderUI
fun onCreateStatistics(): Statistics?
}
class FactoryCSJ(val listener: OnFactoryInfoListener, val builder: AdBuilder) : OnAdFactory {
override fun onCreateLoader(): Loaded {
return AdLoadCSJ(builder, {
it?.let {
listener.handlerEvent(it)
} ?: run {
listener.onNoAdNotice()
}
}, {
listener.handlerStatistics(it)
})
}
override fun onCreateRender(): RenderUI {
return AdRenderCSJ()
}
override fun onCreateStatistics(): Statistics {
return AdStatistics(builder)
}
}
六、简述穿山甲请求类AdLoadCSJ,抽象接口Loaded
interface Loaded {
fun load(builder: AdBuilder) {
loadLogTag(AD_TAG + builder.mAdFrom + "(" + builder.mAdFrom.adName + ") | ")
when (builder.mAdFrom) {
AdFrom.FROM_QDY_AD -> {
loadQiDongYe()
}
AdFrom.FROM_BANNER_AD -> {
loadBanner()
}
AdFrom.FROM_BAPING_AD -> {
loadBaPing()
}
AdFrom.FROM_CHAPING_AD -> {
loadChaPing()
}
AdFrom.FROM_QTP_AD -> {
loadTiePian()
}
AdFrom.FROM_FEED_BANNER_AD -> {
loadFeed()
}
AdFrom.FROM_LUNBO_AD -> {
loadLunBo()
}
AdFrom.FROM_PAUSE_AD -> {
loadPause()
}
else -> {
}
}
}
fun loadLogTag(tag: String)
fun loadQiDongYe()
fun loadBanner()
fun loadBaPing()
fun loadChaPing()
fun loadTiePian()
fun loadFeed()
fun loadLunBo()
fun loadPause()
}
class AdLoadCSJ(
private val builder: AdBuilder,
private val callBack: (listener: Any?) -> Unit,
private val onAdStatistics: (listener: Any?) -> Unit,
) : Loaded {
private var TAG = ""
override fun loadLogTag(tag: String) {
TAG = tag + "穿山甲广告 | "
VCatLog.d(TAG + "adId:" + builder.mSDKEvent?.adid)
}
override fun loadQiDongYe() {
callBack.invoke(null)
}
override fun loadBanner() {
callBack.invoke(null)
}
override fun loadBaPing() {
callBack.invoke(null)
}
override fun loadChaPing() {
callBack.invoke(null)
}
override fun loadTiePian() {
callBack.invoke(null)
}
override fun loadFeed() {
callBack.invoke(null)
}
override fun loadLunBo() {
callBack.invoke(null)
}
override fun loadPause() {
callBack.invoke(null)
}
}
七、AdLoadNotice继续进行分发,分发通过代理类进行分发AdRequestManager
class AdRequestManager(private var builder: AdBuilder) {
private var mRenderUI: RenderUI? = null
private var mStatistics: Statistics? = null
fun loadAd(
loaded: Loaded,
renderUI: RenderUI,
statistics: Statistics? = null,
) {
mRenderUI = renderUI
mStatistics = statistics
statistics?.statistics(builder)
loaded.load(builder)
}
fun onResume() {
mRenderUI?.onResume()
}
fun onPause() {
mRenderUI?.onPause()
}
fun onStop() {
mRenderUI?.onStop()
}
fun onDestroy() {
mRenderUI?.onDestroy()
}
fun handlerEvent(it: Any?) {
mRenderUI?.handlerEvent(it, builder)
}
fun handlerStatistics(it: Any?) {
mStatistics?.handlerEvent(it)
}
}
八、广告渲染类负责渲染AdRenderCSJ,抽象接口RenderUI
interface RenderUI {
fun initData(builder: AdBuilder)
fun handlerEvent(
it: Any?,
builder: AdBuilder,
) {
initData(builder)
}
fun onResume()
fun onPause()
fun onStop()
fun onDestroy()
}
class AdRenderCSJ : RenderUI {
private var TAG = AD_TAG
private var mAdFrom: AdFrom? = null
private var mBuilder: AdBuilder? = null
override fun initData(builder: AdBuilder) {
mBuilder = builder
mAdFrom = builder.mAdFrom
TAG = AD_TAG + mAdFrom + "(" + mAdFrom?.adName + ") | "
mBuilder?.mOnAdLoad?.onAdShow()
mBuilder?.mOnAdLoad?.onLoading(false)
}
override fun handlerEvent(it: Any?, builder: AdBuilder) {
TODO("Not yet implemented")
}
override fun onResume() {
TODO("Not yet implemented")
}
override fun onPause() {
TODO("Not yet implemented")
}
override fun onStop() {
TODO("Not yet implemented")
}
override fun onDestroy() {
TODO("Not yet implemented")
}
}
八、核心类AdLoadNotice案例代码,后续增加各种策略,核心类进行切换,核心类只负责分发
class AdLoadNotice internal constructor(private var builder: AdBuilder) : OnFactoryInfoListener {
private var TAG = AD_TAG + builder.mAdFrom + "(" + builder.mAdFrom.adName + ") | "
private var mAdRequestManager: AdRequestManager? = null
init {
mAdRequestManager = AdRequestManager(builder)
}
override fun getBuilder(): AdBuilder {
return builder
}
/**
* 加载广告 adInfo 未传,内部进行解析
*/
override fun loadAdInfo(adInfo: VCatAdInfo?) {
VCatLog.d(TAG + "广告发起请求")
builder.setAdInfo(adInfo)
builder.mOnAdLoad?.onLoading(true)
loadAdWithType()
}
private fun loadAdWithType() {
AdForAdId(builder, { adType: AdCnType, sdkEvent: SDKEvent ->
builder.setAdCnType(adType)
builder.setSDKEvent(sdkEvent)
loadCnAd()
}, {
onNoAdNotice()
})
}
/**
* 广告发起 请求
*/
private fun loadCnAd() {
VCatLog.d(TAG + builder.mAdCnType.type + " | 广告发起 请求 ")
VCatAdFactory(this, builder).create { loaded, renderUI, statistics ->
builder.mOnAdLoad?.onAdType(builder.mAdCnType)
mAdRequestManager?.loadAd(loaded, renderUI, statistics)
}
}
override fun handlerEvent(it: Any?) {
mAdRequestManager?.handlerEvent(it)
}
override fun handlerStatistics(it: Any?) {
mAdRequestManager?.handlerStatistics(it)
}
/**
* 无广告下发
*/
override fun onNoAdNotice() {
VCatLog.d(TAG + "下发无广告或广告关闭状态")
builder.mOnAdLoad?.onLoading(false)
builder.mOnAdLoad?.onNoAdOrDismissed()
mAdRequestManager?.onDestroy()
}
fun onResume() {
mAdRequestManager?.onResume()
}
fun onPause() {
mAdRequestManager?.onPause()
}
fun onStop() {
mAdRequestManager?.onStop()
}
fun onDestroy() {
mAdRequestManager?.onDestroy()
}
}
注:广告迭代功能会越来越复杂,分层合理后续迭代会简单很多
此篇文章为简述版,如需广告开发,可汲取其中设计思路进行拓展,
——
功能为简版,暂不包含各策略,如多方广告投放,单一广告失败其他广告补充,广告可控系统,串行请求,并行请求,超时暂停,竞价模式,启动图、霸屏、插屏预加载等等各策略
——VCat聚合广告SDK,将整合市面上所有广告SDK,拥有独立的自研广告系统,如需合作可留言
最后祝大家开发愉快