主题
开屏广告
文档版本 v1.0.0
本页对应 iOS SDK 1.0.0,与离线包 docs/ 目录里的同一份文档内容一致。其他版本见文档中心。
开屏卡在 App 的启动路径上,接法与另两种广告位最大的区别是:必须有一道不依赖任何回调的 兜底放行。线上事故最常见的形态不是没广告,而是卡在开屏页进不去。
两家广告平台在 iOS 上的开屏都覆盖在 window 之上展示,不放进宿主的容器, 所以 show 接收的是 window。开屏一旦卡住,整个 App 无法操作。
完整示例
示例用一个占位页面作为启动后的根页面,开屏结束后把根页面换成首页。 已有启动流程的工程,把 goMain 换成自己的「进入首页」即可。
objc
// SplashViewController.h
#import <UIKit/UIKit.h>
@interface SplashViewController : UIViewController
@endobjc
// SplashViewController.m
#import "SplashViewController.h"
#import "MainViewController.h"
#import <AdMixCore/AdMixCore.h>
// ---- 兜底放行的三段时限。真实项目按自己的启动体验调,但三段都不能没有 ----
static const NSTimeInterval kGuardLoad = 5; // loadAd 后没有任何回调
static const NSTimeInterval kGuardShow = 2; // show 后没有 didShow(白屏)
static const NSTimeInterval kGuardShown = 12; // 展示后没有 didClose
@interface SplashViewController () <AdMixSplashAdDelegate>
@end
@implementation SplashViewController {
AdMixSplashAd *_splashAd;
NSTimer *_guard;
BOOL _jumped;
BOOL _adShown;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (_splashAd || _jumped) return;
if (![AdMix isReady]) { [self goMain]; return; } // 未同意隐私政策 / 没有配置:直接放行
// 用户点广告跳出 App 时暂停计时,回来重新计时
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appWillResignActive)
name:UIApplicationWillResignActiveNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appDidBecomeActive)
name:UIApplicationDidBecomeActiveNotification object:nil];
_splashAd = [[AdMixSplashAd alloc] initWithUnitId:@"splash_main"];
_splashAd.delegate = self;
// 先武装兜底,再 load。反过来写的话,命中缓存时回调是同步的,定时器还没装上
[self armGuard:kGuardLoad why:@"load 后无回调"];
[_splashAd loadAd];
}
#pragma mark - AdMixSplashAdDelegate
- (void)splashAdDidLoad:(AdMixSplashAd *)ad adnType:(NSString *)adnType {
// 展示阶段的兜底必须在 show 之前设:有的广告平台在 show 内部同步回调 didShow
[self armGuard:kGuardShow why:@"show 后无 didShow"];
// 返回 NO = 素材已过期或不可用,必须立刻放行
if (![ad showInWindow:self.view.window bottomView:nil]) [self goMain];
}
- (void)splashAd:(AdMixSplashAd *)ad didFailWithError:(NSError *)error {
[self goMain];
}
- (void)splashAdDidShow:(AdMixSplashAd *)ad {
_adShown = YES;
[self armGuard:kGuardShown why:@"展示后无 didClose"];
}
- (void)splashAdDidClose:(AdMixSplashAd *)ad {
[self goMain];
}
#pragma mark - 兜底
- (void)armGuard:(NSTimeInterval)seconds why:(NSString *)why {
[_guard invalidate];
__weak typeof(self) ws = self;
_guard = [NSTimer scheduledTimerWithTimeInterval:seconds repeats:NO block:^(NSTimer *t) {
NSLog(@"开屏兜底放行: %@", why);
[ws goMain];
}];
}
- (void)appWillResignActive {
if (_adShown) { [_guard invalidate]; _guard = nil; }
}
- (void)appDidBecomeActive {
if (_adShown && !_jumped) [self armGuard:kGuardShown why:@"回到 App 后仍无 didClose"];
}
/// 四条路径(三个回调 + 兜底定时器)都收敛到这里,且只执行一次。全部在主线程
- (void)goMain {
if (_jumped) return;
_jumped = YES;
[_guard invalidate];
_guard = nil;
[NSNotificationCenter.defaultCenter removeObserver:self];
[_splashAd destroy];
_splashAd = nil;
self.view.window.rootViewController = [MainViewController new];
}
@end要点:
showInWindow:bottomView:传开屏所在的 window,一般就是 App 的 key windowbottomView可选,用于在广告下方显示 App 的品牌区(Logo、名称),支持的广告平台会把广告压缩到它上方。 不需要时传nil,广告铺满全屏- 开屏在启动路径上,超时应配置得短(建议 3 秒内),拿不到就放行进首页。取值口径见《策略配置格式》
- 首次安装时系统的无线数据授权尚未确认,首次启动通常没有广告,见《初始化》「首次安装的网络授权」
传入 bottomView 时穿山甲一侧尚未经真机验证
真机验证过的开屏都传 nil。传入非空 bottomView 且由穿山甲中标时,品牌区可能显示不出来, 或广告没有按品牌区高度压缩到其上方。优量汇一侧使用其官方的带底部视图展示接口,不受此条影响。 需要品牌区的接入方,上线前应在真机上确认穿山甲中标时的显示效果。
兜底放行
上面的 armGuard 不是可选的最佳实践,是开屏接入的必备项。把全部回调都收敛到 goMain 只能覆盖「回调来了」的情况,真正出事故的是回调不来:
| 阶段 | 超时条件 | 现象 |
|---|---|---|
| 加载 | loadAd 之后既无 splashAdDidLoad:adnType: 也无 splashAd:didFailWithError: | 开屏页白着不动 |
| 展示 | showInWindow:bottomView: 返回 YES 之后无 splashAdDidShow: | 画面全白,开屏永远不关 |
| 关闭 | splashAdDidShow: 之后无 splashAdDidClose: | 广告播完后卡在开屏 |
开屏能不能进首页,不该取决于第三方 SDK 的回调是否守约。
展示阶段的定时器必须在调用 show 之前设置
优量汇在 showInWindow:bottomView: 内部同步回调 splashAdDidShow:(真机实测)。 先调 show、再设「show 后无 didShow」的定时器,会把 didShow 里已经设好的「展示后无 didClose」 定时器覆盖掉,导致广告仍在展示时被强制放行。
时限取值:kGuardLoad 取该广告位后台配的 total_timeout_ms 再加 1~2 秒余量 (首次广告平台初始化会占掉一部分,见《初始化》「广告平台是按需初始化的」);另两段按示例里的值即可。
上线前手动验五条路径
正常展示后关闭、点广告再返回、飞行模式无填充、展示中断网、首次安装时拒绝无线数据授权 —— 五种情况都必须能进首页。
预加载
开屏是缓存收益最大的广告位。真正的冷启动(新进程)第一条开屏一定是实时请求, 缓存命中发生在热启动开屏(App 从后台回到前台时补放开屏)等进程仍然存活的场景。
objc
// 首页展示出来之后,为下一次开屏备货
[AdMixSplashAd preloadUnitId:@"splash_main"];未命中缓存时,开屏的 loadAd 会以入池方式请求并等待 total_timeout_ms, 超时放行后请求继续跑完,拿到的广告进缓存池留给下一次开屏,不会像纯实时请求那样超时即丢弃。 完整行为见《预加载与缓存》。
Swift 写法
swift
import AdMixCore
final class SplashViewController: UIViewController, AdMixSplashAdDelegate {
private enum Guard {
static let load: TimeInterval = 5
static let show: TimeInterval = 2
static let shown: TimeInterval = 12
}
private var splashAd: AdMixSplashAd?
private var guardTimer: Timer?
private var jumped = false
private var adShown = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard splashAd == nil, !jumped else { return }
guard AdMix.isReady() else { goMain(); return }
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(appWillResignActive),
name: UIApplication.willResignActiveNotification, object: nil)
nc.addObserver(self, selector: #selector(appDidBecomeActive),
name: UIApplication.didBecomeActiveNotification, object: nil)
let ad = AdMixSplashAd(unitId: "splash_main")
ad.delegate = self
splashAd = ad
armGuard(Guard.load, why: "load 后无回调") // 先武装兜底,再 load
ad.load()
}
func splashAdDidLoad(_ ad: AdMixSplashAd, adnType: String) {
armGuard(Guard.show, why: "show 后无 didShow") // 必须在 show 之前
if !ad.show(in: view.window!, bottomView: nil) { goMain() }
}
func splashAd(_ ad: AdMixSplashAd, didFailWithError error: Error) { goMain() }
func splashAdDidShow(_ ad: AdMixSplashAd) {
adShown = true
armGuard(Guard.shown, why: "展示后无 didClose")
}
func splashAdDidClose(_ ad: AdMixSplashAd) { goMain() }
private func armGuard(_ seconds: TimeInterval, why: String) {
guardTimer?.invalidate()
guardTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { [weak self] _ in
print("开屏兜底放行: \(why)")
self?.goMain()
}
}
@objc private func appWillResignActive() {
if adShown { guardTimer?.invalidate(); guardTimer = nil }
}
@objc private func appDidBecomeActive() {
if adShown && !jumped { armGuard(Guard.shown, why: "回到 App 后仍无 didClose") }
}
private func goMain() {
guard !jumped else { return }
jumped = true
guardTimer?.invalidate()
guardTimer = nil
NotificationCenter.default.removeObserver(self)
splashAd?.destroy()
splashAd = nil
view.window?.rootViewController = MainViewController()
}
}交互样式
开屏广告的「摇一摇」「扭一扭」「上滑」等交互样式由广告平台后台配置, 热区设置过大即构成诱导点击,是监管通报的高发情形。 平台后台配置的交互样式优先于 SDK 侧的设置,须请平台运营在各广告平台后台逐项确认。 详见《合规与隐私》「广告交互样式」。