简介:本资源面向使用Unity引擎开发iOS应用的开发者,聚焦AppLovin Max平台Native原生广告的接入实践,帮助解决广告变现流程中SDK集成、广告位配置与事件回调处理等关键问题,适合具备一定Unity与iOS开发基础、希望提升广告收益的中高级开发者参考。资源包共58个文件,约367KB,包含21个png界面素材、15个meta配置文件、8个json配置、3个h头文件,以及nib界面文件、swiftmodule、framework、car资源包、plist与mm实现文件等,覆盖原生广告视图布局、Swift缓冲管理与MaxNativeAdFramework框架等模块,目录结构清晰,便于按需查阅与复用。目前已有106人学习下载。通过该资源,读者可获取Native广告接入所需的框架文件、界面素材与配置示例,理解广告加载、展示与回调的完整链路,并参考广告优化与合规性处理思路,为实际项目接入提供可落地的参考。
1. 从一次真机崩溃说起:Unity iOS 接 AppLovin MAX 原生广告到底难在哪
有个做休闲游戏的朋友跟我吐槽,他的 Unity 项目在 Android 上跑 AppLovin MAX 的插屏和激励视频一切正常,打包到 iOS 真机后,激励视频回调不触发,插屏偶尔黑屏,Xcode 控制台里一堆Ad display failed的日志。他一开始以为是广告平台后台配置问题,折腾了两天,最后发现是 iOS 侧的原生桥接层没接对——Unity 的 C# 调用和 iOS 原生 SDK 之间隔着一层 Objective-C 桥接,这层没写对,广告请求发不出去,回调自然也回不来。
这就是 Unity iOS 接 AppLovin MAX 原生广告的典型痛点:它不像纯 Unity 插件那样开箱即用,很多场景需要你手动写 iOS 原生代码去桥接。这份资源就是围绕这个场景整理的,覆盖了从 SDK 导入、Xcode 工程配置、原生桥接代码编写到真机调试的完整链路。如果你正在做 Unity iOS 项目的广告变现,尤其是需要接入 MAX 的插屏、激励视频、Banner 这类原生广告位,这份资源能帮你少走不少弯路。
2. 工程结构与技术选型:Unity 层、iOS 原生层、MAX SDK 怎么分工
2.1 三层架构的职责划分
这个项目的核心思路是把广告逻辑拆成三层,每层各管各的,避免耦合。
第一层是 Unity C# 层,负责游戏内的广告触发时机、奖励发放、UI 状态切换。比如玩家点击“看视频复活”按钮,C# 层调用一个封装好的广告管理器,传入广告位 ID,然后等待回调。
第二层是 iOS 原生桥接层,用 Objective-C 或 C++ 写,负责把 Unity 的调用翻译成 MAX SDK 能听懂的 Objective-C 方法调用,同时把 MAX 的回调通过UnitySendMessage传回 C# 层。这层是整条链路里最容易翻车的地方,因为涉及到 C# 和 Objective-C 的类型转换、线程调度、内存管理。
第三层是 AppLovin MAX SDK 本身,它负责实际的广告请求、填充、展示和事件回调。MAX 的 iOS SDK 是纯 Objective-C 的,所以桥接层必须用 Objective-C 来写,不能用纯 C# 直接调。
三层之间的关系可以用一个简单的数据流来描述:C# 发起请求 → 桥接层转发 → MAX SDK 请求广告 → 广告填充后回调桥接层 → 桥接层通过UnitySendMessage通知 C# → C# 处理奖励逻辑。
2.2 目录结构说明
资源里的目录结构大致是这样的:
UnityAppLovinMaxDemo/ ├── Assets/ │ ├── Scripts/ │ │ ├── AdManager.cs # C# 广告管理器,单例 │ │ ├── AdCallbackHandler.cs # 回调接收器,挂在场景物体上 │ │ └── AdConfig.cs # 广告位 ID 配置 │ ├── Plugins/ │ │ └── iOS/ │ │ ├── AdBridge.mm # Objective-C++ 桥接实现 │ │ ├── AdBridge.h # 桥接头文件 │ │ └── AppLovinMAX.framework # MAX SDK 二进制 │ └── Scenes/ │ └── Main.unity # 演示场景 ├── ios/ │ └── XcodeProject/ # 导出的 Xcode 工程 └── README.mdAdManager.cs是 C# 侧的入口,所有广告请求都从这里发起。AdBridge.mm是桥接层的核心,里面用extern "C"声明了供 Unity 调用的函数。AdCallbackHandler.cs负责接收原生层通过UnitySendMessage发过来的消息,因为UnitySendMessage只能调用场景中某个 GameObject 上的方法,所以这个脚本必须挂在一个常驻的 GameObject 上。
2.3 技术选型:为什么用 Objective-C++ 而不是纯 Objective-C
桥接层用的是.mm后缀,也就是 Objective-C++。这样做的好处是可以在同一个文件里混写 C++ 和 Objective-C,方便处理 Unity 的 C# 字符串和 Objective-C 的NSString之间的转换。如果只用.m文件,字符串转换会麻烦一些,需要额外写 C 函数来中转。
MAX SDK 的初始化需要在 iOS 原生层完成,因为 SDK 的ALSdk单例必须在应用启动后尽早初始化。常见做法是在AppDelegate的application:didFinishLaunchingWithOptions:里调用初始化方法,或者通过桥接层暴露一个InitMaxSDK函数,在 Unity 的Awake里调用。
提示:MAX SDK 的初始化是异步的,初始化完成前请求广告可能会失败。建议在初始化回调里再触发第一次广告预加载。
3. 核心桥接代码拆解:从 C# 调用到 MAX 回调的完整链路
3.1 C# 侧:广告管理器的封装
先看 C# 侧怎么发起一个激励视频请求。AdManager是一个单例,对外暴露ShowRewardedAd方法,内部通过DllImport调用 iOS 原生函数。
// AdManager.cs using System.Runtime.InteropServices; using UnityEngine; public class AdManager : MonoBehaviour { // 声明 iOS 原生函数,函数名要和 .mm 文件里的 extern "C" 函数一致 [DllImport("__Internal")] private static extern void _ShowRewardedAd(string adUnitId); [DllImport("__Internal")] private static extern void _InitMaxSDK(string sdkKey); private static AdManager _instance; public static AdManager Instance => _instance; void Awake() { if (_instance == null) { _instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } // 初始化 MAX SDK,传入后台申请的 SDK Key public void InitSDK(string sdkKey) { #if UNITY_IOS && !UNITY_EDITOR _InitMaxSDK(sdkKey); #else Debug.Log("非 iOS 平台,跳过 MAX SDK 初始化"); #endif } // 展示激励视频,adUnitId 是 MAX 后台创建的广告位 ID public void ShowRewardedAd(string adUnitId) { #if UNITY_IOS && !UNITY_EDITOR _ShowRewardedAd(adUnitId); #else Debug.Log($"模拟展示激励视频: {adUnitId}"); // 编辑器下直接模拟回调,方便调试业务逻辑 AdCallbackHandler.OnRewardedAdCompleted("simulated_reward"); #endif } }这里有几个关键点。DllImport("__Internal")是 Unity 调用 iOS 原生代码的标准方式,__Internal表示链接到当前可执行文件里的符号。函数名前面的下划线是命名约定,不是强制的,但保持 C# 和原生层一致能减少混淆。#if UNITY_IOS && !UNITY_EDITOR这个条件编译很重要,因为编辑器下没有 iOS 原生环境,直接调用会报DllNotFoundException,所以编辑器里走模拟逻辑。
3.2 原生桥接层:Objective-C++ 实现
接下来是桥接层的核心代码。这个文件负责接收 C# 的调用,转发给 MAX SDK,并把回调传回去。
// AdBridge.mm #import <Foundation/Foundation.h> #import <AppLovinSDK/AppLovinSDK.h> // Unity 提供的回调函数声明,用于向 C# 发送消息 extern void UnitySendMessage(const char* obj, const char* method, const char* msg); @interface AdBridge : NSObject <MARewardedAdDelegate, MAAdDelegate> @property (nonatomic, strong) MARewardedAd *rewardedAd; @property (nonatomic, strong) MAInterstitialAd *interstitialAd; @property (nonatomic, copy) NSString *rewardedAdUnitId; @end @implementation AdBridge + (instancetype)sharedInstance { static AdBridge *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[AdBridge alloc] init]; }); return instance; } // 初始化 MAX SDK - (void)initSDKWithKey:(NSString *)sdkKey { ALSdkConfiguration *configuration = [ALSdk shared].configuration; [ALSdk shared].mediationProvider = ALMediationProviderMAX; [[ALSdk shared] initializeWithConfiguration:configuration completionHandler:^(ALSdkConfiguration *config) { // 初始化完成后通知 C# 层 UnitySendMessage("AdCallbackHandler", "OnSDKInitialized", ""); }]; } // 加载并展示激励视频 - (void)showRewardedAdWithAdUnitId:(NSString *)adUnitId { self.rewardedAdUnitId = adUnitId; if (!self.rewardedAd) { self.rewardedAd = [MARewardedAd sharedWithAdUnitIdentifier:adUnitId]; self.rewardedAd.delegate = self; } [self.rewardedAd loadAd]; } #pragma mark - MARewardedAdDelegate // 广告加载成功,立即展示 - (void)didLoadAd:(MAAd *)ad { [self.rewardedAd showAd]; } // 广告展示成功 - (void)didDisplayAd:(MAAd *)ad { UnitySendMessage("AdCallbackHandler", "OnRewardedAdDisplayed", ""); } // 用户看完广告,发放奖励 - (void)didRewardUserForAd:(MAAd *)ad withReward:(MAReward *)reward { NSString *rewardStr = [NSString stringWithFormat:@"%ld", (long)reward.amount]; UnitySendMessage("AdCallbackHandler", "OnRewardedAdCompleted", [rewardStr UTF8String]); } // 广告加载或展示失败 - (void)didFailToLoadAdForAdUnitIdentifier:(NSString *)adUnitIdentifier withError:(MAError *)error { NSString *errMsg = [NSString stringWithFormat:@"%@", error.message]; UnitySendMessage("AdCallbackHandler", "OnAdLoadFailed", [errMsg UTF8String]); } @end // 暴露给 Unity 的 C 函数 extern "C" { void _InitMaxSDK(const char *sdkKey) { NSString *key = [NSString stringWithUTF8String:sdkKey]; [[AdBridge sharedInstance] initSDKWithKey:key]; } void _ShowRewardedAd(const char *adUnitId) { NSString *unitId = [NSString stringWithUTF8String:adUnitId]; [[AdBridge sharedInstance] showRewardedAdWithAdUnitId:unitId]; } }这段代码里有几个容易踩坑的地方。UnitySendMessage的第一个参数是场景中 GameObject 的名字,第二个参数是挂在该 GameObject 上的脚本方法名,第三个参数是字符串消息。如果 GameObject 名字写错或者方法名拼错,回调就会静默失败,不会有任何报错。MARewardedAd的loadAd和showAd是分开的,加载成功后要在didLoadAd回调里手动调用showAd,否则广告不会展示。
3.3 回调接收层:C# 侧怎么接住原生消息
原生层通过UnitySendMessage发过来的消息,需要一个 C# 脚本来接收。这个脚本必须挂在一个名字和UnitySendMessage第一个参数一致的 GameObject 上。
// AdCallbackHandler.cs using UnityEngine; public class AdCallbackHandler : MonoBehaviour { // 这些方法会被 iOS 原生层的 UnitySendMessage 调用 public void OnSDKInitialized(string msg) { Debug.Log("MAX SDK 初始化完成"); // 初始化完成后可以预加载广告 AdManager.Instance.ShowRewardedAd("your_rewarded_ad_unit_id"); } public void OnRewardedAdDisplayed(string msg) { Debug.Log("激励视频开始展示"); // 可以在这里暂停游戏音效或逻辑 } public void OnRewardedAdCompleted(string rewardAmount) { Debug.Log($"激励视频完成,奖励数量: {rewardAmount}"); // 发放奖励给玩家 int amount = int.Parse(rewardAmount); GameManager.Instance.GrantReward(amount); } public void OnAdLoadFailed(string errorMsg) { Debug.LogError($"广告加载失败: {errorMsg}"); // 可以在这里做重试或降级处理 } }这个脚本的关键在于方法名必须和原生层UnitySendMessage的第二个参数完全一致,包括大小写。UnitySendMessage是大小写敏感的,OnRewardedAdCompleted和onRewardedAdCompleted是两个不同的方法。另外,这个脚本挂载的 GameObject 名字要和UnitySendMessage的第一个参数一致,通常做法是创建一个名为AdCallbackHandler的空物体,把这个脚本挂上去,并在Awake里DontDestroyOnLoad。
3.4 Xcode 工程配置要点
Unity 导出 iOS 工程后,还需要在 Xcode 里做一些配置,否则 MAX SDK 可能无法正常工作。
首先是Info.plist里要添加NSAppTransportSecurity允许 HTTP 请求,因为部分广告素材可能走 HTTP。其次是SKAdNetworkItems,MAX 的广告归因需要这个,不配置的话某些广告可能不填充。然后是Other Linker Flags里加上-ObjC,否则 Objective-C 的分类方法可能不会被链接进去。
<!-- Info.plist 片段 --> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> <key>SKAdNetworkItems</key> <array> <dict> <key>SKAdNetworkIdentifier</key> <string>xxxxx.skadnetwork</string> </dict> </array>SKAdNetworkIdentifier的具体值需要从 MAX 后台或官方文档获取,每个广告平台都有自己的标识。这些配置看起来琐碎,但少一个就可能导致广告不填充或者归因失败。
4. 避坑与排查:iOS 原生广告接入的 5 个血泪教训
4.1 回调不触发,控制台没有任何报错
现象:C# 调用了_ShowRewardedAd,但OnRewardedAdCompleted一直不执行,Xcode 控制台也没有明显错误。
原因:最常见的是UnitySendMessage的 GameObject 名字或方法名写错了。UnitySendMessage找不到目标时会静默失败,不会抛异常。另一个可能是 GameObject 在场景切换时被销毁了,导致回调发不到。
解决:确认AdCallbackHandler挂载的 GameObject 名字和原生层UnitySendMessage第一个参数完全一致,并且这个 GameObject 在Awake里调用了DontDestroyOnLoad。可以在原生层加日志确认UnitySendMessage确实被调用了。
4.2 真机上广告加载失败,编辑器里正常
现象:Unity 编辑器里模拟逻辑跑得好好的,打包到真机后didFailToLoadAd频繁触发。
原因:MAX SDK 的初始化是异步的,如果在初始化完成前就请求广告,会直接失败。另外,真机上的网络环境和编辑器不同,某些广告平台可能因为地区或网络原因不填充。
解决:把广告请求放在OnSDKInitialized回调之后。可以在初始化回调里加一个标志位,只有标志位为 true 时才允许请求广告。同时检查 MAX 后台的广告位配置是否正确,测试模式是否开启。
4.3 Xcode 编译报错Undefined symbols for architecture arm64
现象:Unity 导出 Xcode 工程后编译失败,报找不到_ShowRewardedAd等符号。
原因:桥接层的.mm文件没有被添加到 Xcode 工程的编译源里,或者Other Linker Flags缺少-ObjC。
解决:检查 Xcode 工程的Build Phases→Compile Sources里是否包含了AdBridge.mm。如果没有,手动添加。然后在Build Settings→Other Linker Flags里加上-ObjC。
4.4 激励视频看完没有奖励
现象:广告正常播放,但didRewardUserForAd没有触发,或者触发了但 C# 侧没收到。
原因:MAX 的激励视频有两种关闭方式,一种是用户看完获得奖励,一种是用户中途关闭。只有didRewardUserForAd才是发放奖励的回调。另外,如果MARewardedAd的 delegate 没有正确设置,回调也不会触发。
解决:确认self.rewardedAd.delegate = self在请求广告前已经设置。在didRewardUserForAd里加日志确认回调触发。C# 侧检查OnRewardedAdCompleted的方法名和参数类型是否匹配。
4.5 插屏广告展示后游戏卡死
现象:插屏广告关闭后,游戏画面卡住,点击无响应。
原因:插屏广告展示时可能会暂停 Unity 的渲染循环,如果广告关闭后没有正确恢复,就会卡死。另一个可能是广告展示在主线程之外,导致 UI 更新异常。
解决:确保所有 MAX SDK 的调用都在主线程执行。可以在桥接层用dispatch_async(dispatch_get_main_queue(), ^{ ... })包裹广告展示逻辑。插屏关闭后,检查是否有OnApplicationPause相关的逻辑没有正确恢复。
注意:MAX SDK 的某些回调可能在非主线程触发,涉及 UI 操作时一定要切回主线程。
5. 二次开发与改造:从能跑到好用的几个关键技巧
5.1 把广告位 ID 抽成配置表
资源里的广告位 ID 是硬编码在 C# 里的,实际项目里肯定不能这么干。我一般会做一个AdConfig的 ScriptableObject,把不同广告位的 ID 按平台和场景分类存起来。
// AdConfig.cs using UnityEngine; [CreateAssetMenu(fileName = "AdConfig", menuName = "Ad/AdConfig")] public class AdConfig : ScriptableObject { [Header("iOS 广告位")] public string iosRewardedAdUnitId; public string iosInterstitialAdUnitId; public string iosBannerAdUnitId; [Header("Android 广告位")] public string androidRewardedAdUnitId; public string androidInterstitialAdUnitId; public string androidBannerAdUnitId; // 根据当前平台返回对应的广告位 ID public string GetRewardedAdUnitId() { #if UNITY_IOS return iosRewardedAdUnitId; #elif UNITY_ANDROID return androidRewardedAdUnitId; #else return "editor_test_id"; #endif } }这样做的好处是换广告位不用改代码,运营同学也能在 Inspector 里直接改。配合 Unity 的Resources.Load或者 Addressables,还能做到不同渠道包用不同的广告位配置。
5.2 加一层广告加载状态机
直接调ShowRewardedAd有个问题:如果广告还没加载好,用户点了按钮,要么没反应,要么报错。我习惯在 C# 侧加一个简单的状态机,管理每个广告位的加载状态。
public enum AdState { Idle, Loading, Ready, Showing } public class AdSlot { public string adUnitId; public AdState state = AdState.Idle; public Action onReady; public Action onFailed; public bool CanShow => state == AdState.Ready; }AdManager里维护一个Dictionary<string, AdSlot>,每次请求广告前检查状态。如果状态是Idle,先触发加载,加载完成后把状态置为Ready,并执行onReady回调。如果状态是Ready,直接展示。展示时把状态置为Showing,展示结束后重置为Idle并自动预加载下一条。
这个状态机不复杂,但能避免很多“广告没加载好就点”的尴尬情况。尤其是激励视频,用户点了按钮没反应,体验很差。
5.3 用条件编译隔离平台差异
iOS 和 Android 的广告接入方式完全不同,但业务逻辑层不应该关心这些差异。我的做法是在AdManager里用条件编译把平台相关的调用隔离掉,对外只暴露统一的接口。
public void ShowRewardedAd() { string adUnitId = AdConfig.GetRewardedAdUnitId(); #if UNITY_IOS && !UNITY_EDITOR _ShowRewardedAd(adUnitId); #elif UNITY_ANDROID && !UNITY_EDITOR // Android 侧调用 MAX 的 Android 桥接 AndroidAdBridge.ShowRewardedAd(adUnitId); #else // 编辑器下模拟 SimulateRewardedAd(); #endif }这样业务层调AdManager.Instance.ShowRewardedAd()就行,不用关心底层是 iOS 还是 Android。新增平台时只需要加一个分支,不影响已有逻辑。
5.4 真机调试的几个实用技巧
iOS 原生广告调试最头疼的是看不到实时日志。我的习惯是在桥接层的关键节点都加上NSLog,然后在 Xcode 的Devices and Simulators里看设备日志。另外,MAX 后台有测试模式,开启后可以强制填充测试广告,方便验证链路是否通畅。
还有一个技巧是用 Safari 的 Web Inspector 调试 WKWebView 里的广告内容,虽然 MAX 的广告大多是原生渲染,但某些素材还是走 WebView 的。这个在排查素材加载失败时很有用。
从那以后我每次接新的广告平台,都会先把桥接层的日志打全,确认 C# 到原生、原生到 SDK、SDK 回调到原生、原生回调到 C# 这四段链路都通了,再往上叠业务逻辑。这样出问题时能快速定位是哪一段断了,不用靠猜。希望这份资源能帮你在 Unity iOS 接 MAX 原生广告时少踩几个坑。
本文还有配套的精品资源,点击获取