ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity iOS接入微信登录与分享:从配置到避坑全流程

Unity iOS接入微信登录与分享:从配置到避坑全流程

1. 接入前的整体设计与思路拆解

去年我在做一款上线iOS的Unity游戏时,需要接入微信登录和分享功能。本来以为这活儿跟Android端差不多,调个SDK、写几个方法就行,结果真正动手才发现iOS端的流程比想象中繁琐得多,坑也更多。写这篇文章就是为了把这个过程完整记录下来,尤其是几个容易卡住的细节。

先说清楚你将要面对的是什么样的技术链路。微信官方提供的Unity SDK其实已经封了一层,但它在iOS上的实现依赖的是原生Objective-C代码,通过Unity的RuntimeExport机制和UIApplicationDelegate消息转发来打通Unity层和iOS原生层。所以你的Unity工程里最终会同时存在C#脚本和Objective-C脚本,两者之间通过UnitySendMessage互相传消息。

整个接入过程可以拆成三大块:

  • 微信开放平台侧的配置:拿AppID、配置Bundle ID、配置Universal Links,这块属于"一个都不能错"的前置条件,错了后面全白搭。
  • iOS原生层:把微信SDK集成到Xcode工程里,实现WXApiDelegate,把微信回调转发给Unity。
  • Unity C#层:调起登录、调起分享、接收回调结果并处理。

我选择的方案是:不用微信官方Unity SDK,直接在Unity工程里自己写Objective-C的桥接代码。原因有两个,一是官方Unity SDK的更新节奏经常跟不上微信SDK的更新,二是自己写桥接代码能完全掌控回调链路,排查问题的时候思路清晰。当然,如果你只是想快速实现,用官方Unity SDK也不是不行,只是后面如果遇到问题,你可能会跟我一样要拆开SDK源码去翻逻辑。

提示:本文的步骤基于Unity 2021.3 LTS和微信SDK 2.0.2(2024年初版本),不同版本的操作细节可能有差异,但整体流程是通用的。

2. 微信开放平台配置:漏掉任何一项都会让你怀疑人生

很多人习惯先把代码写上,写完再去开放平台注册。我建议反过来,先花十分钟把开放平台的配置搞定,因为AppID和Universal Links是写死在代码里的,后期再改就是一遍全量打包重来。

2.1 应用注册与AppID

登录微信开放平台,在"管理中心"创建移动应用。这里要注意一个细节:iOS的Bundle ID必须和你Xcode工程里的Bundle Identifier完全一致。我见过有人在这里填错一个字母,结果所有API调用都返回"签名错误"。

创建完成后你会拿到一个AppID(形如wx1234567890abcdef)和AppSecret。AppSecret在iOS端基本用不到,但建议保存好,后面调试分享参数详情时可能会用到。AppSecret不要写进客户端代码里,微信SDK的iOS端设计是签名在服务端做,客户端只需要AppID就能调起微信。如果你在客户端里塞了AppSecret,上架审核时会因为"客户端包含敏感密钥"被打回。

2.2 Universal Links配置

Universal Links是微信登录和分享在iOS上的通行凭证。它的本质是:微信服务器验证你的App和域名之间的绑定关系,然后在调起微信后,微信通过Universal Links回调你的App。

这个配置分成三步:

  1. 在你的服务器上放一个apple-app-site-association文件,内容格式像这样:
{ "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.yourcompany.yourgame", "paths": ["/ios/*"] } ] } }

注意appID的格式是TeamID + "." + Bundle ID。TeamID在苹果开发者后台的"Membership Details"里能找到,不是App ID Prefix,这俩我当初绕了半小时才搞明白。

  1. 文件必须能通过HTTPS访问,且不能有重定向。我建议你配置完后直接Safari打开https://yourdomain.com/apple-app-site-association确认能访问到内容。如果你的CDN有二次重定向,iOS是认不出来的。

  2. 在Xcode工程里配置Associated Domains,加applinks:yourdomain.com。

这里有个很容易踩的坑:模拟器上测试Universal Links经常失败。模拟器的Universal Links验证机制跟真机不一样,有时候你在模拟器上试了不行,真机上其实是好的。所以从一开始就用真机调试,别浪费时间在模拟器上。

2.3 URL Scheme配置

微信SDK还需要配置URL Scheme。在Xcode工程里,找到Info.plist的URL Types,添加一个URL Scheme,值就是你的AppID。这个URL Scheme的作用是微信通过OpenURL的方式把结果回调给你的App。

