
简介本资源面向使用 Unity 引擎开发 iOS 应用的开发者聚焦 AppLovin Max 平台 Native 原生广告的接入实践帮助解决广告变现流程中 SDK 集成、广告位配置与事件回调处理等关键问题适合具备一定 Unity 与 iOS 基础的中级开发者参考。资源包共 58 个文件约 367KB包含 21 个 png 图片资源、15 个 meta 配置、8 个 json 数据文件以及 h、mm、m、swift 等源码文件与 MaxNativeAdFramework 框架、nib 界面文件、plist 配置等覆盖原生广告展示所需的素材、脚本与框架依赖。目前已有 106 人学习。通过该资源读者可获取 Native 广告接入所需的完整工程文件与框架结构理解广告视图布局、Swift 与 OC 混编桥接及广告事件回调的实现思路为自身项目的广告模块搭建与调试提供可复用的参考模板。1. 从一次真机崩溃说起Unity 接入 AppLovin MAX 原生广告到底难在哪去年帮一个做休闲游戏的朋友排查线上问题iOS 端接完 AppLovin MAX 的原生广告后Unity 编辑器里跑得好好的一上真机就白屏Xcode 控制台刷出一堆NativeAdView相关的空引用。这个场景其实特别典型——Unity 侧调用的是 C# 封装层真正干活的却是 iOS 原生那一套MAAdView、MANativeAdView和 delegate 回调中间隔着 P/Invoke 和 Objective-C 桥接任何一环没对齐都会翻车。这份资源就是围绕「Unity AppLovin MAX iOS 原生广告」这条链路整理的实战包包含桥接脚本、原生视图绑定示例、Info.plist 配置清单和几个典型崩溃的排查记录。它适合已经跑通基础激励视频、想进一步上原生广告变现的 Unity 开发者也适合被 iOS 审核和 SKAdNetwork 折腾过的同学。下面我按「先搞懂结构、再动手接、最后避坑」的顺序拆一遍。2. Unity 与 iOS 原生广告的桥接结构从 C# 到 Objective-C 的调用链2.1 为什么原生广告不能只靠 C# 搞定Unity 的广告 SDK 大多提供两种形态一种是纯 C# 的 Banner/Interstitial 封装另一种是原生广告Native Ad。原生广告的核心诉求是「让广告素材长得像 App 自己的内容」所以它必须拿到 iOS 原生的UIView层级去摆放标题、图标、CTA 按钮和媒体视图。AppLovin MAX 在 iOS 侧提供的是MANativeAdView和MAAdView这类 Objective-C 类Unity 侧只能通过DllImport声明外部函数再由.mm文件里的 C 函数转发给 Objective-C 对象。这条链路一旦断掉表现就是「广告加载成功但界面不显示」或者「点击无响应」。常见做法是建一个NativeAdBridge.mm里面用extern C导出几个函数初始化、加载、展示、销毁。Unity 侧用[DllImport(__Internal)]声明同名函数iOS 平台才编译其他平台用空实现兜底。这样做的代价是参数传递只能走基础类型和字符串复杂对象要靠句柄int 或 IntPtr来映射。我一般会在 C# 侧维护一个Dictionaryint, NativeAdWrapper把原生返回的指针存起来避免每次调用都重新创建。2.2 桥接层的三个关键对象第一个是MAAdView负责 Banner 和 MREC 这类固定尺寸的广告位它本身继承自UIView可以直接AddSubview到 Unity 的视图上。第二个是MANativeAdView这是原生广告的容器里面包含titleLabel、bodyLabel、callToActionButton、iconImageView、mediaView等子视图你需要把这些子视图的引用交给 MAX SDK它才会往里面填素材。第三个是MAAdDelegate和MANativeAdDelegate前者管加载成功/失败后者管原生广告的点击、展示和销毁。很多新手只实现了didLoadAd忘了didFailToLoadAdWithError结果线上加载失败时没有任何日志只能靠猜。提示MANativeAdView的子视图属性名在不同 MAX SDK 版本里有过调整接入前先确认你用的版本对应的头文件别直接抄旧博客。2.3 视图层级与 Unity 渲染的冲突点Unity iOS 默认使用 Metal 渲染游戏画面在一个UnityView上而原生广告视图是 UIKit 的UIView。如果你直接把MAAdView加到UnityView上它会盖在游戏画面之上但触摸事件可能被 Unity 的输入系统拦截。解决办法是把广告视图加到UnityViewController.view上并且设置userInteractionEnabled YES同时确保 Unity 侧没有全屏的GraphicRaycaster挡住。另一个坑是屏幕旋转Unity 的Screen.orientation和 iOS 的UIInterfaceOrientation不一定同步原生广告视图的frame需要根据安全区域和实际方向重新计算否则会出现广告跑到屏幕外或者被刘海遮住。3. 从零接入的完整步骤配置、桥接与原生视图绑定3.1 环境准备与依赖导入先确认三件事Unity 版本建议 2021 LTS 及以上、Xcode 版本与 MAX SDK 要求匹配、AppLovin MAX Unity 插件版本。把 MAX 的 Unity 包导入后在Assets/AppLovin/下会有Plugins/iOS目录里面是预编译的.framework或.xcframework。如果你要自己写桥接就在同级建一个NativeAdBridge.mm。同时检查Info.plistMAX 要求加NSUserTrackingUsageDescriptionATT 弹窗文案和SKAdNetworkItems数组缺了会导致广告填充率极低。!-- Info.plist 片段ATT 与 SKAdNetwork -- keyNSUserTrackingUsageDescription/key string我们需要您的授权以提供更相关的广告体验/string keySKAdNetworkItems/key array dict keySKAdNetworkIdentifier/key stringxxxxx.skadnetwork/string /dict /array上面这段只是结构示例SKAdNetworkIdentifier的具体值要从 MAX 后台或官方文档复制漏一个都可能影响归因。ATT 文案不要写「为了赚钱」这种审核会拒。导入依赖后在 Unity 的Player Settings里把Target minimum iOS Version设到 MAX 要求的最低版本通常不低于 12.0。3.2 C# 侧封装声明、回调与生命周期C# 侧的核心是声明外部函数和注册回调。下面是一个精简的封装示例重点看DllImport的命名和回调签名。using System; using System.Runtime.InteropServices; using UnityEngine; public class NativeAdBridge : MonoBehaviour { // iOS 平台才导入其他平台用空实现 #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _InitNativeAd(string adUnitId); [DllImport(__Internal)] private static extern void _LoadNativeAd(string adUnitId); [DllImport(__Internal)] private static extern void _ShowNativeAd(string adUnitId, float x, float y, float w, float h); [DllImport(__Internal)] private static extern void _DestroyNativeAd(string adUnitId); #else private static void _InitNativeAd(string id) { } private static void _LoadNativeAd(string id) { } private static void _ShowNativeAd(string id, float x, float y, float w, float h) { } private static void _DestroyNativeAd(string id) { } #endif // 原生回调到 Unity 的入口名字要和 .mm 里导出的函数一致 public void OnNativeAdLoaded(string adUnitId) { Debug.Log($Native ad loaded: {adUnitId}); // 加载成功后按预设区域展示 _ShowNativeAd(adUnitId, 0, 0, Screen.width, 120); } public void OnNativeAdFailed(string adUnitId, string error) { Debug.LogError($Native ad failed: {adUnitId}, {error}); } public void OnNativeAdClicked(string adUnitId) { Debug.Log($Native ad clicked: {adUnitId}); } void Start() { _InitNativeAd(你的广告位ID); _LoadNativeAd(你的广告位ID); } void OnDestroy() { _DestroyNativeAd(你的广告位ID); } }这段代码里_ShowNativeAd的四个 float 参数是屏幕坐标和尺寸单位是点point不是像素。OnNativeAdLoaded等方法会被.mm里的UnitySendMessage调用所以挂载这个脚本的 GameObject 名字必须和原生侧写死的一致否则回调收不到。我一般会把广告位 ID 做成常量避免多处硬编码。3.3 Objective-C 桥接加载、展示与回调转发.mm文件里要做三件事实现导出的 C 函数、创建MAAdView或MANativeAdView、把 delegate 回调转发回 Unity。下面是一个 Banner 型原生广告的骨架。// NativeAdBridge.mm #import AppLovinSDK/AppLovinSDK.h #import UnityFramework/UnityFramework-Swift.h // 视 Unity 版本调整 static NSMutableDictionaryNSString *, MAAdView * *_adViews; extern C { void _InitNativeAd(const char *adUnitId) { NSString *unitId [NSString stringWithUTF8String:adUnitId]; if (!_adViews) _adViews [NSMutableDictionary new]; // 初始化 MAX SDK通常只需一次 [ALSdk shared].mediationProvider ALMediationProviderMAX; [[ALSdk shared] initializeSdkWithCompletionHandler:^(ALSdkConfiguration *config) { NSLog(MAX init done); }]; } void _LoadNativeAd(const char *adUnitId) { NSString *unitId [NSString stringWithUTF8String:adUnitId]; MAAdView *adView [[MAAdView alloc] initWithAdUnitIdentifier:unitId]; adView.delegate (idMAAdViewAdDelegate)[NativeAdDelegate shared]; _adViews[unitId] adView; [adView loadAd]; } void _ShowNativeAd(const char *adUnitId, float x, float y, float w, float h) { NSString *unitId [NSString stringWithUTF8String:adUnitId]; MAAdView *adView _adViews[unitId]; if (!adView) return; dispatch_async(dispatch_get_main_queue(), ^{ UIViewController *root UnityGetGLViewController(); adView.frame CGRectMake(x, y, w, h); [root.view addSubview:adView]; }); } void _DestroyNativeAd(const char *adUnitId) { NSString *unitId [NSString stringWithUTF8String:adUnitId]; MAAdView *adView _adViews[unitId]; [adView removeFromSuperview]; [_adViews removeObjectForKey:unitId]; } }UnityGetGLViewController()是 Unity 提供的函数拿到根视图控制器。所有 UI 操作必须回主线程否则会崩。delegate 里收到didLoadAd后用UnitySendMessage(NativeAdBridge, OnNativeAdLoaded:, [unitId UTF8String])通知 C# 侧。注意UnitySendMessage的第二个参数是方法名第三个是字符串参数多个参数要自己拼接解析。3.4 原生视图绑定与尺寸适配如果你用的是MANativeAdView而不是MAAdView需要自己创建子视图并赋值。常见做法是在.mm里用代码创建UILabel、UIImageView、UIButton然后设置nativeAdView.titleLabel titleLabel等。尺寸适配要结合safeAreaInsets在viewDidLayoutSubviews里更新 frame。Unity 侧传入的坐标建议用相对坐标0~1原生侧再乘以实际宽高这样旋转屏幕时不用重新计算。注意MANativeAdView的mediaView必须设置否则视频类素材无法播放表现为广告区域空白但日志显示加载成功。4. 避坑与排查iOS 原生广告接入的五个血泪经验4.1 广告加载成功但界面不显示现象日志里didLoadAd已经触发但屏幕上什么都没有。原因通常是视图没有加到正确的父视图上或者frame是CGRectZero。解决在_ShowNativeAd里打印adView.frame和root.view的层级确认addSubview执行了。另外检查adView.hidden是否为YES有些 SDK 默认隐藏需要手动设NO。4.2 真机崩溃在UnityGetGLViewController现象Xcode 报EXC_BAD_ACCESS堆栈指向UnityGetGLViewController。原因是调用时机太早Unity 的 GL 视图还没创建。解决不要在_InitNativeAd里直接拿 ViewController延迟到_ShowNativeAd再取并且用dispatch_async切主线程。如果还是崩检查 Unity 版本对应的头文件是否引入正确。4.3 ATT 弹窗不出现导致填充率低现象广告请求返回no fill后台看到大量无归因流量。原因是 iOS 14.5 必须弹 ATT 授权且弹窗只能在 App 活跃时触发。解决在_InitNativeAd里调用[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:]并且确保Info.plist里有NSUserTrackingUsageDescription。弹窗时机建议放在游戏加载完成后不要一启动就弹否则容易被拒。4.4 回调方法名拼写不一致现象原生侧调了UnitySendMessage但 C# 侧没反应。原因是 GameObject 名字或方法名大小写不一致或者方法不是public。解决把 GameObject 名字做成常量C# 方法名和原生侧字符串逐字比对。UnitySendMessage不支持重载参数只能传一个字符串多个值用分隔符拼接。4.5 屏幕旋转后广告错位现象横屏游戏切到竖屏广告跑到屏幕外。原因是原生视图的autoresizingMask没设或者 Unity 的Screen.orientation和 iOS 方向不同步。解决在viewDidLayoutSubviews里根据safeAreaInsets重算 frame并监听UIDeviceOrientationDidChangeNotification。如果游戏锁定方向就在Player Settings里固定避免动态切换。5. 进阶技巧用自动化脚本验证桥接层与填充率接完广告只是第一步真正影响收入的是填充率和展示率。我一般会写一个简单的 Editor 脚本在打包前检查Info.plist里SKAdNetworkItems的数量和NSUserTrackingUsageDescription是否存在缺了就报错。下面这个脚本放在Assets/Editor/下打包 iOS 时自动跑。using UnityEditor; using UnityEditor.Callbacks; using System.IO; using System.Text.RegularExpressions; public class NativeAdBuildCheck { [PostProcessBuild(1)] public static void OnPostProcessBuild(BuildTarget target, string path) { if (target ! BuildTarget.iOS) return; string plistPath Path.Combine(path, Info.plist); string content File.ReadAllText(plistPath); // 检查 ATT 文案 if (!content.Contains(NSUserTrackingUsageDescription)) { UnityEngine.Debug.LogError(Info.plist 缺少 NSUserTrackingUsageDescriptionATT 弹窗不会出现); } // 统计 SKAdNetworkItems 数量 int count Regex.Matches(content, SKAdNetworkIdentifier).Count; if (count 50) { UnityEngine.Debug.LogWarning($SKAdNetworkItems 只有 {count} 个建议补全以提高归因覆盖率); } } }这个脚本的逻辑很简单打包完成后读Info.plist用正则数SKAdNetworkIdentifier出现次数。数量少于 50 就警告因为主流广告平台加起来通常有上百个。ATT 文案缺失直接报错避免上线后才发现。参数上PostProcessBuild的优先级设为 1确保在其他处理之后执行。如果你用 CI 打包可以把LogError改成抛异常让构建失败。另一个技巧是给原生广告加一个「占位视图」在加载成功前显示一个半透明背景避免布局跳动。加载失败时隐藏占位同时上报埋点。这样既能提升体验也方便排查是加载问题还是展示问题。从那以后我每次接新广告位都强制走一遍「Editor 检查 → 真机日志 → 填充率对比」的流程再也没出现过上线才发现配置缺失的情况。希望帮到你。本文还有配套的精品资源点击获取