简介:面向 Unity 开发者的一份华为 HMS SDK 接入示例工程,聚焦 Unity 项目集成华为账号、游戏与推送等服务的完整工程样本,适合具备一定 Unity 和 Android 打包基础、希望快速理解 HMS SDK 调用方式的读者参考。压缩包共 2000 个文件,约 28.82MB,内含 HuaweiSdkDemo 示例工程及配套依赖:bin、info、class、java、jar 等文件对应 Android 侧库与构建产物,cs、dll、asset、meta 等构成 Unity 脚本与资源目录,xml、properties、project 等用于工程和权限配置,另有 apk、log、txt 便于真机验证与问题排查。目前已有 1845 人学习。其价值在于提供较完整的工程结构和接口调用样例,可对照了解 SDK 初始化、登录、推送等功能的接入位置,并借现成配置观察 IL2CPP、AndroidManifest 权限及版本兼容性处理;目录层级与元数据文件也便于理解依赖组织和包体构成,减少从零搭建与排错的时间。
1. 拿到 Demo 先别急着 Import:Unity 接华为 SDK 接的到底是什么
很多人以为 Unity 接华为 SDK 就是导个包、填个 AppID、点一下运行。真正让人出错的地方在后面:换一台机器出包就报 6003,登录能过但支付拉不起,推送 Token 拿到了但通知栏什么都没有。原因是这套东西从来不是「一个 SDK」,而是 HMS SDK 的各 Kit 加上 AGC 的工程配置体系,两边靠包名、签名指纹、agconnect-services.json 这三根钉子绑死。这份 Demo 的价值不在于让你抄几行 C#,而在于它把初始化顺序、Kit 调用链和 Android 构建配置完整摆了一遍。适合需要把 Unity 项目上架应用市场、要华为帐号登录、内支付和推送的团队,也适合接手别人工程、面对一堆 AAR 不知道从哪下手的同学。下面按我实际接入的顺序拆。
2. 从 AGC 建应用到 Unity 工程落地:包名、指纹与 json 三件套
这条链路的起点不在 Unity,在 AGC 控制台。很多 Demo 之所以「直接能跑」,是因为作者把 agconnect-services.json 和自己的签名文件一起打包进去了,你换个 keystore,立刻降级成只能看不能跑。所以第一步不是打开 Unity,而是把你项目的身份信息定死。
2.1 AGC 侧要填的四项,每一项都会在别处讨债
在 AGC 里创建应用时填的东西看着是表单,其实每项都对应后面一个报错。我一般会先把手头工程的包名、签名确定下来再动手建应用,避免建完发现包名对不上又去改。
| 参数 | 填什么 | 后面在哪里讨债 |
|---|---|---|
| 应用包名 | 与 Unity Player Settings 里的 Package Name 完全一致 | 不一致时 SDK 初始化直接返回失败,日志里通常只说「app info invalid」 |
| SHA-256 证书指纹 | 你实际出包用的 keystore 的 SHA-256 | 最常见的就是 6003,登录、支付全挂 |
| 应用分类与内容分级 | 按实际上架类目选 | 影响支付商品和部分 Kit 的开通审核 |
| 需要开通的 Kit | 帐号、IAP、Push 等按需勾选 | 没开通的 Kit 调用会返回权限类错误,而不是崩溃 |
证书指纹这块我用 keytool 取,debug 包和 release 包是两套,别只填一套:
# debug keystore,Unity 默认就用它出调试包 keytool -list -v \ -keystore ~/.android/debug.keystore \ -alias androiddebugkey \ -storepass android # release keystore,上架前必须填这一套 keytool -list -v \ -keystore /path/to/your/release.keystore \ -alias your_alias输出里挑SHA256:那一行,去掉冒号、全大写或全小写都行,AGC 会自动规范化。这里有个血泪经验:团队里如果有人用 Android Studio 出过包,他的 debug keystore 和你机器上的不是同一个文件,指纹天然不同。所以证书指纹列表我建议把每个参与出包的人的 debug 指纹都加进去,别指望「我这儿能跑」。
agconnect-services.json 从 AGC 下载后,常见做法是放在 Unity 工程的Assets/下,构建后它应该出现在 APK 的 assets 根目录。这一步必须验证,不能靠猜:
# 出包后确认配置真的进包了 unzip -l app-release.apk | grep agconnect # 期望看到类似:assets/agconnect-services.json如果这里什么都没有,说明 Unity 没把它当资源打进包,AGC 那套东西在你机器上等于没配置。
2.2 工程目录和插件导入顺序,别把 AAR 叠成一锅粥
Unity 侧的华为 SDK 一般由三部分组成:AGC 基础包(Analytics、Crash、Remote Config 走这套)、各 Kit 的 Unity 封装、以及一堆.aar在Assets/Plugins/Android/下。接手别人工程时我最先做的事是把旧的 AAR 清干净再导新的,因为不同版本的同名库叠在一起,Gradle 只会给你一句Duplicate class,排查起来像黑匣子。
导入顺序我固定成:AGC 基础包 → 需要的 Kit 封装 → 自己的业务脚本。原因是 Kit 封装依赖 AGC 的初始化上下文,你先导 Kit 再导基础包,容易出现AGConnectInstance为空的情况。目录上,Assets/Plugins/Android/只放 AAR 和AndroidManifest.xml,业务 C# 脚本单独放一个目录,出问题时能一眼看出是插件层还是业务层。
另外,如果你项目里已经有别的第三方 SDK,先确认它的AndroidManifest.xml有没有和 HMS 的冲突。合并冲突最常见的表现是minSdkVersion被某一方抬高,或者provider的 authorities 撞车。
2.3 首次出包:Gradle 模板、minSdk 与 ARM64 的硬性约束
Unity 默认的 Gradle 模板不一定带华为的 Maven 仓库,得手动开 Custom Gradle Template。在 Player Settings 里勾上Custom Main Gradle Template和Custom Gradle Properties Template,然后在生成的mainTemplate.gradle里补仓库和插件:
// Assets/Plugins/Android/mainTemplate.gradle allprojects { repositories { google() mavenCentral() // 华为 SDK 的依赖都在这个仓库,缺了就是各种 ClassNotFound maven { url 'https://developer.huawei.com/repo/' } } } // 如果用了 AGC 的 agcp 插件做自动配置 buildscript { dependencies { classpath 'com.huawei.agconnect:agcp:1.x.y.300' } } apply plugin: 'com.huawei.agconnect'agcp的版本号不要凭记忆写,去插件包里的package.json或 release notes 里对一下,版本对不上 Gradle 会明确告诉你找不到。这一段的作用是让 Gradle 在编译期把agconnect-services.json解析成资源,所以你如果漏了apply plugin,运行时不会报「找不到文件」,而是初始化直接失败,反而更难查。
构建参数上,三条硬约束:Scripting Backend 选 IL2CPP,Target Architectures 勾 ARM64(现在上架基本都要 64 位),minSdkVersion 不低于 21。如果项目里有老的三方库只出 armeabi-v7a,会出现「华为设备装得上但登录 SDK 加载失败」的情况,这种问题日志里只报个UnsatisfiedLinkError,容易被误判成签名问题:
// 出包前用一小段编辑器脚本做自检,比出包后再查快得多 #if UNITY_EDITOR using UnityEditor; using UnityEngine; public class HmsBuildCheck { [MenuItem("Tools/HMS/Check Build Settings")] public static void Check() { // 架构检查:缺 ARM64 在部分机型上会直接加载失败 var arm64 = PlayerSettings.Android.targetArchitectures .HasFlag(AndroidArchitecture.ARM64); if (!arm64) Debug.LogError("Target Architectures 未勾选 ARM64"); // 后端检查:Mono 后端下部分 AAR 的 JNI 调用会异常 if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.Android) != ScriptingImplementation.IL2CPP) Debug.LogError("Scripting Backend 需为 IL2CPP"); // 包名检查:和 AGC 上填的必须一致 Debug.Log($"Package Name = {PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android)}"); Debug.Log($"minSdk = {PlayerSettings.Android.minSdkVersion}"); } } #endif这段脚本没做什么高深的事,只是把三个最容易翻车的开关做成一次点击。实际项目里我还会把当前使用的 keystore 路径打印出来,因为「我以为我用的是 release 签名」这种情况出现的频率比想象中高。
3. 初始化链路:HMSInitialize 的时序与登录态获取
Demo 里初始化往往就一行,实际项目里这一行的位置决定了后面所有 Kit 能不能用。这一段把时序、回调和状态缓存拆开讲。
3.1 为什么初始化必须早于第一个 Kit 调用
HMS 的各 Kit 在内部都依赖一个已经就绪的应用上下文,这个上下文由初始化过程建立。你把登录按钮放在首屏,用户点得比初始化回调快,就会拿到「服务不可用」。所以我的做法是:在第一个场景的Awake里发起初始化,用一个静态状态位挡住所有业务入口,UI 上按钮先置灰,回调成功再放开。
using UnityEngine; using System; public static class HmsBootstrap { public static bool Ready { get; private set; } public static event Action<bool> OnReady; // 由首个场景的启动脚本调用,保证只跑一次 public static void Init() { if (Ready) return; // 接口名以你手上插件 HUAWEI_HMS 命名空间里的实现为准, // 不同大版本的封装方法名会有差异,别照抄网上的旧写法 HMSInitialize.Init((success, msg) => { Ready = success; if (!success) Debug.LogError($"[HMS] init failed: {msg}"); OnReady?.Invoke(success); }); } }这里的关键不是那行Init,而是Ready这个闸门。所有 Kit 调用前先判一次HmsBootstrap.Ready,为 false 就直接返回并在 UI 上提示「服务初始化中」。这么做看起来笨,但能把「偶现的登录失败」这一类玄学问题挡在门外。
如果初始化一直失败,先看两件事:agconnect-services.json是否真的在 APK 里,以及当前签名指纹是否在 AGC 列表里。这两条占了初始化失败的绝大多数。
3.2 Account Kit 的静默登录与授权码换 Token
帐号这块,Unity 封装的入口在不同插件版本里长得不一样,有的是HuaweiAccount,有的是直接暴露AccountAuthManager。与其记方法名,不如理解流程:拉起授权 → 拿到 AuthAccount → 取 authorizationCode → 交给自己的服务端换 access token。客户端不要直接信任拿到的信息去做业务判定。
Android 原生侧的等价写法是这样,Unity 层只是薄封装:
// 静默/显式登录的通用形态 AccountAuthParams params = AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM; AccountAuthService service = AccountAuthManager.getService(context, params); service.startSignIn(activity) .addOnSuccessListener(authAccount -> { // authAccount 里有 openId、displayName、avatarUri 等 String code = authAccount.getAuthorizationCode(); // 把 code 回传给自己的服务端,由服务端换 token bridgeToUnity(authAccount.getOpenId(), code); }) .addOnFailureListener(e -> { // 用户取消登录也会走这里,别一律当异常弹窗 bridgeErrorToUnity(e.getMessage()); });参数说明:DEFAULT_AUTH_REQUEST_PARAM只申请基础信息,如果业务要手机号或邮箱,需要换成带 scope 的构造方式,并且在 AGC 侧确认该 scope 已开通。没开通就申请,返回的是权限错误而不是空值,这个区别在排查时很重要。
openId是用户在应用维度的唯一标识,可以做本地缓存做「上次登录帐号」提示;authorizationCode是一次性的,有效期短,必须立刻发往后端。我见过把 code 存在本地、下次启动再用的写法,结果就是间歇性登录失败。
3.3 回调线程与 Unity 主线程的桥接
HMS 的回调大多在 Android 的 UI 线程或子线程触发,而 Unity 的 GameObject API 只能在主线程调。直接在主线程外Instantiate一个 UI 预制体,轻则报错,重则崩溃。稳妥方案是用一个队列把回调塞回Update消费:
using System; using System.Collections.Generic; using UnityEngine; public class MainThreadDispatcher : MonoBehaviour { private static readonly Queue<Action> _queue = new Queue<Action>(); // 任意线程都可调用,只做入队 public static void Enqueue(Action action) { lock (_queue) { _queue.Enqueue(action); } } private void Update() { // 每帧统一消费,保证 Unity API 都在主线程执行 while (true) { Action act; lock (_queue) { if (_queue.Count == 0) break; act = _queue.Dequeue(); } act?.Invoke(); } } }有了这个,SDK 回调里只管MainThreadDispatcher.Enqueue(() => { ... }),业务逻辑不用再关心线程。这个类我几乎每个接第三方 SDK 的项目都会放一份,属于不用后悔药的那类基础件。
4. 三个 Kit 的最小可用接法:登录、支付、推送
Demo 通常只演示登录,真上架至少要这三样。每个 Kit 我都只给「能跑通业务闭环」的量,不铺开讲全部能力。
4.1 Account Kit:字段映射与首次登录的帐号绑定
登录成功后的字段要根据业务做一次映射,别直接把 SDK 返回对象丢给业务层,否则换插件版本时全项目都要改。
| SDK 返回字段 | 业务含义 | 注意事项 |
|---|---|---|
| openId | 应用内唯一用户标识 | 不要用 unionId 做本应用主键,跨应用场景才需要它 |
| displayName | 昵称 | 可能为空,UI 要有兜底 |
| avatarUri | 头像地址 | 有有效期,不要长期缓存这个 URL |
| authorizationCode | 一次性授权码 | 立即发后端换取 token |
首次登录要不要做帐号绑定,取决于你的老用户体系。常见做法是用 openId 查一次自家库,命中就直接登录,没命中再走「绑定已有帐号 / 注册新帐号」的分支。这一步别放在客户端做判定条件,删除本地数据就能绕过。
4.2 IAP:消耗型商品的补单与校验链路
内支付是最容易被「能拉起就行」糊弄过去的一块。下单成功只是第一步,发货必须由服务端确认,客户端拿到的东西一律视为不可信。消耗型商品的完整链路是:客户端发起购买 → 用户支付 → 拿到 purchaseToken 和订单号 → 发后端 → 后端校验并消耗 → 客户端收到发货结果后才给道具。
// 拉起购买,developerPayload 用来带自己的订单号 PurchaseIntentReq req = IapClientHelper.createPurchaseIntentReq( IapClientConst.PRICE_TYPE_INAPP_CONSUMABLE, sku, developerPayload); // 自己的订单号,务必带,补单时靠它对齐 Iap.getIapClient(activity) .createPurchaseIntent(req) .addOnSuccessListener(result -> { // result 里是待拉起的支付信息,这里不是支付成功 startBillingFlow(result); }) .addOnFailureListener(e -> { // 商品未配置、未开通 IAP 都会走这里 reportError(e.getErrorCode(), e.getMessage()); });参数上要注意PRICE_TYPE_INAPP_CONSUMABLE必须和 AGC 后台配的商品类型一致,配成非消耗型却按消耗型下请求,返回的错误信息很含糊,只会让你怀疑人生。
补单是这块的核心。用户支付后 App 被杀,下一次启动必须能把这笔订单找回来。我的做法是:本地记录 pending 状态的订单号(带 developerPayload),启动时先去后端查一遍状态,没有结果再调一次未消耗查询。这块我没见到哪个 Demo 写全,但它决定了你线上会不会收到「我付了钱没到账」的反馈。
4.3 Push Kit:Token 获取、通知栏样式与点击透传
推送的链路是 Token → 后端保存 → 后端下发。Token 会在某些情况下刷新,所以别只在首次启动取一次。
HmsInstanceId inst = HmsInstanceId.getInstance(context); // APP_ID 是 AGC 上的应用 ID,不是包名 String token = inst.getToken(APP_ID, "HCM"); // 主动删除 Token,用于用户退出登录 inst.deleteToken(APP_ID, "HCM");"HCM"这个固定值不是业务参数,别自己改。取 Token 需要网络且大概率要在子线程,第一次拿到可能失败,要带重试。
点击透传这块,通知消息和透传消息走的是两条路。通知消息由系统直接展示,点击后拉起应用,你自定义的键值放在启动 Intent 的 extras 里;透传消息走HmsMessageService.onMessageReceived,需要你自己决定展示方式。Unity 层要做的就是把这两条路上的参数统一成一个「路由 key → 页面」的映射,别在每个入口各写一遍解析。
通知栏的图标不要在 Unity 里配置,它由AndroidManifest.xml里的 meta-data 决定,配错的表现是通知栏显示一个纯白方块。
5. 避坑清单:6003、依赖冲突与「初始化成功但登录失败」
这一章全部是我和同事踩过的,按现象、原因、解决的顺序写,照着比对能省掉大半天。
5.1 指纹与签名不一致:6003 的三种伪装
现象一:调试包登录一直报 6003,换 release 包就正常。
原因:AGC 只填了 release 证书指纹,Unity 调试包用的是 debug keystore。
解决:把~/.android/debug.keystore的 SHA-256 也加进 AGC 的指纹列表,别嫌麻烦。
现象二:本地能跑,打给测试同事的包就 6003。
原因:包签名用的 keystore 和填进 AGC 的不是同一个。这种情况常见于 CI 与本地各有一套签名配置。
解决:在构建脚本里固定 keystore 路径和别名,并在出包日志里打印当前签名指纹,和 AGC 上填的做一次人工比对。
现象三:全部配置都对,清缓存后恢复正常。
原因:设备上残留了旧版本的应用数据和 HMS 侧的应用信息缓存。
解决:卸载重装一次,清理 HMS Core 的应用数据后再试。这类问题没什么道理可讲,但确实存在。
5.2 Gradle 依赖冲突与 AndroidX 的连带反应
现象:编译期报Duplicate class,或者运行时报NoClassDefFoundError。
原因:项目里同时存在新旧版本的同名 AAR,或者接入的其他三方 SDK 自带了一份旧版华为库。
解决:在Assets/Plugins/Android/下全局搜一遍同名 AAR,只留一份;Gradle 侧可以加一段强制版本,但更推荐从源头清理。
现象:开了 AndroidX 和 Jetifier 之后,编包变慢甚至失败。
原因:Jetifier 在转换部分 AAR 时会出现兼容问题。
解决:如果插件版本已支持 AndroidX,优先关掉 Jetifier 让 Gradle 直接走新依赖;不确定就先用一份最小工程验证,再合并进主工程。
现象:Main Gradle Template 配置完了,但依赖压根没进包。
原因:只改了mainTemplate.gradle,没勾选 Custom 模板,Unity 用的还是内置模板。
解决:确认 Player Settings 里模板勾选状态,构建后看gradleTemplate.properties是否生效。
5.3 设备与 HMS Core 版本的验证边界
现象:在一台非华为设备上初始化成功,登录却异常。
原因:登录依赖设备上的 HMS Core 服务,非华为设备的服务版本和可用性都存在差异。
解决:把华为真机作为登录、支付的唯一验收环境,其他机型只用来验证降级逻辑——比如登录不可用时是否能用游客模式继续玩。
现象:老机型上返回 907135003 一类的服务缺失错误。
原因:设备上的 HMS Core 版本过低,不满足当前 Kit 的最低要求。
解决:在代码里做一次版本判断,低于下限时引导用户更新,或直接隐藏对应入口,别让按钮点下去一片空白。
现象:模拟器上一切正常,真机上推送收不到。
原因:推送依赖设备与厂商通道,模拟器不具备完整的推送能力。
解决:推送相关的一切验证只在真机做,模拟器上跑通只能说明代码没崩。
6. 提审前最后一遍:日志开关与一张自检表
到这一步,功能基本跑通了,剩下的问题都在「别人机器上能不能复现」。我的习惯是出包前过一遍下面这张表,每一条都用构建产物验证,而不是看工程配置。
| 检查项 | 验证方式 | 通过标准 |
|---|---|---|
| 配置文件是否进包 | 解压 APK 看assets/agconnect-services.json | 文件存在且内容非空 |
| 签名指纹 | 出包日志打印 SHA-256,与 AGC 比对 | 完全一致 |
| 架构 | unzip -l看lib/arm64-v8a/ | 存在且不为空 |
| 初始化时序 | 冷启动首屏日志 | 初始化回调早于任何 Kit 调用 |
| 登录返回 | 真机登录一次看日志 | openId 非空,code 能换到 token |
| 支付补单 | 支付中杀进程再启动 | 订单状态能从后端查回 |
| 推送透传 | 后端下发一条自定义键 | 点击后跳到指定页面 |
日志开关也值得单独说一句。HMS 侧的详细日志默认不开,线上出问题时你只能看到一句「操作失败」。常见做法是在调试包里打开 SDK 的日志输出,同时给自己每个 Kit 调用点加统一前缀,方便一次性过滤:
// 统一日志前缀,方便 adb logcat 一次过滤 public static class HmsLog { private const string Tag = "HMS_TRACE"; public static void I(string step, string detail = "") => Debug.Log($"[{Tag}] {step} {detail}"); public static void E(string step, string detail = "") => Debug.LogError($"[{Tag}] {step} {detail}"); }配合adb logcat -s Unity就能把整个链路串起来看:初始化开始、初始化完成、拉起登录、拿到 code、发给后端、后端返回。这条日志链在线上排查时特别好用,尤其是用户反馈「点了没反应」的时候,你能立刻区分是没拉起、拉起了但回调没回来、还是回来了但业务层吞了。
我吃过一次亏:项目上线前所有验证都在自己的机器上做,自检表里「签名指纹」那一项心想肯定没问题就跳过了,结果提审版本的 keystore 是同事配的 CI 签名,和 AGC 上填的不一致,整个登录模块在审核环境里直接不可用。从那以后我每次出包都强制走一遍这张表,签名指纹这一项必须是打印出来肉眼看过的,不再靠「应该是对的」。希望帮到你。
本文还有配套的精品资源,点击获取