一、准备与目录结构
- 环境
- Unity 安装 iOS Build Support(含 IL2CPP)
- Xcode + CocoaPods(gem install cocoapods)
- Apple 开发者账号、iOS Team 权限、Provisioning Profile
- Unity 工程建议目录
- Assets/
二、调用链路示意
下面的时序图展示了 C# -> OC -> 广告SDK -> 回调C# 全流程(仅示意,方法名与实现见下文样例)。
三、iOS 原生桥接(OC)代码
放到 Assets/Plugins/iOS 下,Unity 构建时会自动合入 Xcode 工程。
Q1AdsBridge.h
// Q1AdsBridge.h
#import <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
// 初始化广告SDK,传入用于回调的 Unity GameObject 名称
void Q1Ads_Init(const char* appId, const char* appKey, const char* unityGameObjectName);
// 预加载激励视频
void Q1Ads_LoadReward(const char* placementId);
// 是否就绪
bool Q1Ads_IsRewardReady(const char* placementId);
// 展示激励视频
void Q1Ads_ShowReward(const char* placementId);
// iOS14+ 请求跟踪权限(ATT)
void Q1Ads_RequestATT();
#ifdef __cplusplus
}
#endif
Q1AdsBridge.mm
// Q1AdsBridge.mm
#import "Q1AdsBridge.h"
#import <UIKit/UIKit.h>
#import <AdSupport/ASIdentifierManager.h>
#import <AppTrackingTransparency/ATTrackingManager.h>
// 若第三方SDK为 Swift,需要导入对应的 umbrella header 或 @import <Module>;
// #import <YourAdSDK/YourAdSDK.h>
static NSString* gUnityReceiver = @"";
static UIViewController* RootVC() {
return [UIApplication sharedApplication].keyWindow.rootViewController ?: [UIApplication sharedApplication].delegate.window.rootViewController;
}
// Unity 回调
static void UMsg(const char* method, NSDictionary* payload) {
if (gUnityReceiver.length == 0) return;
NSData* data = [NSJSONSerialization dataWithJSONObject:payload ?: @{} options:0 error:nil];
NSString* json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
UnitySendMessage([gUnityReceiver UTF8String], method, [json UTF8String]);
}
void Q1Ads_Init(const char* appId, const char* appKey, const char* unityGameObjectName) {
gUnityReceiver = [NSString stringWithUTF8String:unityGameObjectName ?: ""];
NSString* aid = appId ? [NSString stringWithUTF8String:appId] : @"";
NSString* akey = appKey ? [NSString stringWithUTF8String:appKey] : @"";
// TODO: 替换为真实广告SDK初始化
// [YourAdSDK startWithAppId:aid appKey:akey completion:^(BOOL success, NSError* err){ ... }];
dispatch_async(dispatch_get_main_queue(), ^{
UMsg("OnInit", @{@"ok": @YES, @"appId": aid ?: @""});
});
}
void Q1Ads_LoadReward(const char* placementId) {
NSString* pid = placementId ? [NSString stringWithUTF8String:placementId] : @"default";
// TODO: 调用实际加载API
// [YourAdSDK loadRewardForPlacement:pid completion:^(BOOL ready, NSError* err){ ... }];
dispatch_async(dispatch_get_main_queue(), ^{
// 模拟加载成功
UMsg("OnRewardLoaded", @{@"placementId": pid, @"ready": @YES});
});
}
bool Q1Ads_IsRewardReady(const char* placementId) {
NSString* pid = placementId ? [NSString stringWithUTF8String:placementId] : @"default";
// TODO: return [YourAdSDK isRewardReady:pid];
return true;
}
void Q1Ads_ShowReward(const char* placementId) {
NSString* pid = placementId ? [NSString stringWithUTF8String:placementId] : @"default";
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController* vc = RootVC();
// TODO: 替换为实际展示代码
// [YourAdSDK presentRewardFromViewController:vc placement:pid
// onOpen:^{ UMsg("OnRewardOpened", @{@"placementId": pid}); }
// onEarn:^{ UMsg("OnRewardEarned", @{@"placementId": pid}); }
// onClose:^{ UMsg("OnRewardClosed", @{@"placementId": pid}); }
// onError:^(NSError* e){ UMsg("OnRewardFailed", @{@"placementId": pid, @"code": @(e.code), @"msg": e.localizedDescription ?: @""}); }];
// 示例:模拟回调链路
UMsg("OnRewardOpened", @{@"placementId": pid});
UMsg("OnRewardEarned", @{@"placementId": pid, @"reward": @1});
UMsg("OnRewardClosed", @{@"placementId": pid});
});
}
void Q1Ads_RequestATT() {
if (@available(iOS 14, *)) {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
UMsg("OnATT", @{@"status": @(status)});
}];
} else {
UMsg("OnATT", @{@"status": @3}); // authorized by default pre-iOS14
}
}
四、Unity C# 封装与回调
Q1Ads.cs(C# 调用封装)
// Assets/Scripts/Q1Ads.cs
using System.Runtime.InteropServices;
using UnityEngine;
public static class Q1Ads
{
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")] private static extern void Q1Ads_Init(string appId, string appKey, string goName);
[DllImport("__Internal")] private static extern void Q1Ads_LoadReward(string placementId);
[DllImport("__Internal")] private static extern bool Q1Ads_IsRewardReady(string placementId);
[DllImport("__Internal")] private static extern void Q1Ads_ShowReward(string placementId);
[DllImport("__Internal")] private static extern void Q1Ads_RequestATT();
#else
private static void Q1Ads_Init(string a, string b, string c) { }
private static void Q1Ads_LoadReward(string a) { }
private static bool Q1Ads_IsRewardReady(string a) => true;
private static void Q1Ads_ShowReward(string a) { }
private static void Q1Ads_RequestATT() { }
#endif
private const string Receiver = "Q1AdsListener";
public static void Initialize(string appId, string appKey)
{
EnsureListener();
Q1Ads_Init(appId, appKey, Receiver);
}
public static void LoadReward(string placementId) => Q1Ads_LoadReward(placementId);
public static bool IsRewardReady(string placementId) => Q1Ads_IsRewardReady(placementId);
public static void ShowReward(string placementId) => Q1Ads_ShowReward(placementId);
public static void RequestATT() => Q1Ads_RequestATT();
private static void EnsureListener()
{
if (GameObject.Find(Receiver) == null)
{
var go = new GameObject(Receiver);
go.AddComponent<Q1AdsListener>();
Object.DontDestroyOnLoad(go);
}
}
}
Q1AdsListener.cs(接收 UnitySendMessage 回调)
// Assets/Scripts/Q1AdsListener.cs
using UnityEngine;
using System;
public class Q1AdsListener : MonoBehaviour
{
// 你可以在这里再分发事件给业务层
public static event Action<string> OnInitEvent;
public static event Action<string> OnRewardLoadedEvent;
public static event Action<string> OnRewardOpenedEvent;
public static event Action<string> OnRewardEarnedEvent;
public static event Action<string> OnRewardClosedEvent;
public static event Action<string> OnRewardFailedEvent;
public static event Action<string> OnATTEvent;
// 下列方法名需与 OC 侧 UnitySendMessage 的 method 保持一致
public void OnInit(string json) => OnInitEvent?.Invoke(json);
public void OnRewardLoaded(string json) => OnRewardLoadedEvent?.Invoke(json);
public void OnRewardOpened(string json) => OnRewardOpenedEvent?.Invoke(json);
public void OnRewardEarned(string json) => OnRewardEarnedEvent?.Invoke(json);
public void OnRewardClosed(string json) => OnRewardClosedEvent?.Invoke(json);
public void OnRewardFailed(string json) => OnRewardFailedEvent?.Invoke(json);
public void OnATT(string json) => OnATTEvent?.Invoke(json);
}
link.xml(防止裁剪)
<!-- Assets/link.xml -->
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="Q1AdsListener" preserve="all"/>
</assembly>
</linker>
五、CocoaPods 与 Xcode 自动化配置
推荐用 PostProcessBuild 自动注入 -ObjC、Info.plist 权限文案、SKAdNetwork IDs、Embedding Swift 等,避免每次手改。
Q1AdsPostBuild.cs
// Assets/Plugins/iOS/Q1AdsPostBuild.cs
#if UNITY_IOS
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;
public static class Q1AdsPostBuild
{
[PostProcessBuild(999)]
public static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
{
if (target != BuildTarget.iOS) return;
var projPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
var proj = new PBXProject();
proj.ReadFromFile(projPath);
#if UNITY_2019_3_OR_NEWER
string mainTarget = proj.GetUnityMainTargetGuid();
string frameworkTarget = proj.GetUnityFrameworkTargetGuid();
#else
string mainTarget = proj.TargetGuidByName("Unity-iPhone");
string frameworkTarget = mainTarget;
#endif
// Linker flags
proj.AddBuildProperty(frameworkTarget, "OTHER_LDFLAGS", "-ObjC");
// Swift 支持(如第三方SDK是 Swift)
proj.SetBuildProperty(frameworkTarget, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
// 添加系统 Framework(按需)
proj.AddFrameworkToProject(frameworkTarget, "AdSupport.framework", false);
proj.AddFrameworkToProject(frameworkTarget, "StoreKit.framework", false);
proj.AddFrameworkToProject(frameworkTarget, "AppTrackingTransparency.framework", false);
proj.WriteToFile(projPath);
// 修改 Info.plist
var plistPath = Path.Combine(pathToBuiltProject, "Info.plist");
var plist = new PlistDocument();
plist.ReadFromFile(plistPath);
var root = plist.root;
root.SetString("NSUserTrackingUsageDescription", "为提供更相关的广告与统计分析,我们需要使用您的设备标识。");
// ATS(如需 HTTP,建议改为 HTTPS)
// var ats = root.CreateDict("NSAppTransportSecurity");
// ats.SetBoolean("NSAllowsArbitraryLoads", true);
// SKAdNetwork IDs(按你的广告平台维护清单)
var skArray = root.CreateArray("SKAdNetworkItems");
// 示例,占位,替换为真实ID
var item = skArray.AddDict();
item.SetString("SKAdNetworkIdentifier", "cstr6suwn9.skadnetwork");
plist.WriteToFile(plistPath);
// 生成/更新 Podfile(如需)
var podfilePath = Path.Combine(pathToBuiltProject, "Podfile");
if (!File.Exists(podfilePath))
{
File.WriteAllText(podfilePath,
@"platform :ios, '12.0'
use_frameworks!
target 'Unity-iPhone' do
# 示例:替换为你的广告SDK
# pod 'Google-Mobile-Ads-SDK'
# pod 'YourAdSDK'
end
");
}
}
}
#endif
提示
- 如果你的广告 SDK 必须使用 Pods,构建后在 Xcode 工程目录执行 pod install,然后用 .xcworkspace 打开工程或在 CI 中做。
- 如果 SDK 使用 Swift,确保 Always Embed Swift Standard Libraries = YES。
- 如果需要额外 Capabilities(比如 In-App Purchase、Push),在 Xcode 的 Signing & Capabilities 中添加,或用 PBXProject/ProjectCapabilityManager 注入。
六、请求 ATT 与 IDFA 获取
- OC 已提供 Q1Ads_RequestATT,可在开屏时调用:
Q1Ads.RequestATT();
- 如果需要广告标识符,iOS 14+ 在授权为 Authorized 时再调用:
七、Info.plist 合规清单(最少)
- NSUserTrackingUsageDescription:跟踪说明
- App Transport Security(如需 HTTP):NSAppTransportSecurity/NSAllowsArbitraryLoads
- SKAdNetworkItems:各广告平台的 SKAdNetworkIdentifier 列表
- 隐私权限用途说明(如录屏/麦克风/相册等):NSCameraUsageDescription、NSMicrophoneUsageDescription、NSPhotoLibraryUsageDescription
八、Unity 侧使用范例
// 初始化
Q1Ads.Initialize("your_app_id", "your_app_key");
// 监听回调
Q1AdsListener.OnInitEvent += json => Debug.Log("Init: " + json);
Q1AdsListener.OnRewardLoadedEvent += json => Debug.Log("Loaded: " + json);
Q1AdsListener.OnRewardEarnedEvent += json => Debug.Log("Earned: " + json);
// 加载与展示
Q1Ads.LoadReward("rv_default");
if (Q1Ads.IsRewardReady("rv_default"))
{
Q1Ads.ShowReward("rv_default");
}
九、常见坑与排查
- 链接错误/符号找不到:缺少 -ObjC 或必须的系统 Framework;检查 Pods 是否安装成功。
- OC 方法没有被导出:extern "C" 声明确保 C 符号;.mm/.m 文件需在 Plugins/iOS 下。
- 回调收不到:UnitySendMessage 的 GameObject 名称与方法名必须匹配;类需存在且非裁剪(用 link.xml)。
- 展示崩溃:没有在主线程 present;确保 dispatch 到主线程;RootVC 获取为空时尝试从 window.rootViewController 获取。
- ATT 不弹:iOS <14 或已授权/拒绝;确保 Info.plist 中有 NSUserTrackingUsageDescription。
- 上传审核被拒:SKAdNetworkIDs 不完整、跟踪描述不清、广告 SDK 使用的加密申报未填写。
十、进阶:多广告平台与瀑布兜底
- 在 OC 层抽象统一接口(init/load/isReady/show),内部路由到不同平台(GDT/穿山甲/AdMob/Applovin 等)。
- 在 C# 层只依赖统一 Q1Ads.cs,不感知底层供应商切换。
- 配置用远端参数(例如 JSON 配置)控制优先级与超时,OC 按顺序加载并回调首个可用源。
参考
- iOS 第三方广告SDK接入文档(语雀-内部)https://www.yuque.com/staff-nseb80/it/kxi2zflphsdg677m
- Apple App Tracking Transparency https://developer.apple.com/documentation/apptrackingtransparency
- SKAdNetwork https://developer.apple.com/documentation/storekit/skadnetwork