资讯动态

Unity项目接入APNs推送:从证书到原生桥接的完整指南

发布时间:2026/10/17 16:45:44 来源:尧图企业网站定制
前阵子有个开发者来找我说他们在Unity里做的社交App要接iOS推送问APNs到底怎么整。我第一反应是又是一个被Unity跨平台外衣坑了的兄弟。Unity对安卓推送确实有现成封装但iOS的远程推送从来不是填个参数就能跑的它必须碰原生层要走一套苹果自己的通知机制也就是APNs。这篇文章我把自己踩过的坑、验证过的套路全部整理出来。不管你是Unity新手还是老手只要想在iOS上接推送按这篇的步骤走一遍至少能少折腾三天。我会从证书配置讲到原生桥接再讲Unity侧怎么接收、怎么处理冷启动最后给一份我实测过的排错清单。全程不玩虚的全是能直接抄作业的东西。1. 接入APNs之前先搞清楚推送链路到底是怎么走的很多人在Unity里接入推送时懵就懵在没弄明白APNs在整个环节里处在哪个位置。等你看懂这条链路是怎么转的后面写代码、配证书、联调排错心里就有底了。1.1 APNs在整个环节里管哪一段iOS远程推送的核心链路是这样的服务端你的推送Provider先把消息发给苹果的推送服务器也就是APNsAPNs再把它推送到目标用户的iPhone上最后你的App收到这颗信使决定弹个横幅还是跳转某个页面。这里面有个关键概念叫deviceToken它是苹果颁发给App在当前设备上的唯一门牌号。每一次接入你的App都要向APNs注册自己注册成功后苹果会给它发一串十六进制字符串这就是deviceToken。你的服务器要推送时不是对着你的App直接喊而是拿着这串门牌号发给APNsAPNs才能准确地把消息送到这部设备上。在Unity里最麻烦的就是中间这一段Unity的C#层拿不到deviceToken这个注册动作必须用iOS原生代码来做。你让Unity去单独申请通知权限、去和苹果的令牌服务器说话是做不到的所以必须写一层Objective-C代码作为桥接。理解了这一步就理解了整个接入工作量的核心。1.2 Unity接入APNs常见的三种路径针对Unity项目业内接推送基本是这三条路各有优劣看项目情况选。纯原生桥接插件自己在Xcode层面写Objective-C/Swift代码封装成Unity可以调用的原生插件。优点是完全可控、不依赖第三方、没有额外SDK体积缺点是要懂iOS原生开发而且Unity版本升级、Xcode升级时偶尔得适配一下。第三方聚合推送服务比如极光、个推、友盟这类。它们提供Unity SDK底层把APNs封装好了你在后台配好证书就能用。优点是省事报表统计也齐全缺点是必须把推送请求先经过它们的服务器如果你的App有严格的数据合规要求这条要慎重。另外这类SDK对权限声明、隐私清单的要求也越来越严格。Unity商店里现成的推送插件这类插件其实也是帮你包了一层原生桥接质量参差不齐而且不一定持续维护。好处是省去写原生代码坏处是一旦苹果更新了API插件没跟上你的推送就悄悄挂了。我之前用过一个商店插件后来苹果改了通知回调签名插件作者不更新整个项目被卡住最后还是自己写了一个。我的建议是只要团队里有一个人会写Objective-C就优先自己写桥接层。APNs的接口其实不多注册、回调、处理事件核心代码量也就一两百行。自己写的最大好处是出了问题你有能力自己排查不会被第三方SDK拦在门外。2. 动手前的事证书、开发者账号、工程权限一个都不能少推送这个功能是典型的配置比代码先坑你。代码问题报错了能看见证书问题常常是静默发生的。我见过不少项目代码写得没问题但推送就是不响最后发现是证书配置或者推送环境不匹配。所以这一节老老实实把前置条件讲透。2.1 APNs Auth Key和p12证书我推荐用哪个在苹果开发者后台准备推送凭证有两种主流方式传统的是生成Apple Push Notification service SSL证书也就是p12文件另一种是APNs Auth Key也就是p8文件。两者最大的区别在于p12证书一个App ID就要配一份而且有有效期过期就得重新生成、重新上传服务端开发环境证书和生产环境证书还是分开的。而APNs Auth Key一个钥匙就能给开发者账号下所有App做推送不区分开发或生产环境也不会每年过期长期省心太多。从实操角度除非你的服务端供应商只支持p12格式否则我强烈建议用p8的APNs Auth Key。流程是登录苹果开发者后台在Keys选项里新建一个Key名字随便写但必须勾选APNs权限。创建后只给你下载一次p8文件务必保管好丢了下不回来只能重新生成一个并记录两个值Key ID和你的Team ID这两个后面配置服务端时会用到。2.2 开发者后台App ID与Xcode能力配置关键一步你的App ID必须开启Push Notifications能力。在后台的Identifiers里找到你的Bundle ID勾上Push Notifications保存。很多人以为Unity导出工程后Xcode里能配置就行其实后台这一步没开你的App到了真机上注册推送时会一直失败。接着是Xcode里的配置。导出Unity工程后用Xcode打开选中Target在Signing Capabilities里加一项Push Notifications。如果你的项目还需要静默推送也就是App在后台时也能收数据而不弹出横幅那么还要在Background Modes里勾选Remote notifications。这里有个新老Unity版本的差别老版本Unity导出后Xcode里的能力要手动配。新版本Unity在Player Settings里的iOS Capabilities一栏就能直接勾选Push Notifications导出后自动带上。如果你的项目还在用老版本建议升级时顺手做一次全量回归测试因为工程结构变了桥接插件的挂载方式也可能受影响。2.3 Unity工程侧的几个配置关口在Unity侧有几个配置直接影响打包结果忘掉一个就会在真机上扑街。Bundle Identifier必须和后台App ID完全一致包括大小写。如果Unity这边填了com.example.game后台那边填的是com.example.Game推送注册会直接失败。Scripting Backend建议保持IL2CPPARM64。虽说Mono也能跑但现在很多iOS原生SDK都要求IL2CPP第三方推送服务接入时尤其如此早换早安心。勾选Automatic Signing后确认Team选对了。如果你有多个开发者账号Team选错会导致签名失败或者装到真机上推送收不到。如果是Unity 2022以上版本可以顺手在Player Settings的iOS Capabilities里直接点开Push Notifications导出工程后Xcode里就不用再加了。有一个容易忽略的坑如果你用第三方推送SDK它可能在Unity导入时就自动修改了Xcode工程的能力配置。如果你同时自己写了原生桥接两边可能存在权限重复申请或者delegate冲突。所以我建议要么用自己写的桥接要么用纯三方的SDK别混着来把iOS的UNUserNotificationCenter delegate当成一个全局单例谁先设置谁影响结局混用很容易互相覆盖导致另一个失效。3. 原生桥接层拿到deviceToken并稳稳回传给Unity到了整个接入里最核心的部分。我会用Objective-C写一个独立的桥接类用黑魔法的方式hook掉UnityAppController的代理方法这样不需要修改Unity每次打包都重新生成的模板文件。这也是我试过好几种方案后觉得最稳定的一种。3.1 原生代码文件放哪里怎么让Unity认识它在Unity工程根目录下建一个Assets/Plugins/iOS文件夹把写好的Objective-C文件丢进去。Unity打包时这个文件夹里的源文件会被自动拷贝到Xcode工程里参与编译不需要你每次打包后再手动拖文件。我自己的做法是建两个文件一个PushService.h头文件一个PushService.m实现文件。命名建议别太通用像UnityPushManager这样的名字很容易和第三方SDK里的类撞上我的习惯是带个项目缩写前缀比如XXGameUnityPushManager降低冲突概率。在PushService.m里我直接用分类加方法交换的方式把UnityAppController的若干代理方法拿到手。这么做的好处非常明显Unity每次重新导出工程模板文件都会被覆盖重来我不用再去手动塞代码。需要注意一点UnityAppController这个类在不同Unity引擎版本里的初始化和代理方法签名基本稳定我从Unity 2019一直用到Unity 2022没碰上过例外。但如果哪天Unity官方大改了iOS启动流程这部分逻辑要记得重新检查。3.2 权限申请与链路注册先晾一段核心代码框架后面我会拆开解释每一块的作用。#import UserNotifications/UserNotifications.h #import UnityAppController.h #import objc/runtime.h interface XXGameUnityPushManager : NSObject UNUserNotificationCenterDelegate end implementation XXGameUnityPushManager (instancetype)sharedInstance { static dispatch_once_t onceToken; static id instance nil; dispatch_once(onceToken, ^{ instance [[XXGameUnityPushManager alloc] init]; }); return instance; } - (void)registerForPush { UNUserNotificationCenter *center [UNUserNotificationCenter currentNotificationCenter]; center.delegate self; UNAuthorizationOptions options UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; __weak typeof(self) weakSelf self; [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { __strong typeof(self) strongSelf weakSelf; if (!strongSelf) return; dispatch_async(dispatch_get_main_queue(), ^{ if (granted) { [[UIApplication sharedApplication] registerForRemoteNotifications]; [strongSelf sendAuthResult:granted]; } else { [strongSelf sendAuthResult:denied]; } }); }]; } (void)load { [self swizzleApplicationMethods]; } (void)swizzleApplicationMethods { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class cls NSClassFromString(UnityAppController); SEL originalSelector selector(application:didFinishLaunchingWithOptions:); SEL swizzledSelector selector(xx_application:didFinishLaunchingWithOptions:); Method originalMethod class_getInstanceMethod(cls, originalSelector); Method swizzledMethod class_getInstanceMethod(self, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); // 其余代理方法按同样方式操作 }); } end这块里最关键的是权限申请。iOS 10以后推送权限统一走UNUserNotificationCenterrequestAuthorizationWithOptions会弹出系统权限框选项里声明了你要用横幅、声音、角标。用户点击允许后你再调用registerForRemoteNotifications去真正请求那个deviceToken。这两步是分开的很多第一次接的人会漏掉第二步导致权限框弹了Token却一直没拿到。注意我在这里用了dispatch_async(dispatch_get_main_queue(), ...)因为授权回调不在主线程而registerForRemoteNotifications和UnitySendMessage都应当跑到主线程去执行避免奇怪的线程问题。3.3 deviceToken的字节流转细节决定成败苹果回调返回的deviceToken是一个NSData对象很多教程里会让你deviceToken description然后去掉空格尖括号这个方法相当不严谨因为description的格式从技术上说是不保证稳定的。正确做法是逐字节拼十六进制字符串- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { const unsigned char *dataBuffer (const unsigned char *)deviceToken.bytes; NSUInteger length deviceToken.length; NSMutableString *hexString [NSMutableString stringWithCapacity:(length * 2)]; for (NSUInteger i 0; i length; i) { [hexString appendFormat:%02x, dataBuffer[i]]; } [self sendToken:hexString]; }这个方法得到的字符串才是标准的deviceToken。还要提醒一句开发环境的Token和生产环境的Token不是同一个值你在联调时把开发Token发到服务端切到发布环境后就不能再用了这个我后面还会再提。3.4 用UnitySendMessage把Token和推送内容递到C#拿到Token之后要做的核心动作就是调用UnitySendMessage它的签名是这么三个参数目标GameObject名字、要调用的方法名、参数值。它就像一架跨语言的电话从原生层拨到Unity层。- (void)sendToken:(NSString *)token { UnitySendMessage(PushReceiver, OnDeviceToken, [token UTF8String]); } - (void)sendAuthResult:(NSString *)result { UnitySendMessage(PushReceiver, OnAuthResult, [result UTF8String]); } - (void)sendPushPayload:(NSDictionary *)userInfo { NSError *jsonError nil; NSData *jsonData [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:jsonError]; NSString *jsonString [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; UnitySendMessage(PushReceiver, OnPushPayload, [jsonString UTF8String]); }这里有几个注意点都是实战踩过的坑UnitySendMessage里的GameObject名字必须是场景里真实存在的而且这个对象不能被销毁。所以我在Unity里专门建了一个空物体叫PushReceiver挂上处理脚本在Awake里执行DontDestroyOnLoad这样才能保证跨场景、跨生命周期都能收到原生层消息。调用的方法不能是静态方法也不能带多个参数就一个字符串参数。如果你需要传复杂结构就在原生层把它转成JSON字符串所有字段塞进一个字符串里递给UnityC#侧再解析。这个习惯很重要推送payload里的自定义字段都能靠这个方案传过去。UnitySendMessage调用时如果找不到GameObject它不会报错但你的消息就无声无息丢了排查起来很迷惑。所以我一般会在PushReceiver的Awake里加一行日志确认它没被销毁。4. 收到推送之后Unity这一侧怎么接怎么解析原生层把消息递过来了Unity这边要好生接待。这里包括三个场景App在前台、App在后台被点击、App被彻底杀死后从推送冷启动。这三种情况处理方式完全不同一个考虑不到用户就抱怨点了推送没反应。4.1 C#侧的接收脚本该怎么写首先在Unity里创建一个空GameObject命名为PushReceiver挂上如下脚本的核心部分using UnityEngine; public class PushReceiver : MonoBehaviour { private void Awake() { DontDestroyOnLoad(gameObject); Debug.Log(PushReceiver ready); } public void OnDeviceToken(string token) { Debug.Log(推送Token: token); // 这里把token发给自己的业务服务器绑定用户ID } public void OnAuthResult(string result) { Debug.Log(推送授权结果: result); if (result denied) { // 提示用户去系统设置里打开通知开关 } } public void OnPushPayload(string json) { Debug.Log(收到推送: json); // 解析JSON根据业务字段跳转页面 } }这里有个Uniform的地方要注意Unity方法名区分大小写原生层里UnitySendMessage(PushReceiver, OnDeviceToken, ...)C#侧的方法必须叫OnDeviceToken不能写成Ondevicetoken。这类错误不会编译报错但消息发不过来卡你半天你都不知道哪里出了问题。子啊刚才那句跳过了不重要继续。如果你的Unity工程里同时接了其它原生SDK注意类的冲突问题。我习惯在类名和方法名上都加项目前缀比如XXPushReceiver、OnXXDeviceToken既解决冲突也方便日志里识别。4.2 App被杀掉以后从推送消息冷启动怎么办这是推送接入里最容易被忽略的场景也是我见过最多人卡住的地方。当App完全没有运行时用户点击推送横幅系统会把App拉起来同时把推送内容放在启动参数launchOptions里。但问题是Unity引擎启动需要几秒钟如果你在原生层一启动就立刻调用UnitySendMessageUnity那边可能还没初始化到位消息就丢了。我的解决方案是本地缓存加延时补偿。原生层在didFinishLaunchingWithOptions里读取启动参数中的UIApplicationLaunchOptionsRemoteNotificationKey先把推送内容存到一个本地变量里再等Unity就绪。Unity侧启动后主动调用一个原生接口去取这份缓存如果拿到了就把冷启动的通知补发给业务层。- (BOOL)xx_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 原始方法会被替换到别处这里先调用原有的方法逻辑 // 实际工程里注意swizzle后调用顺序 NSDictionary *remoteNotification launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (remoteNotification) { self.pendingPushPayload remoteNotification; } return YES; }Unity侧在场景加载完成后调用一次GetPendingPushPayload原生接口如果有缓存就把内容解析出来然后清掉缓存。这样做的好处是无论Unity启动多慢推送内容都不会丢一定能在某个确定时机交到业务层手里。4.3 App在前台时为什么不弹横幅以及角标怎么清理一个很基本的iOS特性App在前台时系统默认不展示推送横幅只会静静地走回调。你要是不做处理用户以为推送没来其实消息已经躺在回调里了。所以在willPresentNotification里要根据业务需求决定要不要展示横幅、播放声音- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { if (available(iOS 14.0, *)) { completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound); } else { completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound); } }另外是角标。推送payload里如果带了badge字段App图标上就会出现红点数字。但注意这个数字累不累加、清零逻辑苹果不会替你处理。很多App推送都带badge然后越积越多用户看着极不舒服。我的做法是在每次App从后台进入前台时把角标清零[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];Unity侧可以在生命周期事件里主动调用这个原生接口或者写在App进入前台的代理回调中。这个细节虽然小但直接影响用户体验做得好很加分。4.4 推送payload的自定义字段解析APNs推送的payload标准结构长这样{ aps: { alert: { title: 新的消息, body: 你有一条新的订单通知 }, sound: default, badge: 1 }, customData: { type: chat, targetId: 123456 } }aps键是苹果规定的用来控制系统层面的展示。你自己的业务字段放在aps外面的自定义区域。原生层收到通知后会把整个userInfo字典原封不动转成JSON字符串递给UnityUnity侧拿到后用JsonUtility或者Newtonsoft.Json解析。我一般定义一个完整的模型类[System.Serializable] public class PushPayload { public Aps aps; public CustomData customData; } [System.Serializable] public class Aps { public Alert alert; public string sound; public int badge; } [System.Serializable] public class Alert { public string title; public string body; } [System.Serializable] public class CustomData { public string type; public string targetId; }这样解析出来以后根据type字段去决定跳转到聊天页、订单页还是活动页。这里务必要处理解析异常推送内容结构不可控收到坏数据时不能崩包一层try catch是基本素养。5. 真机联调、打包发布与常见问题速查代码写完了配置齐了接下来是验证环节。推送联调和普通功能联调不一样它依赖苹果服务器、需要外部推送请求、还要看环境配对整个链路任何一个环不对表现都是收不到。5.1 从零到一跑通一次真机推送我自己的联调流程是固定的照着走一遍基本能在半小时内确认链路是否通Unity工程里确认Bundle ID和后台App ID一致Team选对导出Xcode工程。用Xcode打开工程确认Push Notifications能力已勾上Signing Capabilities里没有报错。把真机连上Mac选择自己的开发者团队直接跑Release或Debug包装到手机。在Xcode的控制台里观察日志看到推送Token: 一串十六进制字符串说明注册成功。把这串Token发给自己的服务端同学让他在服务端把这条设备和某个测试用户绑定。服务端用APNs Auth Key发起一条推送请求环境选择sandbox目标就是这串Token。手机锁屏状态下收到横幅点击后观察Unity日志里是否打印出OnPushPayload内容。如果你没有服务端配合也可以用一些在线的APNs调试工具填上p8文件的Key ID、Team ID、Bundle ID和deviceToken点发送就行。这类工具很多选一个界面干净的即可。5.2 那些年踩过的坑我整理成了一张速查表这里把我在多个项目里实际遇到过的推送疑难杂症整理成表格每一条都附上典型原因和解决办法。现象大概率原因解决办法权限框一直不弹App ID后台没开Push Notifications去开发者后台检查Identifiers里的开关授权框弹了但拿不到Token没有调用registerForRemoteNotifications在授权granted回调里补上第二步注册真机跑的时候直接报No valid APNs certificate签名Team不对或APNs能力没开检查Signing Team和CapaibilitiesToken能拿到但服务端推送收不到推送环境发错了开发Token打到了生产环境服务端联调阶段用sandbox环境iOS 13及以上偶尔收不到横幅App处于前台且没有实现willPresent回调实现前台回调并主动声明展示点推送不跳转页面冷启动路径没做缓存补发实现launchOptions缓存与Unity主动拉取Unity侧收不到任何原生回调UnitySendMessage的GameObject被销毁或不存在确认物体存在且DontDestroyOnLoad推送角标数字永远没变化payload没带badge或没有设setApplicationIconBadgeNumber服务端加badge客户端适时清零5.3 上线前必须检查的几件事到了临上线前有几件事建议逐一过一遍不然上架后才发现推送挂了会被用户骂得很惨。第一确认推送环境切换。发布环境推送走的是APNs的生产环境token也变了。服务端如果还拿开发环境的token和配置发生产推送必然失败。发布前让服务端把环境切到production重新拿一份生产token测试。第二用户授权被拒后的引导。如果用户第一次点了不允许iOS不会再次自动弹权限框。应用内得做一个引导页带用户去系统设置里手动打开通知开关。这个功能不做的话你损失一半以上的推送触达率。第三检查隐私清单。苹果对隐私合规越来越严如果你用第三方推送SDK记得生成并配置好PrivacyInfo说明收集的数据类型和用途。自己写桥接的话至少确保你不上传deviceToken之外的额外用户信息把合规风险降到最低。第四推送内容测试要覆盖三态。App前台、后台驻留、完全退出这三种状态下都各推一条测试消息确保回调、横幅、跳转全部正常。很多问题只在其中某一种状态下暴露。老实说iOS推送这东西配置的门槛高过代码门槛。只要你能稳定拿到那一串token后半段的推送链路会好走得多。我自己跑通第一遍时也踩了一脑袋包不是漏了后台开关就是忘了前台回调后来把这些沉淀成一套固定流程后面每个项目都能很快接完。最后再分享一个小技巧把原生层的每个关键节点都加上日志token是否拿到、授权是允许还是拒绝、前台是否走回调、冷启动缓存是否清空。这些日志在联调时是你最重要的眼睛。推送不像普通功能报错是静默的只有日志能告诉你它到底卡在哪一环。保留这套日志以后线上出任何推送问题你都能第一时间定位位置。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