
简介面向使用Unity引擎开发iOS应用并计划接入AppLovin Max Native广告的开发者这份资料围绕原生广告从SDK导入、视图布局到展示回调的落地过程提供可参考的工程素材与配置示例。包内共58个文件约367KB含21个png界面素材、15个meta、8个json配置、3个h头文件以及nib视图、Swift/OC桥接源码、MaxNativeAdFramework框架、Assets.car和plist等覆盖广告资源管理、广告加载与事件处理等环节。已有106人学习。读者可据此了解Native广告在Unity iOS项目中的目录组织与关键文件配合方式参考手动广告视图、图片素材和桥接实现排查SDK导入、广告位配置与展示区域适配中的常见问题适合具备一定Unity iOS基础、希望补齐Native广告接入细节的开发者查漏补缺。1. iOS 上做 Unity AppLovin MAX 的 Native 广告难点在视图归属在 iOS 上用 Unity 接 AppLovin MAX 的 Native 广告很多人第一次接都会卡在同一个地方MaxSdk 的激励视频、插屏能正常显示但换成 Native 就找不到可以承载它的界面了。原因也不复杂激励视频和插屏是 SDK 自己弹出一个全屏控制器Unity 不需要关心而 Native 广告的本质是“把广告素材交给你的原生视图去渲染”它没有固定尺寸也没有固定容器最后必须由你决定广告挂在哪个 UIView 上、用哪个 viewController 注册点击、素材由谁下载。这件事能解决三个问题一是原生广告的展示位置能和你的界面贴合不再出现插屏遮挡按钮二是拿到和原生内容一致的视觉密度用户不容易“跳过”三是 eCPM 通常高于同位置 Banner。适合已经在 MAX 后台开好 Native 广告位、Unity 工程能出 iOS 包、但卡在“素材怎么显示、点击怎么回调”这一步的人。下面按选型、iOS 骨架、Unity 桥接、排查、验证的顺序讲。2. 三条渲染路线怎么选从 MAX 模板到自绘 UIViewNative 广告在 iOS 侧只有一种数据形态一个 MANativeAd 对象里面装着标题、正文、CTA 文案、图标 URL、主图视图、评分、隐私图标这些字段。差别全在“谁把这些字段画到屏幕上”。Unity 项目里能走的路只有三条选错一条后面全是返工。2.1 MAX 自带 MANativeAdView 模板上线最快改版最慢MAX iOS SDK 提供一个预制容器 MANativeAdView它把 titleLabel、bodyLabel、callToActionButton、iconImageView、mediaView、optionsView 这些 outlet 都留好了你只要按字段赋值再调一次 registerViewForInteraction 就能跑通点击。它支持从 xib 加载所以外观能改改的是控件位置、字号、圆角改不了整体结构。适合的场景很明确广告位形态固定、设计稿就是“图标 标题 正文 按钮”的标准四件套、排期紧。我一般会建议第一次接先用它跑通链路——把数据流、回调、点击验证全部打通再决定要不要换自绘。这样出问题时你能确定是“接入错了”还是“自绘错了”不会两个变量一起动。代价在两处。第一是模板的控件是固定的你想做横滑卡片、想做双列瀑布流里那种不规则高度模板撑不起来。第二是模板的布局是 AutoLayout 写的Unity 这边没法参与一旦你的界面有安全区适配、刘海避让、动态字号就得回 xib 里改改完还得重新出包。所以它适合“先上线”不适合“长期迭代”。2.2 自绘 UIView可控性最高代价是双端各写一遍自绘就是你自己 new 一个 UIView把 MANativeAd 的字段一个个填进去最后把这个 view 注册成可点击区域。它给你完全自由的布局卡片高度可以跟着主图比例走CTA 按钮可以做成你 App 的统一按钮样式甚至可以把广告塞进 UICollectionView 的某个 cell。自绘的关键点在注册这一步不在画界面。常见做法是// 自绘容器把素材字段塞进自己的 view 层级 UIView *container [[UIView alloc] initWithFrame:CGRectZero]; UIImageView *icon [[UIImageView alloc] init]; UILabel *title [[UILabel alloc] init]; UILabel *body [[UILabel alloc] init]; UIButton *cta [UIButton buttonWithType:UIButtonTypeSystem]; icon.image nativeAd.icon.image; // icon 可能为 nil必须兜底 title.text nativeAd.title; body.text nativeAd.body; [cta setTitle:nativeAd.callToAction forState:UIControlStateNormal]; // 主图是 SDK 给的 view直接 addSubview不要自己下载 if (nativeAd.mediaView) { nativeAd.mediaView.frame mediaFrame; [container addSubview:nativeAd.mediaView]; } // 注册点击viewController 不能传 nilclickableAdChildViews 要包含所有可点控件 [nativeAd registerViewForInteraction:container withViewController:self.hostViewController withClickableAdChildViews:[cta, container]];三个参数各有含义。container 是“点击热区的外框”用户点框内任意位置都算点击hostViewController 用来 present 落地页传 nil 会导致点击后没有任何反应这是最高频的翻车点clickableAdChildViews 决定哪些子控件单独吞掉点击事件按钮必须放进去否则按钮上的点击会被上层手势吃掉。自绘的成本是维护iOS 写一遍布局Android 再写一遍两边的间距、字号、图片裁切规则要人肉对齐。如果你们的广告位设计要求双端像素级一致那不如走第三条路。2.3 Unity UGUI 回贴跨端统一但素材要自己搬运第三条路是把原生层降级成“数据管道”iOS 侧只负责加载广告、把字段和图片以字节形式回传给 C#界面完全用 UGUI 画。好处是双端一套 UI改版只改一次热更也方便。坏处是工作量从“画界面”转移到了“搬素材”而且搬得不好会出事。需要搬的东西有四类文本字段标题、正文、CTA、图标小图通常 128×128 以内、主图大图常见 1200×628 这种比例、隐私图标 optionsView。文本走 JSON 没问题图片走字节数组就有坑解码后的 Texture2D 是未压缩的 RGBA一张 1200×628 的图在内存里接近 3MB一个列表十条就是 30MB低端机直接触发内存警告被系统杀掉。所以回贴路线必须配纹理压缩和 LRU 淘汰不能来一张留一张。还有一点容易被忽略UGUI 回贴之后点击事件是你自己在 C# 里接的得再回调原生层去触发 nativeAd 的点击上报。如果漏了这一步广告能显示、能点开落地页但 MAX 后台的点击数是零收益结算会出问题。这条链路一定要在联调阶段用测试模式验证一遍。2.4 选型对照与资源清单把三条路的边界摊开看选择就不纠结了维度MAX 模板 MANativeAdView自绘 UIViewUnity UGUI 回贴首次上线耗时最短半天到一天中等2~3 天最长3~5 天布局自由度低控件结构固定高任意卡片形态高且双端统一双端一致性各写各的各写各的一套 UI主图资源处理SDK 给 mediaView不用管SDK 给 mediaView不用管自己下载、解码、压缩点击上报SDK 自动SDK 自动需手动回调原生层主要风险设计稿撑不住双端维护成本内存峰值与点击漏报资源侧的准备清单也跟着路线走。走前两条路你只需要准备占位图和加载态icon 为空时用什么灰底、mediaView 还没 attach 时容器高度怎么定、加载失败时整个卡片是隐藏还是收缩。走第三条路额外要准备图标和主图的磁盘缓存目录、纹理压缩格式iOS 上 ASTC 6×6 是常见选择、以及一套“图片没下完但广告已经曝光”的降级样式——这时候宁可显示纯文字卡片也不要留一块灰。提示三条路可以在同一个工程里共存但同一个广告位不要混用。混用会导致同一个 MANativeAd 被注册两次点击后台点击率翻倍数据看着好看结算时对不上。3. iOS 原生侧最小可跑骨架加载、回调、素材映射先把 iOS 侧跑通再接 Unity。顺序反了的话出问题你分不清是 SDK 没加载到广告还是桥接把回调弄丢了。这一章的代码都放在一个 .mm 文件里编译进 Unity 导出的 Xcode 工程。3.1 初始化顺序ATT、SDK 初始化、预加载谁先谁后这三件事的先后顺序直接决定首次填充率。正确的顺序是App 启动 → 请求 ATT 授权 → MAX SDK 初始化 → 加载第一条 Native 广告。ATT 放最前面是因为 iOS 14 之后没有授权就拿不到 IDFAMAX 侧会按无 IDFA 的流量去竞价eCPM 明显偏低。授权弹窗必须在 SDK 初始化之前弹弹完再初始化这样 SDK 拿到的就是最终状态。常见错误是先初始化 SDK 再弹授权用户点了允许但 SDK 已经用旧状态建立过一次会话只能等下次冷启动才生效。初始化代码本身很短// AppController.mm 或任意启动时机 #import AppLovinSDK/AppLovinSDK.h #import AppTrackingTransparency/AppTrackingTransparency.h - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 1. 先要授权回调里再初始化 SDK if (available(iOS 14, *)) { [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) { dispatch_async(dispatch_get_main_queue(), ^{ [self setupMAX]; }); }]; } else { [self setupMAX]; } return YES; } - (void)setupMAX { ALSdkConfiguration *config [ALSdk shared].configuration; // 静音开关、测试模式等按需设置测试模式上线前务必关掉 [[ALSdk shared] initializeWithCompletionHandler:^(ALSdkConfiguration *configuration) { // 初始化完成后再预加载第一条 Native [self.nativeBridge load]; }]; }initializeWithCompletionHandler的回调是必须等的没等就 load 会拿到 “SDK not initialized” 的错误码。另外 Info.plist 里要提前写好 NSUserTrackingUsageDescription 文案和 SKAdNetworkItems 列表前者缺失会导致授权弹窗直接不出现后者缺失会让归因在部分渠道失效表现为“广告能展示但后台安装数对不上”。3.2 MANativeAdLoader 加载与 delegate 接线加载器是 MANativeAdLoader一个广告位一个实例不要全局共用一个。它有两个 delegate 属性容易搞混nativeAdLoaderDelegate 负责“加载成功/失败”nativeAdDelegate 负责“点击/过期”这类广告生命周期事件。两个都要设只设一个会出现“广告出来了但点不动”或者“点击了但收不到回调”。interface NativeAdBridge () MANativeAdLoaderDelegate, MANativeAdDelegate property (nonatomic, strong) MANativeAdLoader *loader; property (nonatomic, strong) MANativeAd *currentAd; property (nonatomic, weak) UIViewController *hostVC; end implementation NativeAdBridge - (instancetype)initWithAdUnitId:(NSString *)adUnitId host:(UIViewController *)host { if (self [super init]) { _hostVC host; _loader [[MANativeAdLoader alloc] initWithAdUnitIdentifier:adUnitId]; _loader.nativeAdLoaderDelegate self; _loader.nativeAdDelegate self; } return self; } - (void)load { // 部分版本用 loadAdWithPlacement:以所用 SDK 头文件签名为准 [self.loader loadAd]; } - (void)didLoadNativeAd:(MANativeAd *)nativeAd forAdUnitIdentifier:(NSString *)adUnitIdentifier { self.currentAd nativeAd; nativeAd.delegate self; // 单个广告的点击事件挂这里 [self renderAd:nativeAd]; // 进入素材映射 } - (void)didFailToLoadNativeAdForAdUnitIdentifier:(NSString *)adUnitIdentifier withError:(MAError *)error { // 按 error.code 分级无填充可退避重试SDK 未初始化直接报错不重试 NSLog([Native] load failed code%ld, (long)error.code); } - (void)didClickNativeAd:(MANativeAd *)nativeAd { // 打点这里能拿到点击但落地页跳转由 SDK 完成别自己再 present 一次 } - (void)didExpireNativeAd:(MANativeAd *)nativeAd { self.currentAd nil; // 过期必须置空否则会渲染出空白卡片 } end失败重试要分级。无填充no fill是常态退避 3~5 秒再试没意义一般退到 15~30 秒SDK 未初始化、广告位 ID 写错这类错误重试一万次也不会成功直接打日志上报即可。我见过最亏的一种写法是不管什么错误码都立刻重试请求量瞬间翻几十倍被后台判定异常流量限流恢复要等好几天。3.3 素材字段映射表与空值兜底MANativeAd 的字段不是每个广告都有值的尤其是 icon 和 starRating很多广告主压根不传。渲染前必须做空值判断否则就是一个纯灰的方块挂在界面上。字段类型是否必有缺失时的兜底titleNSString通常有缺失则整条广告不展示bodyNSString可能为空隐藏正文 label容器高度收缩callToActionNSString通常有缺失则不显示按钮但保留点击区iconMANImage含 image 与 imageURL可能为空用占位图或直接隐藏 icon 区域mediaViewUIView可能为空隐藏主图区改用纯文字卡片starRatingNSNumber可能为空隐藏评分行optionsViewUIView通常有必须展示政策要求最后一行是政策硬要求。optionsView 是那个小小的隐私图标用户点它能看到“为什么看到这条广告”。隐藏它会过不了审核也容易在渠道侧被判违规。稳妥做法是把它固定在卡片右上角尺寸不小于 20×20别用透明按钮遮住。主图这里有个细节mediaView 是 SDK 给你的 UIView不是 UIImage。你不需要也不能自己去下载主图直接 addSubview 并设置 frame 就行。自己下载反而会出问题——广告主的素材 CDN 可能带防盗链第三方下载拿到的是一张 403 的占位图用户看到的就是裂图。3.4 图标与主图的下载、解码与缓存icon 是个 MANImage它有两个属性image已经解码好的 UIImage可能为 nil和 imageURL原始地址。优先用 image为 nil 时再走 URL 下载。如果走的是原生 UIView 渲染下载用 NSURLCache 就够系统默认缓存策略能覆盖大部分场景- (void)loadIconIfNeeded:(MANImage *)icon into:(UIImageView *)imageView { if (icon.image) { imageView.image icon.image; // 首选SDK 已经解好了 return; } if (icon.imageURL.length 0) { imageView.image [UIImage imageNamed:ad_icon_placeholder]; return; } NSURLRequest *req [NSURLRequest requestWithURL:[NSURL URLWithString:icon.imageURL] cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:8]; [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) { if (!data.length) { return; } // 失败就保留占位图不要清空 UIImage *img [UIImage imageWithData:data]; dispatch_async(dispatch_get_main_queue(), ^{ imageView.image img; }); }]; }两个参数值得说。NSURLRequestReturnCacheDataElseLoad让同一张图第二次出现时直接命中本地缓存列表滚动不会重复请求timeoutInterval设 8 秒是因为原生广告的展示窗口很短超过 8 秒的图基本不用等了用户早划走了。走 UGUI 回贴路线的话这套逻辑要挪到 C# 侧用 UnityWebRequestTexture 下载并手动管理纹理缓存第 4 章会展开。这里先记住一个结论同一张图标 URL 在一天内会被请求几十次缓存没做好就是白烧流量和内存。4. Unity 与 Objective-C 的桥接DllImport、回调与资源过桥原生侧跑通之后Unity 要做的只有三件事告诉原生“加载”接收原生“加载好了/失败了”以及把广告视图放到正确的位置上。听上去简单实际翻车全在回调线程和对象生命周期上。4.1 extern C 导出与 C# P/Invoke 的最小闭环iOS 上 Unity 的 C# 调用原生只有一条路DllImport(__Internal)。原生侧的符号必须用 extern C 导出且不能是 C 的 mangled name否则运行时找不到符号表现是启动即崩或者调用时抛 EntryPointNotFoundException。using System; using System.Runtime.InteropServices; using AOT; using UnityEngine; public class NativeAdBridge : MonoBehaviour { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void NativeAdEventCallback(int eventType, IntPtr payloadUtf8); [DllImport(__Internal)] private static extern void NA_Init(string adUnitId); [DllImport(__Internal)] private static extern void NA_SetCallback(NativeAdEventCallback cb); [DllImport(__Internal)] private static extern void NA_Load(); [DllImport(__Internal)] private static extern void NA_Destroy(); // 必须用静态字段托住委托实例否则可能被 GC 回收 private static NativeAdEventCallback _callback; void Awake() { DontDestroyOnLoad(gameObject); // 切场景不能销毁否则回调无处可去 _callback OnNativeAdEvent; NA_SetCallback(_callback); NA_Init(你的_Native_广告位_ID); } void OnDestroy() { NA_Destroy(); } [MonoPInvokeCallback(typeof(NativeAdEventCallback))] private static void OnNativeAdEvent(int eventType, IntPtr payloadUtf8) { string json payloadUtf8 IntPtr.Zero ? null : Marshal.PtrToStringAnsi(payloadUtf8); // 已经在主线程可以直接碰 GameObject 和 UI NativeAdDispatcher.Dispatch(eventType, json); } }原生侧对应实现typedef void (*NAEventCallback)(int eventType, const char* payloadUtf8); static NAEventCallback gCallback NULL; static NativeAdBridge *gBridge nil; extern C void NA_SetCallback(NAEventCallback cb) { gCallback cb; } extern C void NA_Init(const char* adUnitId) { NSString *unitId [NSString stringWithUTF8String:adUnitId]; gBridge [[NativeAdBridge alloc] initWithAdUnitId:unitId host:UnityGetGLViewController()]; } extern C void NA_Load(void) { [gBridge load]; } static void Emit(int type, NSDictionary *payload) { if (!gCallback) { return; } NSData *d [NSJSONSerialization dataWithJSONObject:payload ?: {} options:0 error:nil]; NSString *s [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding]; dispatch_async(dispatch_get_main_queue(), ^{ gCallback(type, s.UTF8String); // 只在主线程回调 }); }UnityGetGLViewController()是 Unity 导出的函数返回承载渲染的那个控制器注册点击必须用它不能用 keyWindow.rootViewController——后者在某些弹窗场景下是另一个控制器用它注册点击会导致落地页从错误的栈弹出。还有一个坑s.UTF8String指向的内存只在当前 autoreleasepool 内有效。C# 侧必须在回调里立刻把字符串拷贝出来PtrToStringAnsi 就是拷贝不能存 IntPtr 等以后再用那是悬空指针表现是偶发的乱码或者崩溃。4.2 回调线程为什么必须在主线程回 UnitySendMessageMAX 的加载回调不保证在哪个线程触发尤其是无填充和超时这两类经常从内部队列回来。Unity 的 API 全部要求主线程如果在子线程里改 GameObject 的 transform或者调 Instantiate轻则布局错乱重则直接崩。所以原生侧所有回 Unity 的路径都要套一层 dispatch_async(main_queue)上面代码里的 Emit 就是干这个的。这一步不套你会遇到一种很玄学的现象广告偶尔显示偶尔不显示日志里明明有“加载成功”界面就是没反应。另一种更省事的写法是用 UnitySendMessage它天然要求主线程调用也不需要处理字符串封送UnitySendMessage(NativeAdBridge, OnNativeAdLoaded, [self jsonStringForAd:nativeAd].UTF8String);C# 侧就是普通的 MonoBehaviour 方法。它的限制是消息只能发给一个 GameObject 上的方法方法名写错不会报错、只会静默失败而且传参只能是单个字符串。小项目用它足够要传二进制、要高频回调还是走函数指针那条路少一层字符串序列化。4.3 UIImage 过桥Texture2D 的长度、格式与内存峰值只有 UGUI 回贴路线需要把图搬进 Unity。搬的方式有两种我一般推荐第二种。第一种是原生侧把 UIImage 转成 PNG/JPEG 的 NSData再把字节传过去C# 侧 Texture2D.LoadImage 解码。缺点是编解码两次一张 1200×628 的图要几十毫秒列表快速滚动的时候明显卡顿而且 JPEG 解码出来的纹理依然是 RGBA32内存一点没省。第二种是干脆不过桥C# 侧直接拿 imageURL 用 UnityWebRequestTexture 下载。原生侧只负责把 URL 和字段用 JSON 传过来图片的下载、缓存、压缩全在 Unity 侧统一管理跟其他网络资源走同一套缓存池。链路清晰也方便做预加载。// 带磁盘缓存的图标下载同一 URL 只下一次 private IEnumerator DownloadTexture(string url, ActionTexture2D onDone) { string key Md5(url); string path Path.Combine(Application.persistentDataPath, adcache/ key); if (File.Exists(path)) { byte[] cached File.ReadAllBytes(path); Texture2D t new Texture2D(2, 2, TextureFormat.ASTC_6x6, false); if (t.LoadImage(cached)) { onDone(t); yield break; } } using (UnityWebRequest req UnityWebRequestTexture.GetTexture(url, true)) { req.timeout 8; yield return req.SendWebRequest(); if (req.result ! UnityWebRequest.Result.Success) { onDone(null); yield break; } Texture2D tex DownloadHandlerTexture.GetContent(req); tex.Compress(false); // 立刻压缩别留 RGBA32 File.WriteAllBytes(path, tex.EncodeToJPG(85)); onDone(tex); } }TextureFormat.ASTC_6x6是 iOS 上的常见压缩格式压缩后一张主图从接近 3MB 降到 300KB 左右列表十条也不会触发内存警告。tex.Compress(false)的 false 表示不要求同步压缩允许走后台队列。缓存目录一定要清否则半年后用户手机上会躺着几百兆的广告图常见策略是保留最近 7 天启动时扫一遍按修改时间删。注意如果走原生 UIView 渲染就完全不需要这段代码。素材过桥是回贴路线专属的成本别为了“统一”把简单的事做复杂。4.4 生命周期nativeAd 谁持有、什么时候释放MANativeAd 的生命周期比想象的长。它被注册了点击之后SDK 内部还会持有一段时间用于归因如果你把它置空了但视图还在屏幕上用户点击会没有任何反应日志里也没有错误——典型的黑匣子问题。正确的做法是三层同步原生侧 currentAd 强引用着视图挂在原生层级上两者同时存在同时销毁。Unity 侧切场景或者关闭广告页时必须显式调一次销毁接口让原生把视图从父视图上移除、把 currentAd 置空、把 delegate 置 nil。漏掉 delegate 置 nil 会造成更隐蔽的问题delegate 通常是一个单例桥接对象nativeAd 强引用 delegatedelegate 又强引用 loaderloader 再引用 nativeAd绕成一个环。一条广告泄漏看着不多一天几十次冷启动加上百次加载内存曲线就是一路上扬的。5. 排查清单真机白屏、裂图、回调丢失的 5 个现场下面五条都是我在联调阶段真实撞过的按“现象、原因、解决”记遇到同类问题可以直接对号。5.1 现象一编辑器里一切正常真机一片空白现象Unity Editor 里跑日志能看到加载成功打出 iOS 真机包之后广告位区域什么都没有也没有报错。原因原生视图被加到了 keyWindow 上而 Unity 在 iOS 上用的是自己的渲染窗口和 GL 控制器keyWindow 上的视图被 Unity 的渲染层盖住了或者被加到已经不在视图层级里的控制器上。解决把 adView 加到UnityGetGLViewController().view上而不是[UIApplication sharedApplication].keyWindow。加完再确认一次 frame 的坐标系——原生视图用的是屏幕坐标Unity 传过来的位置是 Canvas 坐标中间要按[UIScreen mainScreen].scale换算不换算的典型表现就是“广告出现在右下角还只有一半”。5.2 现象二标题出来了图标和主图是灰块现象文字字段显示正常图标位置是个纯灰的圆主图位置是一块空白偶尔过几秒才出来。原因主图用的是自己下载的 UIImageView 而不是 SDK 给的 mediaView广告主素材 CDN 带防盗链或需要特定 Referer第三方下载拿到 403或者图标 URL 用了 http被 ATS 拦掉。解决主图一律 attach nativeAd.mediaView不要自己下。图标确认 URL 是 https若确实有 http 资源在 Info.plist 里按域名加 NSExceptionDomains别开全局 NSAllowsArbitraryLoads那会在提审时被追问。另外给图标加一个 8 秒超时和占位图超时不清空已经显示的内容。5.3 现象三原生日志有回调C# 侧毫无反应现象Xcode 控制台打出 didLoadNativeAd 的日志Unity 侧对应的 C# 方法一次都没进。原因三种可能。一是 UnitySendMessage 的 GameObject 名写错它不会报错只会静默失败二是那个 GameObject 在切场景时被销毁了三是函数指针委托被 GC 回收了回调打进来指向已释放的内存某些情况下直接静默吞掉。解决接收回调的 GameObject 挂常驻单例并 DontDestroyOnLoad委托实例用静态字段保存别用局部变量如果还是不通在原生回调里加一行 NSLog 确认走到哪一步再在 C# 的接收方法第一行加 Debug.Log两头都打日志二十分钟能定位。5.4 现象四点击没反应或者跳了两次现象广告展示正常点 CTA 按钮没反应另一种是点击之后落地页弹两次。原因没反应多半是 registerViewForInteraction 的 viewController 传了 nil或者 clickableAdChildViews 没包含按钮按钮的点击被自己的手势吞了。跳两次是自己在 didClickNativeAd 里又手动 present 了一次落地页而 SDK 已经跳过一次。解决viewController 传 UnityGetGLViewController()clickableAdChildViews 把按钮和所有可点容器都塞进去didClickNativeAd 里只做打点不要碰跳转逻辑。如果一定要自定义跳转就把 SDK 的自动跳转关掉二选一别两边都做。5.5 现象五填充率正常收益却一路下滑现象后台显示请求量和填充率都正常展示量也不低但 eCPM 一周比一周低。原因最常见的是重复注册点击。同一个 nativeAd 被注册了两次比如列表复用 cell 时没先 unregister 就重新 register后台点击率虚高广告主侧识别为异常流量后压低出价。其次是广告位和内容混排得太像用户误点率高同样会被判定为低质量流量。解决cell 复用时先调一次反注册或者直接把旧 view 从层级移除再重建同一屏不要堆超过两条 Native在卡片上加一个不显眼但可辨识的“广告”标识误点率降下来eCPM 反而会回升。这条属于血泪经验数据掉的时候先查点击率别急着换广告位。6. 把原生广告位做稳的三个动作预加载池、占位高度与数据验证链路跑通只是及格线。真正决定这个广告位能不能持续出量的是三件事我按投入产出比排一下。第一件是做预加载池。别等界面要显示了才去 load那时候用户已经在看空白了。常见做法是维护一个容量 2~3 的池子展示消费一条立刻补一条池子空了才触发阻塞加载// 简化的预加载池消费即补容量上限 3 private readonly Queuestring _pool new Queuestring(); private const int MaxPool 3; public void EnsureFilled() { while (_pool.Count MaxPool) { int before _pool.Count; NativeAdBridge.Load((json) { if (json ! null) { _pool.Enqueue(json); } }); if (_pool.Count before) { break; } // 本次没成功别死循环 } } public string Consume() { string ad _pool.Count 0 ? _pool.Dequeue() : null; EnsureFilled(); // 消费后立刻补位 return ad; }注意break那一行。无填充的时候回调是同步失败还是异步失败取决于版本如果是同步失败while 就会变成死循环把主线程卡住。加一个“本轮没增加就退出”的判断代价只是补位慢一拍。第二件是占位高度。原生广告高度不固定主图有没有、正文几行都会让卡片高度跳变。列表里最难看的就是广告加载完成后把下面的内容顶下去。稳妥做法是按广告位预先定一个最小高度比如主图按 16:9 算再加文字区固定 96加载完成后只允许在这个高度内自适应超出部分裁切。数据上不好看但用户滑动体验稳长期留存更好。第三件是验证。上线前至少跑这四个数指标观察窗口异常信号请求量 / 展示量首日比值低于 0.3说明加载了没展示点击率 CTR首周高于 5%大概率重复注册展示时长中位数首周低于 2 秒位置可能在屏幕外崩溃率首周上升 0.1 个百分点以上先查纹理内存CTR 那一行特别值得盯。原生广告的正常点击率通常比 Banner 高但高到离谱一定是接错了。我第一次接的时候就吃过这个亏列表复用没做反注册CTR 冲到 12%当时还挺高兴两周后 eCPM 掉了四成回头查了两天才发现是重复注册。如果只能给一个建议先用 MAX 模板把链路跑通、把四个指标测出来再考虑自绘或者 UGUI 回贴。顺序颠倒的话你会花大量时间在区分“是桥接错了还是布局错了”上。希望帮到你。本文还有配套的精品资源点击获取