1. 项目概述为什么Unity与Android交互是移动开发的必修课如果你是一名Unity开发者并且你的项目最终需要发布到Android平台那么“Unity与Android交互”这个课题你迟早会碰到而且会反复碰到。这绝不是一个锦上添花的功能而是打通Unity虚拟世界与Android原生硬件、系统服务之间壁垒的核心通道。我见过太多项目前期在Unity编辑器里跑得飞快各种特效酷炫但一到真机测试就卡在了调用系统相册、读写外部存储、集成第三方SDK比如登录、支付、广告、获取设备唯一标识、处理后台服务等环节。这些功能Unity引擎本身并不直接提供或者提供的方案不够灵活、性能不佳。这时你就必须亲自下场去和Android的Java/Kotlin世界打交道。简单来说Unity负责渲染游戏画面和处理游戏逻辑而Android系统则掌管着手机硬件、网络、存储、通知等所有底层资源。Unity与Android交互就是在这两者之间搭建一座稳固的桥梁。这座桥建得好你的应用就能如鱼得水充分利用设备能力建得不好轻则功能缺失、体验割裂重则崩溃频发、性能低下。本次实战指南的目的就是带你从零开始亲手搭建这座桥并深入理解其中的每一个螺栓和接口让你不仅能实现功能更能规避深坑写出健壮、高效的交互代码。无论你是想实现一个简单的Toast提示还是集成复杂的语音识别SDK其底层原理和核心步骤都是相通的。2. 交互原理深度剖析Unity与Android如何“对话”在开始写代码之前我们必须先搞清楚UnityC#和AndroidJava/Kotlin这两个截然不同的环境是如何进行通信的。这并非魔法而是基于一套明确的规则主要分为两大方向Unity调用AndroidC# - Java和Android调用UnityJava - C#。2.1 Unity调用Android跨越虚拟机的函数调用Unity运行在Mono或IL2CPP虚拟机之上而Android原生代码运行在ART/Dalvik虚拟机中。Unity通过一套名为AndroidJavaClass和AndroidJavaObject的API利用Java原生接口JNI来实现对Android层代码的调用。你可以把它们理解为C#端的“代理”或“中介”。核心类解析AndroidJavaClass用于代表一个Java类。当你需要调用静态方法或访问静态字段时就使用它。AndroidJavaObject用于代表一个Java类的实例对象。当你需要创建对象实例并调用其非静态实例方法时就使用它。其工作流程可以简化为定位目标在C#中通过完整的Java类名包名类名创建一个AndroidJavaClass或AndroidJavaObject。这就像拿到了对方公司的总机号码和部门分机号。发起呼叫使用CallStatic调用静态方法、GetStatic获取静态字段、Call调用实例方法、Get获取实例字段等方法并传入相应的参数。协议转换Unity底层IL2CPP运行时通过JNI将C#的数据类型如int,string,bool转换为Java的数据类型如int,java.lang.String,boolean并执行真正的Java方法调用。返回结果Java方法的返回值再通过JNI转换回C#对应的数据类型。注意这个过程的性能开销相对较大因为涉及多次虚拟机间的上下文切换和数据编组Marshalling。因此切忌在每帧更新的Update()方法中进行高频的Android交互调用这会导致严重的性能问题。正确的做法是将调用频率降至最低或通过缓存AndroidJavaObject实例来复用。2.2 Android调用Unity基于消息的回调机制反过来当Android原生代码需要通知Unity某些事件如按钮点击、传感器数据到达、后台服务完成时就需要Android来调用Unity。Unity为此提供了一种基于“消息发送”的机制。核心组件UnityPlayer与UnitySendMessage在Android项目中Unity会生成一个名为UnityPlayer的Activity默认是UnityPlayerActivity。这个Activity承载着Unity的渲染视图。同时Unity提供了一个原生接口函数UnitySendMessage。其工作流程如下持有引用在Unity调用Android时可以将一个GameObject的名字和方法名作为参数传递给Android端。Android端需要保存这些信息。事件触发当Android端特定事件发生时如收到推送、完成支付它获取当前活动的UnityPlayer实例。发送消息Android端调用UnitySendMessage(String gameObjectName, String methodName, String message)方法。gameObjectName: Unity场景中某个GameObject的名称。methodName: 该GameObject上挂载的脚本中的一个公有方法名。message: 一个字符串类型的参数用于传递信息。Unity响应Unity运行时在指定的GameObject上查找并调用对应的方法从而实现回调。重要限制UnitySendMessage只能传递一个String类型的参数。如果需要传递复杂数据通常需要将其序列化为JSON字符串在Unity端再反序列化。此外这个方法本身是异步的且有一定的性能开销。2.3 交互方式选型与对比除了上述基础的原生JNI交互在实际开发中我们还会根据场景选择不同的辅助方式交互方式原理简述优点缺点适用场景原生JNI交互(AndroidJavaClass/Object)通过Unity提供的C# API直接调用JNI。最灵活、最直接能调用任何Java方法。代码繁琐类型转换需小心性能开销需注意。所有需要深度定制和复杂参数传递的场景。AndroidJavaProxy一个C#类用于实现Java接口允许Java端回调C#。比UnitySendMessage更类型安全可以定义复杂回调接口。设置稍复杂需要定义对应的Java接口。需要处理Java端事件监听器如按钮点击监听器的场景。UnitySendMessageAndroid端通过Unity提供的原生函数发送消息。使用简单Android端调用方便。只能传一个字符串参数性能一般强依赖于GameObject名称。简单的通知、回调如“支付成功”、“广告加载完毕”。Unity Jar/AAR 插件将Android原生代码打包成.jar或.aar文件放入Unity项目的Plugins/Android目录。代码封装性好便于复用和分发可以包含资源文件.aar。需要配置Android项目对不熟悉Android开发的Unity程序员有一定门槛。集成第三方SDK如微信登录、穿山甲广告、封装复杂原生功能模块。实操心得对于初学者建议从UnitySendMessage和简单的AndroidJavaClass调用开始建立直观感受。但在正式项目中对于复杂的、需要频繁交互的功能强烈推荐使用封装成AAR插件的方式。它将Android端的复杂性封装起来向Unity暴露清晰、简单的C#接口大大降低了耦合度和出错概率也是行业内的标准做法。3. 开发环境与项目结构搭建工欲善其事必先利其器。一个清晰的项目结构能让你在后续的开发和调试中事半功倍。3.1 环境准备清单Unity Hub Unity Editor建议使用稳定的LTS版本如2022.3 LTS。确保在安装时勾选了Android Build Support模块包括Android SDK NDK Tools。Android Studio主要用于开发、编译和调试Android原生模块即我们的AAR插件。它自带了SDK Manager方便我们管理Android SDK。JDK (Java Development Kit)Unity和Android Studio都需要。推荐使用OpenJDK 11或17并与Android Studio的JDK设置保持一致。关键路径配置Unity中打开Edit - Preferences - External Tools。Android SDK指向Android Studio安装目录下的sdk文件夹例如C:\Users\YourName\AppData\Local\Android\Sdk。JDK指向你的JDK安装目录例如C:\Program Files\Microsoft\jdk-11.0.xx。NDK可选但推荐Unity通常自带NDK但如果你需要特定版本可以在这里指定。3.2 创建标准的Unity-Android混合项目结构一个易于维护的项目应该将Unity代码和Android原生代码分离。我推荐以下结构YourUnityProject/ ├── Assets/ │ ├── Plugins/ │ │ └── Android/ # 存放所有Android相关插件 │ │ ├── YourPlugin.aar # 编译好的AAR文件 │ │ ├── AndroidManifest.xml # 插件的清单文件可合并 │ │ └── res/ # 插件的资源文件如图标 │ └── Scripts/ │ └── NativeBridge.cs # 供游戏逻辑调用的C#桥接脚本 ├── YourAndroidModule/ # 项目外部Android Studio模块目录 │ ├── app/ │ │ ├── libs/ # 可能依赖的第三方jar/aar │ │ ├── src/ │ │ │ └── main/ │ │ │ ├── java/com/yourcompany/plugin/ # Java源码 │ │ │ ├── res/ # 资源 │ │ │ └── AndroidManifest.xml │ │ └── build.gradle # 模块构建配置 │ └── build.gradle, settings.gradle ... └── ...操作步骤在Unity中创建新项目。在项目外部使用Android Studio创建一个新的Empty Activity项目但这不是为了生成APK而是为了获得一个标准的Android模块app模块。我们后续会将这个模块编译成AAR。在Unity项目的Assets目录下手动创建Plugins/Android文件夹。这个文件夹对Unity有特殊意义打包时其中的内容会被合并到最终的Android工程中。3.3 创建第一个Android原生模块AAR我们的目标是制作一个最简单的插件在Android端显示一个Toast消息并由Unity触发。在Android Studio中操作在app/src/main/java/下创建包例如com.yourcompany.unityplugin。在该包下创建Java类ToastHelper.java。package com.yourcompany.unityplugin; import android.app.Activity; import android.content.Context; import android.widget.Toast; import com.unity3d.player.UnityPlayer; public class ToastHelper { // 保存当前Unity活动的上下文 private static Activity currentActivity; // Unity在初始化时应调用此方法传入当前Activity public static void setActivity(Activity activity) { currentActivity activity; } // 供Unity调用的静态方法显示Toast public static void showToast(final String message) { if (currentActivity null) { // 如果未设置尝试获取Unity的当前Activity更稳健的做法 currentActivity UnityPlayer.currentActivity; } if (currentActivity ! null) { // 必须在UI线程运行Toast currentActivity.runOnUiThread(new Runnable() { Override public void run() { Toast.makeText(currentActivity, message, Toast.LENGTH_LONG).show(); } }); } } // 一个示例方法用于演示Android如何回调Unity public static void triggerUnityCallback(String gameObjectName, String callbackMethod) { // 使用UnitySendMessage通知Unity UnityPlayer.UnitySendMessage(gameObjectName, callbackMethod, Hello from Android!); } }配置app模块的build.gradle确保其能编译成AAR。默认情况下Android Studio的application插件生成的是APK。我们需要将其改为library插件。// 将这一行 plugins { id com.android.application } // 改为 plugins { id com.android.library } // 同时注释掉或删除 applicationId 这一行 android { ... defaultConfig { // applicationId com.yourcompany.yourapp // 注释掉这行 ... } ... }点击Android Studio右侧Gradle面板 -YourModule-Tasks-build- 双击assembleRelease或assembleDebug。编译完成后在app/build/outputs/aar/目录下可以找到app-release.aar文件。3.4 在Unity中集成与调用导入AAR将生成的app-release.aar文件复制到Unity项目的Assets/Plugins/Android目录下。你可以将其重命名为更有意义的名称如NativeToastPlugin.aar。创建C#桥接脚本在Unity中创建Assets/Scripts/NativeBridge.cs。using UnityEngine; public class NativeBridge : MonoBehaviour { // 定义Android端的类名包名类名 private const string ToastHelperClassName com.yourcompany.unityplugin.ToastHelper; void Start() { // 可选初始化设置Activity。对于简单的静态方法调用有时可以省略。 // 但对于需要Context/Activity的方法最好在Unity启动时设置。 using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (AndroidJavaClass toastHelperClass new AndroidJavaClass(ToastHelperClassName)) { toastHelperClass.CallStatic(setActivity, currentActivity); } } // 供Unity按钮调用的公共方法 public void ShowNativeToast(string message) { // 使用AndroidJavaClass调用静态方法 using (AndroidJavaClass toastHelperClass new AndroidJavaClass(ToastHelperClassName)) { toastHelperClass.CallStatic(showToast, message); } } // 测试从Android回调Unity的方法 public void OnAndroidCallback(string message) { Debug.Log($收到来自Android的回调: {message}); // 在这里处理回调逻辑例如更新UI } // 触发Android端发起回调 public void TriggerCallbackFromUnity() { using (AndroidJavaClass toastHelperClass new AndroidJavaClass(ToastHelperClassName)) { // 传入接收回调的GameObject名和本脚本的方法名 toastHelperClass.CallStatic(triggerUnityCallback, this.gameObject.name, OnAndroidCallback); } } }场景测试在场景中创建一个空GameObject命名为Bridge并将NativeBridge脚本挂载上去。创建一个UI按钮在其OnClick事件中拖入Bridge对象选择方法NativeBridge.ShowNativeToast并在输入框中填入Hello Android!。创建另一个按钮调用NativeBridge.TriggerCallbackFromUnity。连接Android真机确保USB调试已打开在Unity中切换平台为Android然后点击Build And Run。如果一切顺利点击第一个按钮你将在手机上看到Android原生的Toast提示。点击第二个按钮你将在Unity的Console窗口中看到收到来自Android的回调: Hello from Android!的日志。至此一个双向交互的闭环就完成了。4. 核心交互场景实战与避坑指南掌握了基础流程后我们来看几个在移动游戏和应用中极其常见且容易出错的交互场景。每一个场景我都会给出实现方案和必须注意的“坑”。4.1 场景一获取与存储——访问Android外部存储Unity的Application.persistentDataPath在Android上对应的是内部存储的私有目录无需权限。但如果需要访问公共目录如下载文件夹、相册或读取其他应用的数据就需要处理Android的运行时权限和存储访问框架SAF。需求让用户选择一张手机相册中的图片并在Unity中显示。Android端实现Java 我们需要启动一个系统Intent来打开图片选择器。// 在之前的ToastHelper类中新增方法 public static void pickImageFromGallery(Activity activity, int requestCode) { Intent intent new Intent(Intent.ACTION_PICK); intent.setType(image/*); activity.startActivityForResult(intent, requestCode); } // 在UnityPlayerActivity或你的自定义Activity中重写onActivityResult Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode Activity.RESULT_OK requestCode YOUR_REQUEST_CODE) { Uri selectedImageUri data.getData(); // 将图片Uri的字符串形式发送回Unity String uriString selectedImageUri.toString(); UnityPlayer.UnitySendMessage(YourGameObject, OnImagePicked, uriString); } }Unity端实现C#public void PickImage() { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (AndroidJavaClass pluginClass new AndroidJavaClass(com.yourcompany.unityplugin.ImagePicker)) { pluginClass.CallStatic(pickImageFromGallery, currentActivity, 1001); } } // 回调方法 public void OnImagePicked(string imageUriString) { Debug.Log($选中图片URI: {imageUriString}); // 注意直接使用这个URIUnity的WWW或UnityWebRequest可能无法直接读取。 // 需要将URI转换为实际的文件路径或使用Android端的辅助方法将图片字节流传递给Unity。 // 更稳健的做法是在Android端将图片读取为字节数组通过Base64编码成字符串传给Unity或写入临时文件后传递文件路径。 StartCoroutine(LoadImageCoroutine(imageUriString)); } IEnumerator LoadImageCoroutine(string uriString) { // 这是一个简化示例。实际中对于content://类型的URI需要使用Android端的ContentResolver来读取。 // 建议在Android端完成图片加载和缩放然后将字节或Texture2D传递回来。 // 这里仅作流程演示。 yield return null; }避坑指南权限问题从Android 10 (API 29) 开始作用域存储Scoped Storage被强制执行。直接通过文件路径访问公共目录变得困难。ACTION_PICKIntent是推荐方式它不需要READ_EXTERNAL_STORAGE权限。URI处理Intent.ACTION_PICK返回的是一个content://URI而不是文件路径。Unity的传统文件读取API无法直接处理。最佳实践是在Android端使用ContentResolver.openInputStream(uri)读取图片流将其转换为字节数组然后通过JNI传递给Unity或者保存到应用的私有缓存目录后再将文件路径传回。主线程所有与UI相关的操作如启动Activity必须在主线程执行。我们的Java代码通过activity.runOnUiThread或由Unity在主线程发起调用确保了这一点。请求码startActivityForResult的请求码需要在Android和Unity间约定一致用于区分不同的Intent返回结果。4.2 场景二后台服务与通知——维持应用的生命力游戏可能需要在后台下载资源、播放音乐或接收网络消息。这就需要用到Android的Service。需求创建一个后台服务每隔一段时间向Unity发送一个计数。Android端实现创建Service类MyBackgroundService.javapublic class MyBackgroundService extends Service { private int counter 0; private Handler handler; private Runnable runnable; Override public void onCreate() { super.onCreate(); handler new Handler(Looper.getMainLooper()); runnable new Runnable() { Override public void run() { counter; // 回调Unity UnityPlayer.UnitySendMessage(ServiceManager, OnServiceTick, String.valueOf(counter)); // 发送一个前台通知Android 8.0必需否则服务容易被杀死 showForegroundNotification(); // 10秒后再次执行 handler.postDelayed(this, 10000); } }; handler.postDelayed(runnable, 10000); } private void showForegroundNotification() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel(service_channel, Background Service, NotificationManager.IMPORTANCE_LOW); getSystemService(NotificationManager.class).createNotificationChannel(channel); Notification notification new Notification.Builder(this, service_channel) .setContentTitle(Unity后台服务) .setContentText(正在运行...计数 counter) .setSmallIcon(R.drawable.app_icon) // 需要图标资源 .build(); startForeground(1, notification); } } Override public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; // 服务被杀死后尝试重启 } Override public void onDestroy() { super.onDestroy(); if (handler ! null runnable ! null) { handler.removeCallbacks(runnable); } stopForeground(true); } Override public IBinder onBind(Intent intent) { return null; } }在AndroidManifest.xml中声明服务service android:name.MyBackgroundService android:enabledtrue android:exportedfalse / !-- 如果针对Android 8.0还需要声明前台服务权限 -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE /创建启动/停止服务的方法public static void startBackgroundService(Context context) { Intent serviceIntent new Intent(context, MyBackgroundService.class); if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent); } else { context.startService(serviceIntent); } } public static void stopBackgroundService(Context context) { Intent serviceIntent new Intent(context, MyBackgroundService.class); context.stopService(serviceIntent); }Unity端调用public void StartService() { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (AndroidJavaClass serviceClass new AndroidJavaClass(com.yourcompany.unityplugin.ServiceHelper)) { serviceClass.CallStatic(startBackgroundService, currentActivity); } } // 停止服务类似避坑指南前台服务从Android 8.0API 26开始如果应用在后台运行服务必须将其设置为前台服务并显示一个持续的通知否则服务几秒后就会被系统停止。这就是showForegroundNotification方法的作用。服务类型START_STICKY使得服务被意外杀死后系统会尝试重新创建它。但这不保证立刻执行。功耗与策略后台服务会消耗电量用户可以在系统设置中强制停止你的应用。在设计后台功能时务必考虑其对用户体验和电池的影响并遵循Android的最佳实践如使用WorkManager进行延迟任务。线程安全服务中的Handler运行在主线程Looper.getMainLooper()确保了对UI的操作安全。如果服务中有耗时操作应另开工作线程。4.3 场景三集成第三方SDK——以微信登录为例这是商业项目中最常见的需求。第三方SDK通常提供Jar或AAR文件以及详细的集成文档。通用集成步骤获取SDK从微信开放平台下载最新的SDK通常是一个.aar文件。导入Android模块将微信的.aar文件放入你的Android Studio模块的app/libs/目录下。配置依赖在模块的build.gradle文件中添加依赖。dependencies { implementation fileTree(dir: libs, include: [*.jar, *.aar]) // 或者具体指定 // implementation files(libs/wechat-sdk-android-without-mta-xxx.aar) }编写封装类创建一个Java类如WeChatWrapper.java根据微信SDK的文档初始化SDK、处理登录请求、实现回调接口等。这个类将复杂的原生API封装成几个简单的静态方法供Unity调用。处理回调微信SDK的回调会回到你的Activity。你需要在一个统一的Activity最好是继承自UnityPlayerActivity的自定义Activity中接收回调并通过UnitySendMessage转发给Unity。编译AAR将整个Android模块包含你的封装代码和微信SDK编译成新的AAR文件。集成到Unity将最终生成的AAR、以及必要的配置如AndroidManifest.xml中的Activity声明、权限、AppKey等放入Unity项目的Assets/Plugins/Android目录。避坑指南包名与签名微信等平台需要绑定应用的包名和签名。确保Android Studio模块的applicationId在打包AAR时虽被忽略但源码中的包名需一致和最终Unity导出APK的包名一致并且使用正确的签名证书进行调试和发布。回调Activity第三方SDK通常要求配置一个特定的Activity如WXEntryActivity来处理回调。这个Activity必须放在指定的包名下例如你的包名.wxapi.WXEntryActivity。你需要在你的Android模块中创建这个包和类并在AndroidManifest.xml中正确声明然后在Unity打包时确保这个配置被合并进去。资源冲突不同SDK可能携带同名的资源文件如strings.xml导致合并冲突。需要在build.gradle中配置packagingOptions来排除或合并资源。ProGuard混淆如果发布版本启用了代码混淆ProGuard必须在proguard-rules.pro文件中为第三方SDK的类添加保留规则-keep防止其被混淆导致功能异常。5. 高级技巧与性能优化当交互变得复杂和频繁时性能和稳定性就成为关键。5.1 使用AndroidJavaProxy实现类型安全的回调UnitySendMessage使用字符串匹配容易出错且只能传一个参数。AndroidJavaProxy允许你定义一个C#接口来对应Java接口实现类型安全的回调。场景Android端有一个按钮点击监听器接口。定义Java接口在Android端public interface MyButtonClickListener { void onButtonClicked(int buttonId, String buttonText); }在C#端实现代理public class MyButtonClickProxy : AndroidJavaProxy { public MyButtonClickProxy() : base(com.yourcompany.plugin.MyButtonClickListener) {} // 方法名必须与Java接口完全一致 public void onButtonClicked(int buttonId, string buttonText) { Debug.Log($按钮被点击! ID: {buttonId}, Text: {buttonText}); // 在这里处理点击事件可以直接访问Unity的API } }在Unity中设置代理public void SetupButtonListener() { using (AndroidJavaObject buttonInstance ... ) // 获取你的Java按钮对象 { MyButtonClickProxy proxy new MyButtonClickProxy(); // 将C#代理对象设置给Java对象 buttonInstance.Call(setClickListener, proxy); } }这样当Java端的按钮被点击时会直接调用到C#端的onButtonClicked方法参数类型是安全的。5.2 减少JNI调用开销频繁的JNI调用是性能杀手。优化策略包括缓存Java对象对于需要反复使用的AndroidJavaClass或AndroidJavaObject在类的初始化阶段如Awake()或Start()获取并存储为成员变量避免每次调用都创建新的。private AndroidJavaClass _cachedPluginClass; void Start() { _cachedPluginClass new AndroidJavaClass(com.yourcompany.plugin.NativePlugin); } void SomeMethod() { // 使用缓存的对象 _cachedPluginClass.CallStatic(someMethod); } void OnDestroy() { if (_cachedPluginClass ! null) { _cachedPluginClass.Dispose(); // 记得释放 } }批量操作如果可能设计接口时让一次JNI调用完成更多工作而不是多次调用完成一系列小操作。避免在Update中调用这是铁律。任何JNI调用都应放在事件驱动或协程中。5.3 处理Android生命周期与Unity活动重建当用户旋转屏幕或应用被切换到后台再回来时Android的Activity可能会被销毁和重建。这会导致Unity端的场景重新加载而之前通过JNI获取的AndroidJavaObject引用可能失效指向旧的Activity实例。解决方案在Unity的OnApplicationPause中处理当应用暂停切到后台时释放所有持有Activity引用的Java对象。当应用恢复时重新初始化它们。private bool _isPaused false; private AndroidJavaObject _cachedActivity; void OnApplicationPause(bool pauseStatus) { if (pauseStatus !_isPaused) { // 应用进入后台 if (_cachedActivity ! null) { _cachedActivity.Dispose(); _cachedActivity null; } _isPaused true; } else if (!pauseStatus _isPaused) { // 应用回到前台 InitializeAndroidReferences(); _isPaused false; } } void InitializeAndroidReferences() { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { _cachedActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); _cachedActivity.Call(runOnUiThread, ...); // 重新设置回调等 } }使用单例或静态类管理将所有的Android交互逻辑集中在一个单例管理类中统一处理生命周期的重置。5.4 调试技巧联合Logcat与Unity Console调试跨平台交互问题需要同时查看两边的日志。Android Logcat在Android Studio中打开Logcat工具过滤你的应用包名。在Java代码中使用Log.d(YourTag, message)打印日志。Unity Console在C#代码中使用Debug.Log。关键点在发起一个从Unity到Android的调用时可以立即在Android端打印一条日志确认调用已到达。同样在Android端调用UnitySendMessage前后也打印日志。通过两边的日志串联可以清晰地追踪交互流程快速定位问题是出在调用传递、参数转换还是回调接收环节。6. 常见问题排查实录QA在实际开发中你一定会遇到下面这些问题。这里是我踩过坑后的经验总结。Q1: Unity打包后调用Android方法没有任何反应也不报错。A1:这是最常见的问题。首先检查AAR/Jar放置位置确保文件在Assets/Plugins/Android下且没有放在更深层的子文件夹中除非有特殊配置。类名和方法名检查C#代码中的包名、类名、方法名是否与Java代码完全一致大小写敏感。方法签名检查Java方法是静态static还是实例方法。静态方法用CallStatic实例方法用Call。ProGuard混淆发布包如果是Release版本出现问题而Debug版本正常极可能是代码混淆导致。在proguard-user.txt中添加-keep规则保留你的插件类。最简单的调试在Java方法的第一行加一句Log.e(DEBUG, Method Called!)然后通过Logcat查看这条日志是否出现。如果不出现说明调用根本没到Java层问题在Unity端或AAR集成上。Q2: 调用Android方法导致Unity应用崩溃ANR或闪退。A2:崩溃通常更直接查看Logcat的崩溃堆栈。主线程问题在非UI线程调用了必须运行在主线程的Android API如显示Toast、启动Activity。确保这些调用包裹在runOnUiThread中或在Unity的主线程发起。JNI引用泄漏频繁创建AndroidJavaObject而未妥善处理Dispose可能导致本地引用表溢出。养成使用using语句或在Dispose中释放的习惯。参数类型不匹配传递的C#参数类型与Java方法期望的类型不兼容。例如Java的int对应C#的intboolean对应boolString对应string。传递数组或复杂对象需要特殊处理。空指针异常最常见的NullPointerException。检查你获取的AndroidJavaObject如currentActivity是否为null尤其是在Activity生命周期变化后。Q3:UnitySendMessage回调收不到或者GameObject找不到。A3:GameObject名称确保UnitySendMessage第一个参数传入的GameObject名称与场景中存在的、激活的GameObject名字完全一致。方法名与可见性确保第二个参数传入的方法名是目标GameObject上某个脚本的公有public方法。场景与生命周期发送消息时目标GameObject必须存在于当前活动的场景中并且脚本实例是启用的。如果GameObject在DontDestroyOnLoad的场景中需要确保其路径正确。字符串参数第三个参数只能是一个字符串。如果需要传递多个参数或复杂数据请将其序列化为JSON字符串。Q4: 集成第三方SDK后Unity打包失败报错“Duplicate class”或资源合并冲突。A4:依赖冲突两个不同的SDK或SDK与Unity自身包含了同一个库的不同版本。在mainTemplate.gradle如果你启用了Gradle构建或导出Android工程后在build.gradle中使用exclude或resolutionStrategy强制指定使用某个版本。资源冲突在Assets/Plugins/Android目录下创建res文件夹来覆盖冲突的资源或者通过修改baseProjectTemplate.gradle配置packagingOptions来排除特定文件。Manifest合并错误在Assets/Plugins/Android下创建AndroidManifest.xml文件使用tools:replace或tools:ignore属性来处理与主Manifest的冲突。例如如果多个SDK都定义了application的android:theme你可以指定使用哪一个。Q5: 在Android 12及以上版本无法启动其他应用或选择文件。A5:Android 12引入了更严格的隐私保护。PendingIntent可变性如果使用PendingIntent必须显式声明其可变性FLAG_IMMUTABLE或FLAG_MUTABLE。导出组件如果你的插件声明了Activity、Service或Receiver并设置了android:exportedtrue则必须为其显式声明intent-filter否则在Android 12上安装会失败。模糊位置权限访问精确位置需要单独的ACCESS_FINE_LOCATION权限仅申请ACCESS_COARSE_LOCATION将只能获取模糊位置。跨平台交互就像在两个说不同语言的国家之间建立外交关系初期沟通成本很高但一旦协议确立通道建好后续的协作就会顺畅无比。我的经验是永远保持耐心从最简单的“Hello World”开始验证通道然后逐步增加复杂度。对于任何第三方SDK先严格按照其官方文档在纯净的Android Studio项目中跑通Demo再思考如何将其“翻译”并封装给Unity调用。多打日志善用Logcat它是你洞察两个世界之间发生了什么的最强眼睛。最后记得处理好生命周期和异常你的桥梁才能经得起用户各种复杂操作和系统升级的考验。