Info.plist里要有这几项:

  • LSApplicationQueriesSchemes:添加weixin和weixinULAPI,否则iOS 9以后无法判断微信是否安装。
  • NSAppTransportSecurity:如果你的Universal Links服务端走的是HTTP(不建议),需要配置NSAllowsArbitraryLoads。但我强烈建议全部走HTTPS,省得后面被审核盯着问。
  • CFBundleURLTypes:添加你的AppID作为URL Scheme。

这三项配置缺一个,微信SDK的表现会非常奇怪——不是报"已安装"判断错误,就是回调完全收不到。

3. iOS原生层实现:Objective-C桥接的完整写法

配置做完,开始写代码。这一节的代码是需要在Unity工程里新建一个Assets/Plugins/iOS目录,放一个Objective-C文件,内容长这样(下面会逐步拆解):

3.1 导入微信SDK

微信SDK的下载包里有libWechatSDK.a这个静态库,还有一堆头文件。你需要:

  1. 把.a文件和头文件整体拖进Assets/Plugins/iOS目录。
  2. 给.a文件添加-ObjC标志。这个操作在Xcode里做:Build Settings->Other Linker Flags-> 添加-ObjC。不加这个标志,你会在调用微信API时报一堆"unrecognized selector"错误。

顺带一提,微信SDK有两个版本,WechatAuthSDK(用于语音识别等能力)和WeChatSDK(核心登录/分享)。我们只需要后者,别一股脑全拖进去。

3.2 AppDelegate的消息转发

微信SDK要求你继承WXApiDelegate,并在AppDelegate里转发两个方法。但由于Unity工程里的AppDelegate是从UnityAppController继承来的,你不能直接修改Unity生成的AppDelegate,得通过RuntimeExport来"注入"方法。

在Objective-C文件里写一个分类(Category):

#import <UIKit/UIKit.h> #import "WXApi.h" #import "UnityAppController.h" @interface UnityAppController (WeChat) <WXApiDelegate> @end @implementation UnityAppController (WeChat) - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { return [WXApi handleOpenURL:url delegate:self]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { return [WXApi handleOpenURL:url delegate:self]; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler { return [WXApi handleOpenUniversalLink:userActivity delegate:self]; } - (void)onReq:(BaseReq *)req { // 微信主动发起的请求,一般这里不需要处理 } - (void)onResp:(BaseResp *)resp { if ([resp isKindOfClass:[SendAuthResp class]]) { SendAuthResp *authResp = (SendAuthResp *)resp; NSString *code = authResp.code ?: @""; NSString *result = [NSString stringWithFormat:@"login_%@_%@", code, @(resp.errCode)]; UnitySendMessage("WeChatManager", "OnLoginCallback", result.UTF8String); } else if ([resp isKindOfClass:[SendMessageToWXResp class]]) { SendMessageToWXResp *shareResp = (SendMessageToWXResp *)resp; NSString *result = [NSString stringWithFormat:@"share_%@", @(resp.errCode)]; UnitySendMessage("WeChatManager", "OnShareCallback", result.UTF8String); } } @end

这里有几个关键点要说明:

第一,RuntimeExport其实不需要额外写代码。Unity的UnityAppController在启动时会扫描所有分类(Category),把实现过的方法自动跟UIApplicationDelegate绑定。你只需要确保分类里的方法签名跟UIApplicationDelegate一致就行。我最初不知道这个机制,以为还要手动注册,实际无需多此一举。

第二,onResp里的UnitySendMessage是Unity给原生层的"对话窗口"。这个方法在Unity启动后随时可用,它会找到名为WeChatManager的GameObject,调用上面的OnLoginCallback方法。这个GameObject必须提前存在于场景里,而且名字要完全匹配,否则消息会静默丢失——这是我最常遇到的"回调消失"问题。

第三,errCode是微信返回的错误码,0表示成功,-2表示用户取消。在onResp里不要做复杂的业务逻辑,把它原样转发给Unity层处理就行。原生层越"傻"越不容易出问题。

3.3 登录和分享的调用方法

调起微信登录需要在Unity层发指令给原生层,同样用RuntimeExport机制。在Objective-C文件里加一个方法:

