Unity iOS打包,SDK接入,OC代码调用


一、准备与目录结构

  1. 环境
  • Unity 安装 iOS Build Support(含 IL2CPP)
  • Xcode + CocoaPods(gem install cocoapods)
  • Apple 开发者账号、iOS Team 权限、Provisioning Profile
  1. Unity 工程建议目录
  • Assets/

二、调用链路示意
下面的时序图展示了 C# -> OC -> 广告SDK -> 回调C# 全流程(仅示意,方法名与实现见下文样例)。

image.png




三、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




-

Unity 自用帧同步架构分享

评 论