简介本资源面向使用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。其次是SKAdNetworkItemsMAX 的广告归因需要这个不配置的话某些广告可能不填充。然后是Other Linker Flags里加上-ObjC否则 Objective-C 的分类方法可能不会被链接进去。!-- Info.plist 片段 -- keyNSAppTransportSecurity/key dict keyNSAllowsArbitraryLoads/key true/ /dict keySKAdNetworkItems/key array dict keySKAdNetworkIdentifier/key stringxxxxx.skadnetwork/string /dict /arraySKAdNetworkIdentifier的具体值需要从 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里维护一个Dictionarystring, 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 原生广告时少踩几个坑。本文还有配套的精品资源点击获取