extern "C" { void _WeChatLogin() { SendAuthReq *req = [[SendAuthReq alloc] init]; req.scope = @"snsapi_userinfo"; req.state = @"your_custom_state"; [WXApi sendReq:req completion:nil]; } void _WeChatShareText(const char *text, int scene) { SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init]; req.bText = YES; req.text = [NSString stringWithUTF8String:text]; req.scene = (int)scene; // WXSceneSession 0, WXSceneTimeline 1 [WXApi sendReq:req completion:nil]; } void _WeChatShareLink(const char *title, const char *desc, const char *url, const char *thumbImagePath, int scene) { WXMediaMessage *message = [WXMediaMessage message]; message.title = [NSString stringWithUTF8String:title]; message.description = [NSString stringWithUTF8String:desc]; [message setThumbImage:[UIImage imageWithContentsOfFile:[NSString stringWithUTF8String:thumbImagePath]]]; WXWebpageObject *webObj = [WXWebpageObject object]; webObj.webpageUrl = [NSString stringWithUTF8String:url]; message.mediaObject = webObj; SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init]; req.bText = NO; req.message = message; req.scene = (int)scene; [WXApi sendReq:req completion:nil]; } }

extern "C"是关键。C#端通过[DllImport("__Internal")]调用原生方法时,要求方法名不被C++编译器修饰,extern "C"保证了这一点。如果你的文件扩展名是.mm(Objective-C++),不加extern "C"会链接失败;如果扩展名是.m,本来就够,但加上没坏处。

微信登录的scope参数,目前官方文档只推荐"snsapi_userinfo",它可以拿到用户的openid、unionid和access_token。state字段是防CSRF攻击用的,建议服务端生成一个随机值,回传时校验。

注意:setThumbImage的图片大小不能超过32KB,超过的话分享卡片会显示异常。我之前处理一张大尺寸截图,压了几次都没压到32K以内,最终只好裁剪加压缩——这个限制卡得很死,必须在分享前处理干净。

3.4 注册微信SDK

启动时注册也是必要的一步。微信SDK要求在application:didFinishLaunchingWithOptions:里注册registerApp。同样在分类里加:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [WXApi registerApp:@"wx1234567890abcdef" universalLink:@"https://yourdomain.com/ios/"]; return YES; }

这里注意:universalLink必须跟你开放平台填写的Universal Links前缀一致,而且要以/结尾。如果这里填错了,登录回调会偶尔成功偶尔失败,表现非常诡异。

如果你同时在别的渠道(比如一键登录)也实现了didFinishLaunchingWithOptions,注意方法覆盖的问题。UnityAppController的分类方法在运行时是动态合并的,多个分类实现同一个方法时,只有一个会生效。我处理过一起冲突,解决办法是不要重名,把微信注册逻辑挪到application:didFinishLaunchingWithOptions里调用一个单独的初始化函数。但如果你用的是最新Unity版本,UnityAppController提供了startUnity相关方法,你可以把微信注册放在Unity启动完成后的OnUnityReady回调里。

4. Unity C#层实现:回调管理与业务拆分

原生层打通了,C#这边就要负责把回调接到游戏业务逻辑上。这一层比较容易被忽略,实际上最需要设计。

4.1 桥接类WeChatManager

创建场景里的一个空GameObject,挂上WeChatManager脚本。脚本里定义回调方法,方法名必须跟原生层UnitySendMessage的第三个参数对应。

