简介:面向Unity iOS开发者,这份资料包聚焦AppLovin Max Native广告的接入与展示,提供可直接参考的工程级示例,涵盖SDK初始化、广告位加载、原生模板视图与事件回调等关键环节,适合已具备基础Unity与iOS打包经验、希望提升广告变现能力的中级开发者。包内共58个文件,约367KB。以21张png界面素材和若干json配置、meta文件为主,辅以Objective-C/C++混编源码、Swift模块说明文件、nib视图、framework及Assets.car资源,能较完整地呈现原生广告的UI布局与桥接结构。已有106人学习。读者可从中获得Native广告视图的搭建思路、素材组织方式、Unity与iOS原生层交互的参考实现,以及接入过程中常见配置与回调处理的可复用代码片段。
1. 一个 Native 广告位,为什么最后靠 Objective-C 才贴得准
设计稿发过来时是一张卡片:左上 48×48 的 icon,右侧两行标题加一行描述,底部一条 CTA 按钮,圆角 12,半透明底。接入 Unity 侧现成的横幅和 MREC 接口后,实际出来的是一条 320×50 的横条,位置、比例、样式全对不上,美术看过一眼就摇头。AppLovin MAX 聚合里能按你自己的版式渲染素材的只有 Native 这一路,而 Native 在 iOS 上最终落到的是一个原生 UIView,不是 UGUI 里能直接塞进 RawImage 的贴图。所以只要 Unity 项目要在 iOS 上做原生卡片位,就绕不开一层 Objective-C 桥接:把 MANativeAdLoader 包成 Unity 能调的函数,再把原生广告视图按 UGUI 的位置贴到画面上。下面按加载链路、桥接层、坐标与层级、翻车现场、上线自测五段讲,适合已经跑通横幅和激励视频、准备上原生位的团队。
2. MAX 的 Native 链路:从 MANativeAdLoader 到能点的 UIView
2.1 请求、填充、渲染是三步,不是一步
MAX 后台创建 Ad Unit 时要选 Native 格式,这是第一步,选错了后面全白搭。一个 Ad Unit 一旦建成其他格式,改格式等于新建,历史数据不继承。创建完把 Ad Unit ID 抄到 Unity 侧配置里,同时确认这个 Unit 在后台挂了至少一条 Bidding 或 Waterfall 线路,否则本地请求会稳定返回空。
iOS 侧的加载入口是 MANativeAdLoader。它和横幅、插屏的最大区别是:加载成功的回调给你的不是“一个可以 show 的视图”,而是“一份素材对象”。素材里有标题、描述、CTA 文案、icon、主图或视频容器,怎么摆、摆多大、什么背景色,全是你的代码决定的。这一步想不通的人会在文档里找 show 方法找半天——Native 没有传统意义上的 show,只有“把素材渲染进我提供的视图,然后把视图加到屏幕上”。
渲染这一步有两个选择。一是用 SDK 提供的原生广告视图模板(xib 形态),通过 binder 把素材字段绑上去,省事但版式固定;二是自己用 UIView 拼一个容器,手动把 titleLabel、bodyLabel、ctaButton、iconImageView、mediaViewContainer 一个个赋值。设计稿越花,越要走第二条。无论哪条,mediaView 容器必须存在且尺寸非零,视频类素材就是往这里渲染的,缺了它你会看到“广告加载成功但一片空白”,这个问题后面避坑章还会再提一次。
2.2 纹理回传看起来省事,但走不远
我见过最常见的偷懒方案是:把原生广告视图渲染成 UIImage,转成 Texture2D,塞进 UGUI 的 RawImage。代码量小,布局用 UGUI 的 RectTransform 调,产品改稿也快,第一天跑起来确实爽。
但这条路在真实场景里会接连出问题。第一,素材里的视频不会动,静态首帧就是永远的首帧,点击率会被拉低。第二,每帧或每次刷新截图有性能成本,中低端机上滑动手感会掉。第三,点击热区要自己从 UGUI 坐标反算到原生视图坐标,CTA 按钮的点击判定一旦偏十几个点,用户点不动就是点不动。第四,Retina 下放大后边缘发虚,设计验收过不了。第五,无障碍和系统级的广告标识会被截断。
常见做法是原生视图覆盖:广告视图真真实实加在 UIKit 层级上,Unity 只负责告诉它“放在屏幕的哪个矩形里”,点击、视频播放、跳转全部由原生层自己处理。代价是坐标和层级要自己算准,而这正是最容易翻车的地方。
2.3 工程接线:Podfile 会被覆盖,源头在 Dependencies.xml
Unity 导出 iOS 工程后,依赖管理有两种情况。如果工程里用了 EDM4U(External Dependency Manager),Podfile 是自动生成的,你手动加一行 pod,下次构建会被原样覆盖掉,白改。这时候真正的源头是工程里的 Dependencies.xml,把 iOS 依赖写进去,让 EDM4U 去生成 Podfile。
<!-- Assets/Plugins/Editor/Dependencies.xml 片段 --> <dependencies> <iosPods> <!-- 聚合 SDK 本体,版本区间按后台文档锁,不要写 latest --> <iosPod name="AppLovinSDK" version="~> 12.0" minTargetSdk="13.0" /> <!-- mediation adapter 按后台给的清单逐个加,全量接入会让包体明显膨胀 --> <iosPod name="AppLovinMediationXXXAdapters" version="~> 1.0" minTargetSdk="13.0" /> </iosPods> </dependencies>如果工程没接 EDM4U,直接维护 Podfile:
# Podfile,只在没有 EDM4U 的工程里手写 platform :ios, '13.0' target 'UnityFramework' do # 注意 target 是 UnityFramework,不是 Unity-iPhone, # 桥接代码编译在 framework 里,pod 也要跟着进去 pod 'AppLovinSDK' pod 'AppLovinMediationXXXAdapters' end两个容易忽略的点。第一,target 必须是 UnityFramework,写错 target 后桥接文件找不到头文件,报一堆 use of undeclared identifier。第二,桥接的 .mm 文件放在 Assets/Plugins/iOS/ 下,Unity 会自动加进 Xcode 工程,但如果你手动改过导出工程,重新导出会丢,养成“改 Unity 侧源文件、不改导出产物”的习惯。
2.4 方法签名以头文件为准,别照抄旧博客
MAX 的 Native 回调签名在小版本之间调整过,参数列表里多一个 adView 或少一个,编译期就报错,或者编译过了但拿到的对象是 nil。接手项目第一件事不是写代码,是确认手上的 SDK 到底长什么样:
# 在导出的 iOS 工程根目录执行,确认 native 相关类和方法签名 grep -rn "MANativeAdLoader" Pods/AppLovinSDK/ --include=*.h | head -20 grep -rn "didLoadNativeAd" Pods/AppLovinSDK/ --include=*.h grep -rn "MANativeAdViewBinder" Pods/AppLovinSDK/ --include=*.h | head -20这三条命令的输出就是你的“正确文档”。参数名、是否带 adView、binder 的属性叫什么,全部以这里为准。网上抄来的代码如果和头文件对不上,别怀疑头文件,怀疑抄来的代码。
提示:把这三条命令的输出贴进项目 README,下次升级 SDK 时先跑一遍对比,能省掉一次通宵排查。
3. 桥接层怎么写:把 Native 广告包成 Unity 能 DllImport 的四个函数
3.1 接口只留四个,别把整个 SDK 都暴露出去
Unity 和原生之间的边界越窄越好维护。我的习惯是只导出四个 C 函数,Unity 侧不需要知道 MAX 的任何类名。
| C 函数 | 作用 | 调用时机 |
|---|---|---|
| MaxNative_Init | 初始化聚合 SDK,打开测试模式开关 | 启动时一次 |
| MaxNative_Load | 按 Ad Unit ID 发起加载,同时传入屏幕矩形 | 页面打开、失败重试 |
| MaxNative_Show | 把已渲染好的容器挂到窗口上 | 加载成功且页面可见时 |
| MaxNative_Hide | 从父视图移除并释放素材引用 | 页面关闭、切场景 |
四个函数都是幂等的:Load 在加载中时直接忽略、Show 在没素材时返回、Hide 调两次不出错。这个约束看着笨,但能挡掉 Unity 侧绝大多数乱序调用。
3.2 Objective-C++ 实现骨架
// MaxNativeBridge.mm // 放在 Assets/Plugins/iOS/ 下,Unity 自动加入 Xcode 工程 #import <AppLovinSDK/AppLovinSDK.h> #import <UIKit/UIKit.h> #import "UnityInterface.h" // UnitySendMessage / UnityGetMainWindow static MANativeAdLoader *gLoader = nil; static MANativeAd *gNativeAd = nil; static UIView *gContainer = nil; // 我们自己的卡片容器 static UIWindow *gAdWindow = nil; // 覆盖在 Unity 之上的窗口 extern "C" { void MaxNative_Init(const char *sdkKey, BOOL testMode) { // 初始化必须走主线程,Unity 启动早期调用要切一下 dispatch_async(dispatch_get_main_queue(), ^{ ALSdk *sdk = [ALSdk sharedWithKey:[NSString stringWithUTF8String:sdkKey] settings:[ALSdkSettings new]]; [sdk initializeSdkWithCompletionHandler:nil]; // 测试设备用 SDK 内部日志开关,别把测试标识写进线上包 (void)testMode; }); } void MaxNative_Load(const char *adUnitId, float px, float py, float pw, float ph, float unityScreenW, float unityScreenH) { dispatch_async(dispatch_get_main_queue(), ^{ NSString *unit = [NSString stringWithUTF8String:adUnitId]; if (!gLoader) { gLoader = [[MANativeAdLoader alloc] initWithAdUnitIdentifier:unit]; // delegate 是弱引用,代理对象要用 static 持有,别用局部变量 gLoader.delegate = (id<MANativeAdLoaderDelegate>)[MaxNativeDelegate shared]; } // 每次加载都重建 loader 时记得重置 adUnitId,复用 loader 时只改 id CGRect uikitRect = UnityPixelRectToUIKit(CGRectMake(px, py, pw, ph), CGSizeMake(unityScreenW, unityScreenH)); [MaxNativeDelegate shared].pendingRect = uikitRect; [gLoader loadAd]; }); } void MaxNative_Show(void) { dispatch_async(dispatch_get_main_queue(), ^{ if (!gContainer || !gAdWindow) { return; } gAdWindow.hidden = NO; [gAdWindow makeKeyAndVisible]; }); } void MaxNative_Hide(void) { dispatch_async(dispatch_get_main_queue(), ^{ [gContainer removeFromSuperview]; gAdWindow.hidden = YES; gNativeAd = nil; // 断掉素材引用,避免退出页面后仍占内存 }); } } // extern "C"几个关键点。第一,所有 UI 和 SDK 操作都 dispatch 到主线程,Unity 的调用线程不保证是主线程,省这一步会出现偶发的 Main Thread Checker 报错。第二,delegate 必须是长期存活的对象,写成一个单例,写成局部变量会在方法返回后变成野指针,回调时直接崩。第三,Hide 里显式置空 gNativeAd,这是内存增长问题最常见的一处遗漏。
3.3 加载回调:只做两件事
// MaxNativeDelegate.mm 核心回调 - (void)didLoadNativeAd:(MANativeAd *)nativeAd forAdUnitIdentifier:(NSString *)adUnitIdentifier { gNativeAd = nativeAd; // 渲染进自己的容器,第二步再挂窗口 [self renderNativeAd:nativeAd intoRect:self.pendingRect]; UnitySendMessage("MaxNativeReceiver", "OnNativeAdLoaded", adUnitIdentifier.UTF8String); } - (void)didFailToLoadNativeAdForAdUnitIdentifier:(NSString *)adUnitIdentifier withError:(MAError *)error { gNativeAd = nil; // 把 code 和 message 一起回传,Unity 侧才好决定是重试还是报错埋点 NSString *payload = [NSString stringWithFormat:@"%ld|%@", (long)error.code, error.message ?: @""]; UnitySendMessage("MaxNativeReceiver", "OnNativeAdFailed", payload.UTF8String); }回调方法名以 2.4 节 grep 出来的头文件为准,参数列表不一致时按头文件改,逻辑不变。回传给 Unity 的错误信息里带上 code,非常重要——后续按错误码分类统计,能一眼看出是“没库存”还是“配置写错了”。
3.4 Unity 侧:平台宏 + 回调派发
// MaxNativeBridge.cs using System; using System.Runtime.InteropServices; using UnityEngine; public static class MaxNativeBridge { #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void MaxNative_Init(string sdkKey, bool testMode); [DllImport("__Internal")] private static extern void MaxNative_Load( string adUnitId, float x, float y, float w, float h, float screenW, float screenH); [DllImport("__Internal")] private static extern void MaxNative_Show(); [DllImport("__Internal")] private static extern void MaxNative_Hide(); #endif public static void Load(string adUnitId, Rect rectInPixels, Vector2 screenInPixels) { #if UNITY_IOS && !UNITY_EDITOR MaxNative_Load(adUnitId, rectInPixels.x, rectInPixels.y, rectInPixels.width, rectInPixels.height, screenInPixels.x, screenInPixels.y); #else // 编辑器和其他平台走假数据,保证 UI 流程能跑通 MaxNativeReceiver.Instance?.OnNativeAdFailed("-1|editor_mock"); #endif } }这里有一个必须做的设计:Rect 用像素传,不要传 UGUI 的锚点或归一化坐标。UGUI 的锚点、CanvasScaler 的匹配模式、安全区补偿全在 Unity 侧算完,原生层只接受“屏幕左下角为原点的像素矩形”,职责单一,出问题好定位。
回调统一走 UnitySendMessage 到一个常驻 GameObject:
// MaxNativeReceiver.cs using UnityEngine; public class MaxNativeReceiver : MonoBehaviour { public static MaxNativeReceiver Instance { get; private set; } public event System.Action<string> OnLoaded; public event System.Action<string> OnFailed; private void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // 切场景时不销毁,否则原生回调找不到接收方 } // 原生层通过 UnitySendMessage 调到这里,方法名必须完全一致 public void OnNativeAdLoaded(string adUnitId) => OnLoaded?.Invoke(adUnitId); public void OnNativeAdFailed(string payload) => OnFailed?.Invoke(payload); }DontDestroyOnLoad 这一行别省。切场景时接收方被销毁,原生层回调打过来会静默失败,表现是“广告加载成功但界面永远不刷新”,排查起来非常耗时间。
4. 坐标系与视图层级:把原生 UIView 精确贴到 UGUI 的坑位
4.1 像素、点、nativeScale 三个概念先分清
Unity 的 Screen.width 返回像素,UIKit 的 bounds 用点,中间隔着一个缩放比。直接拿 Unity 矩形除以[UIScreen mainScreen].scale,在多数机型上对得上,在 Plus 系列和历史缩放模式上会偏。我的做法是用两端上报的屏幕宽度反算比例,不依赖任何固定常量:
// 用 Unity 上报的屏幕像素宽 / UIKit 的屏幕点宽,得到真实换算比 static CGRect UnityPixelRectToUIKit(CGRect pixelRect, CGSize unityScreenPx) { CGFloat uiWidthPt = [UIScreen mainScreen].bounds.size.width; if (uiWidthPt <= 0 || unityScreenPx.width <= 0) { return CGRectZero; } CGFloat ratio = unityScreenPx.width / uiWidthPt; // 像素 / 点 CGFloat x = pixelRect.origin.x / ratio; CGFloat w = pixelRect.size.width / ratio; CGFloat h = pixelRect.size.height / ratio; // Unity 原点在左下,UIKit 原点在左上,纵向要翻一次 CGFloat y = (unityScreenPx.height - pixelRect.origin.y - pixelRect.size.height) / ratio; return CGRectMake(x, y, w, h); }纵向翻转是最容易写错的一行。写错的表现是广告出现在屏幕顶部而不是 UGUI 里预留的位置,很多人第一反应是“坐标传错了”,实际是忘了翻 Y。
4.2 安全区和旋转:刘海屏上 CTA 被切一半
原生窗口如果覆盖全屏,容器贴到屏幕底部时会被 Home Indicator 压住,贴到顶部会被灵动岛区域盖住。处理方式是在容器的 layoutSubviews 里读 safeAreaInsets 做内缩:
- (void)layoutSubviews { [super layoutSubviews]; if (@available(iOS 11.0, *)) { UIEdgeInsets inset = self.safeAreaInsets; CGRect frame = self.frame; // 只在广告矩形与安全区重叠时才内缩,避免整体位移 if (CGRectGetMaxY(frame) > CGRectGetHeight(self.superview.bounds) - inset.bottom) { frame.origin.y -= inset.bottom; } self.frame = frame; } }旋转的处理更麻烦。Unity 旋转后屏幕宽高互换,原生窗口不会自动跟着重排。可靠做法是监听UIDeviceOrientationDidChangeNotification,在回调里延迟一帧(旋转动画结束)重新向 Unity 请求当前矩形,而不是自己猜新坐标。让 Unity 侧每次屏幕尺寸变化都重新调一次 Load 或一个新的 UpdateRect 函数,逻辑只有一份,不会两边算出不同结果。
注意:游戏锁定竖屏时也要处理“设备旋转但界面不转”的情况,部分机型会因此触发一次多余的布局,导致广告位置瞬移。
4.3 层级方案三选一,选错就是点不动
| 方案 | 实现方式 | 优点 | 风险 |
|---|---|---|---|
| 独立 UIWindow | 新建 UIWindow,windowLevel 高于主窗口 | 不被 Unity 视图裁剪,弹窗层级可控 | 键盘、旋转要自己转发;容易被误做成全屏遮挡 |
| 挂 rootViewController.view | addSubview 到 Unity 根视图 | 跟随旋转,改动最小 | 会被 Unity 后添加的视图盖住,需要手动置顶 |
| 挂 UnityView 子视图 | 直接加到渲染视图上 | 最简单 | Unity 渲染层可能覆盖它,触摸事件容易冲突 |
我一般选独立 UIWindow,因为它把“广告层”和“游戏层”彻底解耦,页面关闭时整窗隐藏,不留残余视图。代价是要把窗口的isUserInteractionEnabled和 frame 管好——窗口如果铺满全屏,用户点游戏其他地方就会被吞掉。
4.4 素材绑定:mediaView 容器必须有尺寸
无论用 SDK 模板还是自绘容器,绑定时有几个字段不能漏:
// 自绘容器的绑定顺序,先赋值再布局,避免首帧空白 - (void)bindAd:(MANativeAd *)ad { self.titleLabel.text = ad.title; self.bodyLabel.text = ad.body; [self.ctaButton setTitle:ad.callToAction forState:UIControlStateNormal]; self.iconImageView.image = ad.icon.image; // mediaView 容器必须先有非零 frame,视频素材才会渲染 self.mediaContainer.frame = CGRectMake(0, 0, self.bounds.size.width, 180); [ad prepareViewForAd:self.adView]; }prepareViewForAd:这类方法名同样以头文件为准。顺序上,先把容器尺寸算好再调渲染,反过来会出现“素材已绑定但区域高度为 0”的白屏,肉眼看起来就是加载失败,实际是布局时序问题。
5. 避坑:Native 广告在 iOS 上最容易翻车的 5 个现场
5.1 测试机永远 no fill,线上填充率像过山车
现象:同一份代码,开发机连续几天一条广告都不出,日志里全是加载失败;换台设备又正常。上线后后台看填充率,早上 90%、晚上 40%,波动没有规律。
原因有两个层面。一是测试设备没有在后台登记,MAX 对未登记设备可能返回测试素材或直接不填充,尤其在你开了测试模式之后,测试模式下只走测试源,真实库存根本不参与。二是填充率本身和地域、时段、广告位类型强相关,Native 在部分地区的库存本就比激励视频薄,拿单一设备的观感去判断“SDK 有问题”是误判。
解决:先在后台把测试设备的标识登记进测试设备列表,测试阶段只看“能不能加载出素材、样式对不对”,不看收益。判断填充是否正常,用后台按国家和广告位维度看趋势,连续观察三天再下结论。本地排查时把错误码打全,区分“无库存”和“配置错误”两类,前者重试有意义,后者重试一万次也没用。
5.2 广告明明在那儿,手指戳上去毫无反应
现象:卡片渲染出来了,图片文字都对,点 CTA 按钮没有任何跳转,连按下的高亮都没有。
原因通常出在视图层级和触摸传递上。最常见的是容器外层包了一个透明遮罩视图,isUserInteractionEnabled默认是 YES 但没实现任何手势,触摸全被它吃掉。其次是广告窗口虽然显示,但windowLevel设得比某个全屏 UI 低,视觉上能看到,触摸却落到了上层视图。还有一种隐蔽情况:Unity 侧在广告上方盖了一个透明的 UGUI 面板用于拦截游戏输入,UGUI 的射线检测和 UIKit 的 hitTest 是两套体系,肉眼看不出来谁在上面。
解决:用 Xcode 的 View Debugger 逐层看 hitTest 落到哪个视图,比打印日志快十倍。广告容器的父级链上,除按钮和可点击区域外,其余视图一律设isUserInteractionEnabled = NO。Unity 侧的拦截面板在广告展示期间关掉,或者把广告窗口层级抬高到它之上。
5.3 首帧白屏,或者高度塌成 0
现象:加载成功回调也收到了,广告区域一片白,过一两秒偶尔自己出现,滚动列表时又变白。
原因:渲染发生在布局之前。容器 frame 还是 CGRectZero,mediaView 容器没有尺寸,SDK 把素材塞进去也显示不出来。列表复用时更明显,容器被复用但没重新绑定。
解决:绑素材前强制[container layoutIfNeeded],确保约束已经算出实际尺寸;容器每次复用时先清空所有子视图再绑定;给容器一个最小高度兜底(比如 80pt),避免极端情况下高度为 0 导致整块消失。
5.4 提审被打回,理由和广告有关
现象:功能都正常,审核反馈说广告展示存在问题,或者要求补充隐私相关说明。
原因集中在三处:缺少跟踪授权相关的用途描述,系统弹窗弹不出来;广告遮挡了游戏关键操作区域或者关闭入口;广告样式诱导点击(比如把 CTA 做得像游戏内按钮)。这几条都是审核的高频关注点。
解决:Info.plist 里补齐用途描述文案,启动后合适的时机请求授权,被拒绝也要保证广告能正常展示非个性化素材。广告位不要压在主操作按钮上,卡片之间留出足够间距。CTA 文案用 SDK 给的原值,不要自己改成“领取奖励”之类的诱导话术。
5.5 来回切场景几次之后,崩在回调里
现象:单次展示正常,进游戏、退出、再进,第三次左右崩溃,堆栈指向原生广告的回调或者 delegate。
原因:Unity 侧的接收对象被销毁了,原生层还在往一个不存在的对象发消息;或者 Hide 只做了 hidden 没有 removeFromSuperview,素材对象越积越多;又或者 delegate 被设成了局部对象,方法返回即成野指针。
解决:接收对象挂 DontDestroyOnLoad;Hide 里做完整的三件事——removeFromSuperview、置空素材引用、把 delegate 置回单例;页面销毁时先调 Hide 再销毁 GameObject,顺序反了就会出现“对象已销毁、回调还在路上”的经典组合。
6. 上线前的一次量化自测:填充率、eCPM 和脚本化回归怎么跑
功能跑通不等于能上线。我一般在提测前做一轮量化自测,把“感觉能用”变成“数据上能用”。
先做一个最小回归脚本,在真机上连续跑 50 次加载,统计成功率和平均耗时:
// 只用于测试包的批量加载统计,正式包不打包 IEnumerator LoadLoop(int times) { int ok = 0; for (int i = 0; i < times; i++) { float t0 = Time.realtimeSinceStartup; MaxNativeBridge.Load(adUnitId, rect, screenSize); yield return new WaitForSeconds(8f); // 给加载留足超时窗口 // 结果由 OnLoaded / OnFailed 回调累加,这里只统计次数 Debug.Log($"[NativeTest] round={i} cost={Time.realtimeSinceStartup - t0:F1}s"); } Debug.Log($"[NativeTest] success={ok}/{times}"); }观察三个数:成功率低于 70% 说明配置或地域库存有问题,别急着怪代码;平均耗时超过 5 秒说明聚合层线路太多、超时设置偏长;连续失败集中在同一个错误码,直接去后台核对那个网络的接入状态。
再看一张自测项表,逐项过一遍再发包:
| 自测项 | 判定标准 | 出问题先看哪里 |
|---|---|---|
| 冷启动首次加载 | 3 秒内出素材 | 初始化时机是否早于广告请求 |
| 列表复用 | 滚动 20 次无白屏、无错位 | 容器是否每次重新绑定 |
| 横竖屏切换 | 位置跟随、不遮挡安全区 | 是否重新请求矩形 |
| 点击跳转 | CTA 可点、返回后游戏正常 | 视图层级与 hitTest |
| 连续 50 次加载 | 无崩溃、内存无持续增长 | Hide 是否完整释放 |
| 弱网 | 有超时兜底、不卡主线程 | 加载是否在主线程同步等待 |
最后是收益侧的观察。上线后按国家和广告位维度看 eCPM,Native 的 eCPM 通常低于激励视频但展示频次高,两个指标要一起看,只盯 eCPM 会得出“原生位不值得做”的错误结论。开启 Bidding 后前三天数据波动大,别急着调价,等填充稳定再看。
说个我自己的教训:早期做原生位,我把“加载成功”当成“展示成功”上报,结果后台数据和真实曝光差了将近四成,花了很久才定位到是布局时序导致的白屏。从那以后我坚持两条——曝光埋点必须放在原生层确认视图真正可见之后,任何涉及坐标和层级的改动都要在三种机型上各走一遍。原生广告这层桥接不难,难的是把每一个“看起来正常”的环节都验证一遍。希望帮到你。
本文还有配套的精品资源,点击获取