1. 项目概述:为什么Unity与Android的深度交互是个技术活
如果你做过Unity移动端开发,尤其是需要接入第三方SDK(比如登录、支付、广告、数据统计),那你肯定对“交互”这个词不陌生。常规的交互,比如调用一个Android方法获取个版本号,或者打开一个原生页面,用AndroidJavaClass和AndroidJavaObject基本就能搞定,网上教程一抓一大把。但一旦需求升级,变成“Android原生层发生某个事件(比如支付成功、广告加载完成、收到推送),需要实时、可靠地通知回Unity层”,很多开发者就开始头疼了。这不再是单向的“调用-返回”,而是需要建立一个双向的、基于事件的通信桥梁。
这就是“深度交互”的核心场景。而AndroidJavaProxy,正是Unity引擎为我们搭建这座桥梁提供的一个关键、且相对优雅的工具。它允许我们在C#脚本中定义一个接口,这个接口的实现可以被传递给Android的Java/Kotlin层。当Android那边有事情发生时,就可以直接回调到我们C#接口里定义的方法,就像在Unity里监听一个普通C#事件一样自然。
但理想很丰满,现实往往会在打包成aar(Android Archive)这个环节给你使绊子。直接写个Android插件放在Plugins/Android目录下,在Editor里测试回调可能一切正常。可一旦你把这个插件模块编译成独立的aar包,再让另一个Unity项目去依赖它,回调失效、空指针、类找不到这些“幽灵问题”就全来了。网上搜到的解决方案七零八落,很多只讲了AndroidJavaProxy怎么用,却没告诉你它在aar的上下文中有什么不同,更没提那些只有踩过坑才知道的配置细节。
所以,这篇内容我们就聚焦一个实战目标:构建一个基于AndroidJavaProxy的、可打包成独立aar、并能被其他Unity项目稳定集成的事件回调系统。我会把从原理设计、代码编写、aar打包、到Unity集成的完整链路,以及我趟过的所有坑,都掰开揉碎了讲清楚。
2. 核心原理与架构设计拆解
在动手写代码之前,我们必须把几个核心概念和它们之间的关系理清楚。这能帮你从根本上理解为什么有些做法行不通,而正确的路径又是什么。
2.1 AndroidJavaProxy 的本质:JNI桥接的代理模式
AndroidJavaProxy不是一个魔法黑盒。你可以把它理解为一个“约定”或“适配器”。在Java/Android的世界里,有一种常见的模式叫“接口”和“监听器”(或叫回调)。比如,你定义一个OnPaymentListener接口,里面有个onSuccess(String orderId)方法。你的支付模块会在完成后调用这个监听器。
Unity运行在C#环境,而Android SDK是Java/Kotlin环境,它们之间的通信靠的是JNI(Java Native Interface)。AndroidJavaProxy的作用,就是让我们在C#端创建一个对象,这个对象在JNI层“看起来”像是一个实现了某个特定Java接口的实例。
工作流程简化版:
- 你在C#中定义一个类,继承自
AndroidJavaProxy,并在构造函数中传入你要实现的Java接口的完整类名(如"com.example.sdk.PaymentListener")。 - 你在这个C#类中,编写与Java接口方法同名、同签名的C#方法。
- 当你将这个C#代理对象的实例(一个
IntPtr,代表JNI中的对象引用)通过AndroidJavaObject.Call等方法传递给Android端时。 - Android端拿到这个引用,可以把它当作真正的
PaymentListener接口对象来使用,调用其方法。 - 这个调用通过JNI层,被路由回你C#类中对应的那个方法,从而在Unity中触发逻辑。
关键在于,这个“路由”是由Unity引擎的运行时在底层管理的。这就要求Unity的运行时环境(即libunity.so)必须存在且正常工作。这也是为什么在非Unity环境(如纯Android App)中直接使用包含这种代理的aar会出问题。
2.2 aar包的角色与限制:不只是jar的压缩包
很多开发者认为aar就是一个带资源的jar包,这理解不够。aar是Android Library的发布格式,它包含了编译后的代码(classes.jar)、资源(res/)、清单文件(AndroidManifest.xml)、原生库(jni/)等。当你把Unity插件做成aar,意味着你希望它是一个独立的、可复用的Android模块。
但在Unity-Android交互的语境下,aar包有其特殊性:
- 运行时依赖:你的aar中的Java代码,其运行时环境是依附于宿主App的。在Unity游戏中,这个宿主就是UnityPlayerActivity。你的代码不能假设自己运行在一个标准的Android应用中。
- 类加载器:
AndroidJavaProxy创建的代理对象,与Unity引擎的类加载器紧密相关。当你的aar被另一个Unity项目引用时,必须确保这个代理类能被正确的类加载器找到。这常常需要通过UnityPlayer.currentActivity来获取当前活动的Context,进而获取到正确的ClassLoader。 - 初始化时机:Unity游戏的启动生命周期(
Awake->Start)与Android Activity的生命周期(onCreate->onResume)是交织的。你的回调系统必须在合适的时机(通常是Unity侧Start方法中,确保Activity已就绪)进行初始化绑定,过早会导致Android侧拿到空的或无效的代理引用。
2.3 事件回调系统的双向通信模型
我们需要设计一个清晰的双向模型:
- Unity (C#) 侧:作为事件消费者。它创建代理对象,并将其“设置”给Android模块。同时,它定义好当特定事件(如
onEventReceived)被触发时,自己要执行的逻辑(例如更新UI、发放游戏奖励)。 - Android (Java/Kotlin) 侧:作为事件生产者。它持有一个来自Unity的监听器引用。当内部业务逻辑完成(如网络请求返回、传感器数据到达、其他SDK回调触发),它就调用这个监听器的方法。
这个模型的关键在于“设置监听器”这个动作。Android侧必须提供一个公开的API(如setUnityEventListener),让Unity侧在初始化时能够将代理对象传递进去。这个API的设计必须考虑前面提到的类加载器和上下文问题。
3. 实战构建:从零创建带回调的Android Library
理论说再多不如动手。我们一步步来构建这个系统。我选择使用Android Studio和Kotlin,因为这是目前的主流,语法也更简洁。
3.1 创建Android Library模块
- 打开Android Studio,新建一个空项目(
File -> New -> New Project),模板选Empty Views Activity即可,语言选Kotlin。 - 项目创建好后,
File -> New -> New Module,选择Android Library。给它起个名,比如unity-bridge,包名可以设为com.yourcompany.unitybridge。确保最低API Level和你的Unity项目设置兼容(通常至少API 21)。 - 创建完成后,你会在项目视图中看到两个模块:
app(可忽略)和你的unity-bridge。
3.2 定义通信接口(Java/Kotlin侧)
在unity-bridge模块的src/main/java/...目录下,创建我们的核心接口。这个接口定义了Unity侧需要实现哪些回调方法。
IUnityEventCallback.kt
package com.yourcompany.unitybridge /** * 定义从Android向Unity发送事件的回调接口。 * 注意:方法名和参数列表必须与C#侧代理类中的方法严格匹配。 */ interface IUnityEventCallback { /** * 通用事件回调 * @param eventType 事件类型,用于区分不同业务,如 "PAY_SUCCESS", "AD_LOADED" * @param jsonData 事件数据,以JSON字符串形式传递复杂数据 */ fun onUnityEvent(eventType: String, jsonData: String) /** * 带错误信息的事件回调 */ fun onUnityEventWithError(eventType: String, errorCode: Int, errorMsg: String) // 你可以根据业务需要定义更多方法... }为什么用Kotlin接口而不是抽象类?接口更灵活,AndroidJavaProxy机制就是要求实现一个Java接口。Kotlin的接口完全兼容。方法参数使用String和基本类型(Int)是为了简化JNI类型映射,复杂数据用JSON字符串传递是通用且可靠的做法。
3.3 实现Android侧的桥接服务
我们需要一个单例或静态工具类来管理这个回调接口的实例,并提供给Android内部的其他组件调用。
UnityBridge.kt
package com.yourcompany.unitybridge import android.util.Log object UnityBridge { private const val TAG = "UnityBridge" // 持有Unity传过来的回调实例 private var eventCallback: IUnityEventCallback? = null /** * 由Unity侧在初始化时调用,设置回调监听器。 * 这是整个通信链路的关键入口。 * @param callback 来自Unity的IUnityEventCallback代理对象 */ @JvmStatic // 重要!让该方法在Java中显示为静态方法,便于Unity调用 fun setEventCallback(callback: IUnityEventCallback) { Log.d(TAG, "Unity event callback set.") this.eventCallback = callback } /** * 提供给Android内部其他组件触发事件的方法。 * 例如,你的支付模块成功后在内部调用此方法。 * @param eventType 事件类型 * @param data JSON格式的数据字符串 */ @JvmStatic fun sendEventToUnity(eventType: String, data: String) { Log.d(TAG, "Attempting to send event to Unity: $eventType, Data: $data") eventCallback?.onUnityEvent(eventType, data) ?: run { Log.w(TAG, "Event callback is null. Did Unity call setEventCallback?") } } @JvmStatic fun sendErrorEventToUnity(eventType: String, errorCode: Int, errorMsg: String) { eventCallback?.onUnityEventWithError(eventType, errorCode, errorMsg) } /** * 清理资源,避免内存泄漏。 * 可以在Unity的OnDestroy中调用对应的清理方法。 */ @JvmStatic fun release() { eventCallback = null Log.d(TAG, "UnityBridge released.") } }关键点解析:
@JvmStatic注解:这是让Kotlin中的方法在生成的Java字节码中成为静态方法的关键。Unity的AndroidJavaClass调用静态方法更直接、更常见。没有这个注解,Unity侧调用会非常麻烦。- 空安全判断:
eventCallback?.onUnityEvent(...) ?: run { ... }。这是Kotlin的优雅之处,安全地处理回调可能未设置的情况,并打日志提示,这对调试至关重要。 - 日志:在关键节点添加
Log.d。当集成出现问题时,通过adb logcat查看这些日志是定位问题最快的方式。
3.4 构建与生成aar包
- 在Android Studio右侧的
Gradle面板中,找到你的unity-bridge模块,展开Tasks -> build,双击assemble或bundle。 - 构建成功后,aar文件会生成在
unity-bridge/build/outputs/aar/目录下,通常有debug和release两个版本。用于发布时请使用release版本(如unity-bridge-release.aar)。 - 这个aar文件现在包含了我们定义的接口和桥接类。
注意:此时生成的aar是一个“纯净”的Android库。它还不知道任何关于Unity的事情。Unity相关的交互逻辑,将在Unity侧的C#脚本中实现。这种分离关注点的设计,使得这个aar理论上也可以被其他非Unity的Android项目使用(尽管它们可能不会设置那个回调)。
4. Unity侧C#代理与集成管理
现在切换到Unity项目。我们需要在Unity中创建对应的C#脚本,来实现Android端的接口并完成初始化。
4.1 创建AndroidJavaProxy派生类
在Unity项目的Assets/Scripts(或任何你喜欢的目录)下创建C#脚本。
UnityEventCallbackProxy.cs
using UnityEngine; /// <summary> /// 实现Android侧IUnityEventCallback接口的代理类。 /// 方法签名(名称、参数类型、返回类型)必须与Kotlin接口严格一致。 /// </summary> public class UnityEventCallbackProxy : AndroidJavaProxy { // 定义事件委托,用于在Unity内部解耦 public delegate void UnityEventDelegate(string eventType, string jsonData); public delegate void UnityErrorEventDelegate(string eventType, int errorCode, string errorMsg); public static event UnityEventDelegate OnEventReceived; public static event UnityErrorEventDelegate OnErrorEventReceived; // 构造函数中传入Android接口的完整类名 public UnityEventCallbackProxy() : base("com.yourcompany.unitybridge.IUnityEventCallback") { Debug.Log("[Unity] UnityEventCallbackProxy constructed."); } // 必须与IUnityEventCallback.onUnityEvent方法匹配 public void onUnityEvent(string eventType, string jsonData) { Debug.Log($"[Unity] Event received from Android: Type={eventType}, Data={jsonData}"); // 切换到Unity的主线程执行事件派发,确保UI操作安全 MainThreadDispatcher.ExecuteOnMainThread(() => { OnEventReceived?.Invoke(eventType, jsonData); }); } // 必须与IUnityEventCallback.onUnityEventWithError方法匹配 public void onUnityEventWithError(string eventType, int errorCode, string errorMsg) { Debug.Log($"[Unity] Error event from Android: Type={eventType}, Code={errorCode}, Msg={errorMsg}"); MainThreadDispatcher.ExecuteOnMainThread(() => { OnErrorEventReceived?.Invoke(eventType, errorCode, errorMsg); }); } }核心要点与避坑指南:
- 基类构造函数:
base("完整.接口.类名")。这个字符串必须100%准确,包括包名。大小写敏感。这是代理类与Android接口绑定的唯一标识。 - 方法签名一致性:
public void onUnityEvent(string eventType, string jsonData)。方法名、参数数量、参数类型、返回类型必须与Kotlin接口完全一致。C#方法名默认是小写字母开头,而Kotlin/Java方法在接口定义中也是小写开头,所以这里直接用小写。如果不一致,回调将无法触发,且不会报错,这是最隐蔽的坑。 - 主线程安全:Android的回调通常发生在非Unity主线程(JNI线程)。直接在这些回调方法里操作
GameObject、Transform或UI可能会引发异常。使用一个主线程调度器(MainThreadDispatcher)将事件排队到主线程执行是必备操作。下面是一个简单的实现示例。
MainThreadDispatcher.cs(简化版)
using System.Collections.Generic; using UnityEngine; public class MainThreadDispatcher : MonoBehaviour { private static readonly Queue<System.Action> _executionQueue = new Queue<System.Action>(); private static MainThreadDispatcher _instance; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { if (_instance == null) { GameObject obj = new GameObject("MainThreadDispatcher"); _instance = obj.AddComponent<MainThreadDispatcher>(); DontDestroyOnLoad(obj); } } public static void ExecuteOnMainThread(System.Action action) { lock (_executionQueue) { _executionQueue.Enqueue(action); } } void Update() { lock (_executionQueue) { while (_executionQueue.Count > 0) { _executionQueue.Dequeue()?.Invoke(); } } } }4.2 编写Unity侧桥接管理器
这个管理器负责初始化通信:创建代理实例,并将其传递给Android侧的UnityBridge.setEventCallback方法。
AndroidBridgeManager.cs
using UnityEngine; public class AndroidBridgeManager : MonoBehaviour { private AndroidJavaObject _unityBridgeJavaClass; private UnityEventCallbackProxy _callbackProxy; void Start() { InitializeAndroidBridge(); } void InitializeAndroidBridge() { Debug.Log("[Unity] Initializing Android Bridge..."); try { // 1. 创建代理实例 _callbackProxy = new UnityEventCallbackProxy(); // 2. 获取Android侧的UnityBridge类 // 使用AndroidJavaClass来访问静态方法和字段 using (AndroidJavaClass jc = new AndroidJavaClass("com.yourcompany.unitybridge.UnityBridge")) { if (jc == null) { Debug.LogError("[Unity] Failed to find Java class: com.yourcompany.unitybridge.UnityBridge"); return; } // 3. 调用静态方法,将代理对象传过去 jc.CallStatic("setEventCallback", _callbackProxy); Debug.Log("[Unity] UnityBridge.setEventCallback called successfully."); } // 4. 订阅事件(示例:在某个UI管理器或游戏逻辑管理器里订阅) UnityEventCallbackProxy.OnEventReceived += HandleUnityEvent; UnityEventCallbackProxy.OnErrorEventReceived += HandleUnityErrorEvent; // 5. 测试:模拟从Unity触发一个事件到Android(可选,用于双向测试) // SendTestEventToAndroid(); } catch (System.Exception e) { Debug.LogError($"[Unity] Exception during bridge initialization: {e.Message}\n{e.StackTrace}"); } } private void HandleUnityEvent(string eventType, string jsonData) { // 在这里处理具体业务逻辑 Debug.Log($"[Unity] Handling event: {eventType}. Data: {jsonData}"); // 例如: if (eventType == "PAY_SUCCESS") { // 解析jsonData,发放游戏货币... } else if (eventType == "AD_REWARD_GRANTED") { // 给予玩家奖励... } } private void HandleUnityErrorEvent(string eventType, int errorCode, string errorMsg) { Debug.LogError($"[Unity] Error from Android - Type:{eventType}, Code:{errorCode}, Msg:{errorMsg}"); // 显示错误提示给玩家... } // 示例:从Unity主动调用Android方法 public void SendTestEventToAndroid() { try { using (AndroidJavaClass jc = new AndroidJavaClass("com.yourcompany.unitybridge.UnityBridge")) { // 假设我们在Android侧也提供了一个供Unity调用的测试方法 jc.CallStatic("simulateEventFromUnity", "TEST_EVENT", "{\"msg\":\"Hello from Unity!\"}"); } } catch (System.Exception e) { Debug.LogError($"[Unity] Failed to send test event: {e.Message}"); } } void OnDestroy() { // 清理,避免内存泄漏 if (_callbackProxy != null) { UnityEventCallbackProxy.OnEventReceived -= HandleUnityEvent; UnityEventCallbackProxy.OnErrorEventReceived -= HandleUnityErrorEvent; try { using (AndroidJavaClass jc = new AndroidJavaClass("com.yourcompany.unitybridge.UnityBridge")) { jc.CallStatic("release"); } } catch { /* 忽略清理时的异常 */ } } Debug.Log("[Unity] AndroidBridgeManager destroyed."); } }初始化流程详解:
- 时机:在
Start()中初始化,确保Unity环境完全就绪。不要在Awake()中做,因为此时原生的Activity可能还未完全创建好。 AndroidJavaClass与AndroidJavaObject:这里我们只调用静态方法,所以使用AndroidJavaClass。如果要操作一个Java对象实例,才需要AndroidJavaObject。CallStatic方法:第一个参数是方法名(字符串),后面是可变参数,对应Java方法的参数。我们将C#的_callbackProxy对象传递过去。Unity引擎在底层会处理这个代理对象的JNI传递。- 异常捕获:整个初始化过程必须用
try-catch包裹。JNI调用可能因为类找不到、方法签名不匹配、Android环境未就绪等多种原因失败,详细的日志是调试的生命线。 - 资源管理:
using语句用于包裹AndroidJavaClass和AndroidJavaObject,确保其底层的JNI引用被及时释放,这是一种好习惯。在OnDestroy中清理事件订阅和调用Android侧的release方法,有助于预防内存泄漏。
4.3 在Unity中集成aar包
- 将之前生成的
unity-bridge-release.aar文件复制到Unity项目的Assets/Plugins/Android目录下。如果目录不存在就创建它。 - 关键步骤:检查或创建
Assets/Plugins/Android/mainTemplate.gradle文件(对于Unity 2019.3+的Gradle构建系统)。你需要在这个文件中声明对这个aar的依赖。因为我们的aar是本地文件,需要特殊处理。- 在Unity编辑器中,打开
Project Settings -> Player -> Android -> Publishing Settings,勾选Custom Main Gradle Template和Custom Gradle Properties Template。这会在Assets/Plugins/Android下生成模板文件。 - 打开生成的
mainTemplate.gradle,在dependencies块内添加:dependencies { // ... Unity自动生成的其他依赖 implementation files('libs/unity-bridge-release.aar') // 如果你的aar直接放在Plugins/Android下 // 或者,如果你在Plugins/Android下创建了libs子文件夹,则用: // implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar']) } - 确保你的aar文件在正确的路径。更规范的做法是在
Plugins/Android下创建一个libs文件夹,把aar放进去,然后使用fileTree的方式引入。
- 在Unity编辑器中,打开
- 如果你需要这个aar依赖其他第三方库(如Gson、OkHttp等),你同样需要在
mainTemplate.gradle的dependencies块中声明它们,例如implementation 'com.google.code.gson:gson:2.10.1'。切记:避免不同库的版本冲突。
5. 打包、测试与深度问题排查
集成完成,代码写完,最激动人心也最容易崩溃的环节来了:打包测试。
5.1 构建APK与真机调试
- 在Unity中,
File -> Build Settings,选择Android平台,切换过去。 - 使用
Development Build并勾选Autoconnect Profiler和Deep Profiling(初期调试建议勾选)。 - 连接你的Android测试手机,开启USB调试。
- 点击
Build And Run。第一次构建可能会比较慢,因为Gradle需要解析依赖。
5.2 调试与日志查看
这是定位问题的核心手段。
- Unity日志:在代码中关键位置添加
Debug.Log。在手机上运行游戏,通过adb logcat -s Unity命令可以过滤查看Unity输出的日志。这能帮你确认C#侧的初始化是否成功,代理方法是否被调用。 - Android日志:我们在
UnityBridge.kt中添加了Log.d(TAG, ...)。使用adb logcat -s UnityBridge来查看。这能帮你确认Android侧是否收到了Unity设置的callback,以及sendEventToUnity是否被正确执行。 - JNI/崩溃日志:如果发生崩溃,使用
adb logcat *:E查看所有错误信息,或者直接adb logcat查看完整日志,搜索AndroidRuntime、FATAL EXCEPTION、JNI DETECTED ERROR等关键词。
5.3 常见问题与解决方案实录
以下是我在多个项目中趟过的坑,以及最终的解决方案。
问题1:回调完全没触发,Android日志显示“Event callback is null.”
- 现象:Unity游戏启动了,Android日志也打印了,但就是收不到回调。
sendEventToUnity里的警告日志出现了。 - 排查:
- 检查Unity日志,确认
InitializeAndroidBridge方法被调用,且jc.CallStatic("setEventCallback", _callbackProxy)这行没有抛出异常。 - 检查Android日志,确认
setEventCallback方法内的Log.d是否打印。如果没打印,说明Unity的调用根本没到Java层。
- 检查Unity日志,确认
- 可能原因与解决:
- 类名错误:
AndroidJavaClass构造时传入的类名字符串有误。检查包名、类名是否与aar中完全一致。注意:Kotlin的object类在Java中会生成一个名为UnityBridge.INSTANCE的静态实例,但我们用了@JvmStatic,所以直接调用静态方法即可,类名就是com.yourcompany.unitybridge.UnityBridge。 - 方法签名不匹配:
CallStatic的第一个参数是方法名,必须与Kotlin中使用@JvmStatic注解的方法名一致。大小写敏感。 - 构建依赖问题:aar没有正确打入APK。检查
mainTemplate.gradle的配置,检查构建日志中是否有关于unity-bridge库的警告或错误。最直接的方法是解压生成的APK文件,查看libs或classes.dex相关的路径下是否有你的库文件。 - 初始化时机过早:确保在
Start()中初始化,而不是Awake()。可以尝试延迟初始化,比如用Invoke(nameof(InitializeAndroidBridge), 0.5f)。
- 类名错误:
问题2:回调触发了,但Unity侧的代理方法没执行,或者参数是错的
- 现象:Android日志显示
sendEventToUnity被调用且没有打印null警告,但Unity中onUnityEvent方法里的Debug.Log没看到。 - 排查:
- 在Unity代理类的构造函数和
onUnityEvent方法第一行都加上Debug.Log,确认对象是否创建、方法是否被JNI调用。 - 核对C#代理类方法签名与Java接口方法签名100%一致,包括方法名、参数类型、参数顺序、返回类型(都是
void)。
- 在Unity代理类的构造函数和
- 可能原因与解决:
- 方法名大小写:这是最常见的坑。Java/Kotlin接口方法通常是
camelCase(首字母小写)。确保C#方法名也是完全相同的小写开头。不要因为C#的惯例是PascalCase就写成OnUnityEvent。 - 参数类型不匹配:Java的
int对应C#的int,String对应string,boolean对应bool。确保完全对应。使用JSON字符串传递复杂对象是最稳妥的。 - JNI线程问题:代理方法被调用了,但里面的逻辑(比如直接设置UI文本)崩溃了,导致日志没打印完。检查是否所有Unity引擎对象的操作都放在了主线程(通过
MainThreadDispatcher)。
- 方法名大小写:这是最常见的坑。Java/Kotlin接口方法通常是
问题3:打包成aar后,在另一个Unity项目中集成,回调失效
- 现象:在开发项目(Android Library模块直接引用)中工作正常,但将模块打包成aar,导入到一个新的干净Unity项目后,回调不工作了。
- 排查:
- 检查新项目的
Plugins/Android结构,aar放置位置,mainTemplate.gradle依赖声明。 - 对比两个项目的
Player Settings,特别是Minimum API Level、Target API Level以及Scripting Backend(IL2CPP vs Mono)。IL2CPP可能会带来额外的JNI交互挑战。
- 检查新项目的
- 可能原因与解决:
- ProGuard/R8混淆:如果你在Android Library的
build.gradle中开启了minifyEnabled true,代码会被混淆。这会导致Unity通过反射找不到正确的类和方法名。解决方案:在library的proguard-rules.pro中添加规则,保持通信接口和桥接类不被混淆。-keep class com.yourcompany.unitybridge.** { *; } -keep interface com.yourcompany.unitybridge.** { *; } - IL2CPP Stripping:Unity的IL2CPP代码裁剪可能会移除“未使用”的代码。如果你的C#代理类只在Android侧通过JNI反射调用,IL2CPP可能认为它未被使用而将其剥离。解决方案:在
Assets目录下创建link.xml文件,告诉链接器保留这些类。<linker> <assembly fullname="Assembly-CSharp" preserve="all"/> <!-- 保留整个程序集,比较粗暴 --> <!-- 或者更精确地 --> <assembly fullname="Assembly-CSharp"> <type fullname="UnityEventCallbackProxy" preserve="all"/> <type fullname="AndroidBridgeManager" preserve="all"/> </assembly> </linker>
- ProGuard/R8混淆:如果你在Android Library的
问题4:在非Unity环境(如原生Android App)中调用aar方法崩溃
- 现象:你的aar设计是希望通用,但当它在纯Android App中被调用
UnityBridge.sendEventToUnity时,可能因为eventCallback是AndroidJavaProxy对象而崩溃。 - 解决:这是设计预期。在你的Android库代码中,可以进行防御性判断。修改
UnityBridge.kt的sendEventToUnity方法:
更健壮的设计是,在接口或回调设置时,传递一个环境标识。fun sendEventToUnity(eventType: String, data: String) { val callback = eventCallback if (callback != null) { // 检查callback是否是来自Unity的代理(这是一个简化判断,实际可能需要更复杂的标记) // 一种常见做法是:在setEventCallback时,设置一个标志位。 Log.d(TAG, "Sending event to Unity callback.") callback.onUnityEvent(eventType, data) } else { Log.w(TAG, "Event callback is null. This might be a non-Unity environment or not initialized.") // 可以选择将事件缓存起来,或者忽略,或者通过其他方式通知 } }
5.4 性能与最佳实践心得
- 减少JNI调用频率:每次
AndroidJavaObject.Call或AndroidJavaClass.CallStatic都是一次JNI边界穿越,有开销。避免在每帧的Update中频繁调用。对于需要频繁传递的数据(如传感器信息),考虑在Android端缓存并定时批量发送,或使用更高效的通信方式(如Unity的UnitySendMessage,但它更不灵活)。 - 数据序列化:使用JSON(如
UnityEngine.JsonUtility或第三方库如Newtonsoft.Json)在复杂对象和字符串间转换。协议缓冲区(Protobuf)是更高效的选择,但引入复杂度。简单类型(int, float, bool, string)直接传递即可。 - 错误处理标准化:定义好错误码和错误信息格式,在
IUnityEventCallback接口中提供专门的错误回调方法,便于Unity侧统一处理。 - 生命周期管理:务必在Unity的
OnDestroy或OnApplicationPause中清理Android侧的引用和资源,并在恢复时重新初始化,防止内存泄漏和状态不一致。 - aar版本管理:给你的aar库定义版本号(在library的
build.gradle中修改versionCode和versionName),并在Unity项目中通过文档或脚本说明依赖的版本,避免升级冲突。
构建一个稳定的Unity-Android双向事件回调系统,就像在两者之间架设一座精心设计的桥梁。AndroidJavaProxy是桥墩,清晰的接口定义是桥面,而细致的错误处理和生命周期管理则是护栏。把本文中的原理吃透,代码流程走通,再结合你具体的业务逻辑加以调整,就能打造出足以支撑复杂功能交互的通信基石。