using System; using System.Runtime.InteropServices; using UnityEngine; public class WeChatManager : MonoBehaviour { [DllImport("__Internal")] private static extern void _WeChatLogin(); [DllImport("__Internal")] private static extern void _WeChatShareText(string text, int scene); [DllImport("__Internal")] private static extern void _WeChatShareLink(string title, string desc, string url, string thumbImagePath, int scene); private static Action<string> _loginCallback; private static Action<int> _shareCallback; public static WeChatManager Instance { get; private set; } private void Awake() { Instance = this; DontDestroyOnLoad(gameObject); } public static void Login(Action<string> onLogin) { _loginCallback = onLogin; _WeChatLogin(); } public static void ShareText(string text, int scene, Action<int> onShare) { _shareCallback = onShare; _WeChatShareText(text, scene); } public static void ShareLink(string title, string desc, string url, string thumbImagePath, int scene, Action<int> onShare) { _shareCallback = onShare; _WeChatShareLink(title, desc, url, thumbImagePath, scene); } public void OnLoginCallback(string result) { // result格式: login_{code}_{errCode} if (result.StartsWith("login_")) { string[] parts = result.Split('_'); if (parts.Length >= 3) { string code = parts[1]; int errCode = int.Parse(parts[2]); _loginCallback?.Invoke(errCode == 0 ? code : $"ERROR_{errCode}"); } } } public void OnShareCallback(string result) { // result格式: share_{errCode} if (result.StartsWith("share_")) { string[] parts = result.Split('_'); if (parts.Length >= 2) { int errCode = int.Parse(parts[1]); _shareCallback?.Invoke(errCode); } } } }

这个设计有几个考量:

  • 回调入参是string,不是对象。因为UnitySendMessage只能传字符串,把对象序列化成JSON从一个方向传也是可以的,但我为了简单直接用分隔符拼接。如果业务复杂,可以考虑用JSON格式传,解析更稳。
  • 回调处理放在Update之外,直接在UnitySendMessage调用的线程里执行。Unity的UnitySendMessage实际上是在主线程执行的,可以放心操作Unity API。
  • 静态回调变量_loginCallback和_shareCallback在登录/分享发起时赋值、回调后清空(可以在调用后做null处理),避免跨多次调用的消息串台。

4.2 登录后的业务处理

微信登录在客户端只能拿到code,真正的用户信息(openid、nickname、avatar)需要用code去服务端换。这是微信SDK的安全设计,也是我强烈建议你不要在客户端直接调接口换token的原因。

我当时的做法是:

  1. 客户端拿到code后,通过我们自己服务器的登录接口传过去。
  2. 服务器拿着code去微信的https://api.weixin.qq.com/sns/oauth2/access_token这个接口换access_token和openid。
  3. 再用access_token调/sns/userinfo拿用户资料。
  4. 服务器处理好后返回带签名的登录凭证给客户端,客户端存到本地,进入游戏。

这个流程里有个坑:微信的code有效期只有五分钟,而且只能用一次。如果你的服务器处理超时或失败,你不能用同一个code重试,得让用户重新调起登录。所以多一步异常处理是必须的,提示语要友好一点,不然用户会觉得"登录怎么老失败"。

分享那边相对简单,OnShareCallback拿到errCode后,0就是成功,-2就是用户取消,弹出对应的UI提示即可。

4.3 图标与缩略图的处理

分享链接时,微信要求有缩略图。图上文说过不能超过32KB,实际操作中还要注意格式。我测试下来,PNG格式的透明图在某些版本微信上会变成黑底,建议用JPG。另外imageWithContentsOfFile要求传的是本地路径,你要先把图从StreamingAssets或PersistentDataPath拷贝出来,不能直接传Application.streamingAssetsPath那样的虚拟路径。

public static string PrepareShareThumb(Texture2D tex) { // 等比缩放 int maxSize = 100; Texture2D scaled = ScaleTexture(tex, maxSize, maxSize); byte[] bytes = scaled.EncodeToJPG(80); string path = Path.Combine(Application.persistentDataPath, "share_thumb.jpg"); File.WriteAllBytes(path, bytes); return path; }

这里压缩完最好校验一下bytes.Length,大于32768就继续降画质或缩尺寸。你不想用户分享时微信弹"缩略图过大",这种体验很伤。

5. 常见问题与排查技巧实录

这块是本文的重点,全是实打实踩过坑后总结的经验。微信iOS接入的大部分调试时间都花在排查下面几个问题上。

5.1 登录回调"偶发"收不到

这是最常见的坑,表现是:调起微信登录,微信界面正常弹出,输完密码授权后,微信界面消失,但游戏里没有任何反应。

排查链路从三个方向走:

第一步,确认微信是否真的调用了你的onResp。可以在原生代码里加日志,或者直接加NSLog,看Xcode控制台有没有输出。如果没有输出,说明微信压根不认识你的App,多半是registerApp的universalLink填错了,或者开放平台配置的Universal Links跟代码对不上。

第二步,确认onResp执行了但UnitySendMessage没送达。这种情况在onResp里有日志、在C#的OnLoginCallback里打了断点又没执行的时候,要检查UnitySendMessage的第一个参数是不是WeChatManager。这个字符串必须跟场景里GameObject的名字完全一致,大小写、空格都不能差。很多人把脚本挂在名叫"Manager"的物体上,代码里却写着"WeChatManager",消息就这样丢了。

第三步,检查回调是否在App激活前到达。如果用户是冷启动直接被微信通过Universal Links拉起来的(比如通过分享链接进入App然后马上登录),回调时机可能太早,Unity还没准备好接收消息。稳妥的做法是启动时等UnitySendMessage的接收方注册完成再调起微信登录,或者在微信回调里加一个小延时(比如0.5秒)再发消息。

5.2 分享卡片显示"未验证应用"或加载失败

这个提示的意思通常是微信认为你这个App不具备分享权限。有几种情况:

  • 开放平台的应用审核状态不是"已审核通过"。微信开放平台的规则是,新注册的应用默认是"审核中"状态,审核通过后才能拥有完整的分享和登录能力。我第一次调试时就是审核被拒,状态一直是"审核中",结果分享卡片永远显示"未验证应用"。解决方法只能是去开放平台确认状态,该整改的整改。
  • Universal Links的路径规则没匹配上。重新核对apple-app-site-association文件,注意微信要求路径中不能有?参数。
  • 同一个App ID在开放平台绑定了错误的Bundle ID。这种问题通常在别人接手项目或打包机配置不一致时出现——你本地对了,发出去的包不对,别人自然跳不起来。

5.3 分享缩略图显示为空白

我遇到过分享出去好友一看是空白卡片,但卡片样式正常,跳转也正常。查了半天发现是缩略图路径写错了,imageWithContentsOfFile传了一个不存在的路径,微信静默加载失败,卡片留了标题和描述,但没图。

解决办法就是我从4.3节开始坚持的那套:先把图写到Application.persistentDataPath(这个路径对iOS是真实存在的、可写的),确认文件存在后再传给原生层。路径最好在回调里加一层Debug.Log打印出来,装上真机验证一遍。

5.4 微信SDK初始化崩溃

如果你在registerApp之前就调了sendReq,或者在Unity还没完全启动时就调_WeChatLogin,会crash。原因其实很简单,微信SDK要求先注册后才能发请求。我在项目启动流程里把登录按钮放在所有初始化完成之后才激活,可还是因为第三方登录插件的时序问题提前触发过一次。

这种情况建议在C#层做保护:

private static bool _isReady = false; public static void Init() { // 调用完registerApp后再设为true _isReady = true; } public static void Login(Action<string> onLogin) { if (!_isReady) { onLogin?.Invoke("ERROR_NOT_READY"); return; } _loginCallback = onLogin; _WeChatLogin(); }

给用户一个"微信组件初始化中"的提示,远好过App闪退体验。

5.5 排查工具

最后分享一个排查工具链,调试微信SDK时非常有用:

  • Xcode控制台的NSLog输出。在原生代码里加日志,特别是application:openURL:是不是被调到了。
  • safari直接访问https://yourdomain.com/apple-app-site-association,看文件是否符合JSON格式且没有跳转。
  • 用curl -I看服务器的返回头,确认Content-Type是application/json或application/pkix-crl,没有经过302跳转。
  • 断点调试onResp,确认errCode的具体值。微信的错误码有文档,-1是系统错误,-2是用户取消,-4是操作不支持,-5是授权拒绝,排查时直接看文档对比。

6. 实用建议:把登录分享做成可持续维护的模块

整个流程走通之后,有几个工程层面的建议供你参考。

建议在Unity侧把微信能力封装成独立模块,不跟业务逻辑耦合。比如WeChatManager只负责调起登录/分享和返回结果,具体"登录后要不要跳转关卡""分享成功要不要发奖励"这些逻辑,放在业务层去订阅回调。这样一来,换SDK版本、换第三方渠道,都只需要改桥接层,业务代码不动。

打包机配置要保证统一。微信SDK的集成状态跟Info.plist强相关,我出过一次事故:开发环境调通了,到了发布机因为Info.plist里的LSApplicationQueriesSchemes被热更新脚本覆盖,导致正式包调不起微信。后来我索性在Assets/Plugins/iOS下放了一份专门的Info.plist片段,用Unity的"追加模式"合并,确保每次出包都带上微信必需的字段。

版本管理上,微信SDK的版本迭代不算慢,建议升级前先去开放平台看更新日志,特别是Universal Links相关的改动,因为微信对Universal Links的校验规则有过收紧。我有一个习惯,每次升级完SDK就在真机上完整跑一遍登录+分享+被回调三个链路,不要只看编译通过就放行。

从研发效率的角度说,微信登录分享这种能力,尽早接入、尽早调通比较好。因为它牵扯开放平台审核、iOS签名、Universal Links等多个外部依赖,任何一个环节都需要等审核或者等配置生效,越往后拖越容易让发版流程卡壳。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进