
1. 项目概述为什么Unity与Android原生代码的交互是移动开发的必修课如果你是一个Unity开发者并且你的项目需要发布到Android平台那么你迟早会碰到一个绕不开的坎如何让Unity的C#脚本和Android原生的Java/Kotlin代码“说上话”。这个需求太普遍了比如你需要调用手机的系统相册、使用特定的硬件传感器如NFC、集成第三方SDK如微信登录、支付宝支付、或者实现一个Unity界面无法直接完成的原生弹窗。单纯靠Unity内置的API很多时候是力不从心的。这就是“Unity Android平台适配”的核心议题之一。它不是一个可选项而是当你需要深度定制App功能、提升性能或接入生态时的必选项。很多新手开发者初次接触时会觉得一头雾水JNI、AndroidJavaClass、AndroidJavaObject这些名词听起来就让人望而生畏。网上的教程要么过于零散要么直接丢出一段“魔法代码”让你照抄出了问题也不知道从何查起。我经历过无数次在Unity Editor里跑得好好的一打包成APK就崩溃的深夜调试。也踩过内存泄漏、线程冲突、字符串编码等各种稀奇古怪的坑。这篇文章我就想把这些年积累的实战经验用最直白的方式拆解给你看。我们不谈空泛的理论就聚焦在“怎么做”和“为什么这么做”上目标是让你看完之后能独立完成一个健壮、高效的Unity与Android原生交互模块。无论你是想集成一个推送服务还是做一个自定义的相机滤镜这里面的核心逻辑都是相通的。2. 交互原理深度拆解从JNI到Unity封装的桥梁在开始写代码之前我们必须先搞清楚Unity为我们搭建了什么样的桥梁。理解了这个底层机制你才能明白每一行代码在做什么出了问题也知道该往哪个方向排查。2.1 JNI一切交互的基石Java Native Interface简称JNI是Java平台提供的标准编程接口它允许运行在Java虚拟机中的Java代码调用或者被用其他语言如C、C编写的本地应用程序或库调用。在Unity-Android的语境下Unity Runtime本质上是C/C写的需要通过JNI与运行在Android ART/Dalvik虚拟机上的Java/Kotlin代码进行通信。你可以把JNI想象成一个翻译官。UnityC侧说“嘿Java帮我创建一个对象。” 这句话请求被JNI翻译成Java虚拟机能够理解的指令执行完毕后JNI再把结果翻译回C能理解的数据格式。这个过程涉及复杂的类型映射、内存管理和异常处理。幸运的是Unity已经为我们封装了这层最复杂的调用。2.2 Unity的封装AndroidJavaClass与AndroidJavaObjectUnity提供了两个核心类来简化JNI调用AndroidJavaClass和AndroidJavaObject。它们位于UnityEngine命名空间下。AndroidJavaClass代表一个Java/Kotlin的类。你可以用它来调用静态方法、访问静态字段。比如你想获取Android系统的当前版本号这个信息定义在android.os.Build.VERSION类中它是一个静态字段。AndroidJavaObject代表一个Java/Kotlin的对象实例。你需要用它来调用实例方法、访问实例字段。比如你想显示一个原生的Toast提示就需要先创建一个android.widget.Toast的对象然后调用它的show()方法。它们的构造函数和工作原理非常直观// 获取一个Java类的引用例如UnityPlayer的当前Activity AndroidJavaClass jc new AndroidJavaClass(com.unity3d.player.UnityPlayer); // 调用该类的静态方法获取一个对象实例 AndroidJavaObject currentActivity jc.GetStaticAndroidJavaObject(currentActivity); // 创建一个新的Java对象例如一个Intent AndroidJavaObject intent new AndroidJavaObject(android.content.Intent, currentActivity, new AndroidJavaClass(com.yourpackage.YourNativeActivity)); // 调用该对象的方法 currentActivity.Call(startActivity, intent);关键理解当你new一个AndroidJavaClass或AndroidJavaObject时Unity并没有在C#侧真正创建一个“对象”。它只是在C#侧创建了一个“代理”或“句柄”这个句柄通过JNI持有了对Java侧真实类或对象的引用。所有通过这个代理进行的操作都会被转发到Java侧执行。2.3 方法调用与参数传递的“黑盒”与“白盒”Unity的封装让调用变得简单但它也是一个“黑盒”。你传一个C#的string过去Unity会通过JNI帮你转换成Java的String。你传一个int它就转换成int。对于基本数据类型int,float,bool,string和AndroidJavaObject本身这个过程是自动的。但是当你需要传递复杂对象、数组或者接收一个非基本类型的返回值时就需要小心了。例如你想从Java侧返回一个自定义的UserInfo对象到C#Unity的自动封装就无能为力了。这时通常有两种策略将复杂对象拆解在Java侧将UserInfo对象的字段如name, id拆成多个基本类型参数返回或者在C#侧用多个Call方法分别获取。使用JSON作为中介在Java侧将对象序列化为JSON字符串传到C#侧再用JsonUtility或第三方库反序列化。这是最通用、最推荐的做法。注意参数传递是性能损耗的主要来源之一。频繁地通过JNI层传递大量数据尤其是字符串和数组会带来显著的开销。在设计交互接口时应遵循“少次多量”的原则即减少调用次数单次传递尽可能整合好的数据。3. 从零构建一个完整的交互实例调用系统相册选择图片理论讲得再多不如动手做一遍。我们来实现一个最经典的需求在Unity中点击一个按钮调用Android原生的相册选择器选择一张图片后将图片的路径传回Unity并在Unity的UI中显示。这个例子涵盖了启动Activity、处理返回结果、文件路径传递、权限处理等核心环节。3.1 第一步创建Android原生模块Java/Kotlin首先我们需要在Unity项目中创建一个Android插件。标准做法是在Assets目录下创建Plugins/Android文件夹结构。创建Android Library模块推荐 更专业的方式是使用Android Studio创建一个Android Library模块File - New - New Module选择Android Library。这样你可以享受完整的IDE支持代码提示、语法检查、依赖管理。将编译好的aar包放入Plugins/Android目录。但对于简单功能直接写Java文件也足够。编写核心交互类 我们在com.yourcompany.unityplugin包下创建一个ImagePicker.java文件。直接放在Assets/Plugins/Android下即可Unity在打包时会自动识别。package com.yourcompany.unityplugin; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.provider.MediaStore; import androidx.core.content.FileProvider; import java.io.File; public class ImagePicker { // 保存一个静态引用用于在onActivityResult中回调Unity private static Activity unityActivity; private static String callbackGameObjectName; private static String callbackMethodName; // 请求码用于标识我们发起的Activity请求 private static final int PICK_IMAGE_REQUEST 1001; // 初始化方法由Unity调用传入当前的Activity上下文 public static void initialize(Activity activity) { unityActivity activity; } // 打开相册选择图片的核心方法 public static void pickImage(String gameObjectName, String methodName) { if (unityActivity null) { // 可以在这里Log.e但Unity侧可能看不到更好的方式是返回一个错误码给Unity return; } callbackGameObjectName gameObjectName; callbackMethodName methodName; Intent intent new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); // 设置类型为图片 intent.setType(image/*); // 使用createChooser让用户可以选择不同的图片应用 Intent chooser Intent.createChooser(intent, 选择图片); // 关键启动Activity并期待返回结果 unityActivity.startActivityForResult(chooser, PICK_IMAGE_REQUEST); } // 这个函数必须由Unity的Activity在onActivityResult中调用 public static void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode PICK_IMAGE_REQUEST) { String result ; if (resultCode Activity.RESULT_OK data ! null) { Uri selectedImageUri data.getData(); if (selectedImageUri ! null) { // 获取图片的真实路径这是一个复杂过程Android Q之后更推荐使用ContentResolver直接打开流 // 这里返回Uri的字符串形式Unity侧需要用UnityWebRequest或WWW加载 result selectedImageUri.toString(); } } else { result cancelled; // 用户取消选择 } // 回调Unity if (callbackGameObjectName ! null callbackMethodName ! null) { com.unity3d.player.UnityPlayer.UnitySendMessage(callbackGameObjectName, callbackMethodName, result); } // 清空回调避免重复调用或内存泄漏 callbackGameObjectName null; callbackMethodName null; } } }代码要点解析UnityPlayer.UnitySendMessage这是从Java回调到Unity的“官方通道”。它接受三个参数Unity场景中的游戏对象名、该对象上挂载的脚本的方法名、要传递的字符串参数。这个方法必须在主线程调用。startActivityForResult我们不是简单地启动一个Activity而是启动一个期望返回结果的Activity。结果会返回到UnityPlayerActivity的onActivityResult方法。onActivityResult的挂载我们写的这个静态方法不会自动被调用。必须修改Unity生成的Android工程的主Activity在其onActivityResult中手动调用我们的ImagePicker.onActivityResult(...)。这是关键一步下文会详述。3.2 第二步修改Unity的主Activity关键步骤Unity默认的主Activity是com.unity3d.player.UnityPlayerActivity。为了让我们的onActivityResult能被正确触发我们需要继承它并重写方法。创建自定义Activity 在同一个Android插件目录下创建CustomUnityPlayerActivity.java。package com.yourcompany.unityplugin; import android.os.Bundle; import com.unity3d.player.UnityPlayerActivity; public class CustomUnityPlayerActivity extends UnityPlayerActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化我们的插件传入当前Activity上下文 ImagePicker.initialize(this); } Override protected void onActivityResult(int requestCode, int resultCode, android.content.Intent data) { super.onActivityResult(requestCode, resultCode, data); // 先调用父类方法 // 将结果传递给我们的ImagePicker处理 ImagePicker.onActivityResult(requestCode, resultCode, data); } }修改AndroidManifest.xml Unity打包时会合并多个AndroidManifest.xml文件。我们需要在Plugins/Android下创建一个AndroidManifest.xml文件指定使用我们自定义的Activity。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.unityplugin application !-- 覆盖Unity默认的Activity指向我们自定义的 -- activity android:namecom.yourcompany.unityplugin.CustomUnityPlayerActivity android:exportedtrue android:configChangesfontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen android:launchModesingleTask android:hardwareAcceleratedtrue android:themestyle/UnityThemeSelector intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter meta-data android:nameunityplayer.UnityActivity android:valuetrue / /activity /application /manifest重要提示AndroidManifest.xml的合并是一个复杂过程如果处理不当会导致编译失败或运行时Activity找不到。确保你的自定义Activity的android:configChanges等属性至少包含了Unity默认Activity中的所有值可以从Temp\gradleOut\下的中间manifest文件查看。最简单的方法是先让Unity导出一个Android工程参考其生成的Manifest。3.3 第三步编写Unity C#调用脚本现在回到Unity中我们创建一个C#脚本来调用原生插件。using UnityEngine; using UnityEngine.UI; using System.Runtime.InteropServices; public class NativeImagePicker : MonoBehaviour { public RawImage displayImage; // 用于显示选中图片的UI组件 public Text resultText; // 用于显示路径的文本 // 定义Android原生类的常量避免硬编码字符串 private const string PluginClassName com.yourcompany.unityplugin.ImagePicker; void Start() { // 初始化插件获取当前Activity并传递给Java侧 // 这是很多插件需要的初始化步骤确保Java侧有正确的Context using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (AndroidJavaClass pluginClass new AndroidJavaClass(PluginClassName)) { pluginClass.CallStatic(initialize, currentActivity); } Debug.Log(ImagePicker initialized.); } // 供UI按钮调用的方法 public void OnPickImageButtonClicked() { // 调用Java静态方法指定回调的游戏对象名和方法名 using (AndroidJavaClass pluginClass new AndroidJavaClass(PluginClassName)) { pluginClass.CallStatic(pickImage, this.gameObject.name, OnImagePicked); } } // 由Java侧通过UnitySendMessage调用的方法 private void OnImagePicked(string result) { Debug.Log(Image picked result: result); if (resultText ! null) resultText.text Result: result; if (result cancelled || string.IsNullOrEmpty(result)) { Debug.Log(User cancelled or no image selected.); return; } // 处理返回的Uri这里以Android返回的content:// Uri为例 // 注意在Android上直接使用file://路径可能行不通尤其是Android QAPI 29之后。 // 更可靠的方式是在Java侧将Uri转换为可访问的路径或者直接在Java侧将图片字节流读取后以Base64字符串传回。 // 此处简化处理仅展示逻辑。 StartCoroutine(LoadImageFromUri(result)); } private System.Collections.IEnumerator LoadImageFromUri(string uriStr) { // 使用UnityWebRequest加载图片 using (UnityEngine.Networking.UnityWebRequest request UnityEngine.Networking.UnityWebRequestTexture.GetTexture(uriStr)) { yield return request.SendWebRequest(); if (request.result UnityEngine.Networking.UnityWebRequest.Result.Success) { Texture2D texture ((UnityEngine.Networking.DownloadHandlerTexture)request.downloadHandler).texture; if (displayImage ! null) { displayImage.texture texture; displayImage.SetNativeSize(); } } else { Debug.LogError(Failed to load image: request.error); resultText.text Load Failed: request.error; } } } }C#脚本要点解析using语句AndroidJavaClass和AndroidJavaObject实现了IDisposable接口。使用using语句可以确保在代码块结束时自动释放JNI侧的引用避免内存泄漏。这是一个非常重要的好习惯。CallStatic方法用于调用Java侧的静态方法。第一个参数是方法名后面是可变参数列表。回调方法签名OnImagePicked方法必须为private void MethodName(string message)形式。因为UnitySendMessage只能调用公有或私有的实例方法且只能传递一个字符串参数。方法名必须与Java侧调用时传入的完全一致。异步加载从Uri加载图片是网络I/O操作必须使用协程或异步任务避免阻塞主线程。3.4 第四步处理Android权限与文件访问从Android 6.0API 23开始危险权限需要运行时申请。访问相册READ_EXTERNAL_STORAGE在旧版本上是危险权限但在Android 10API 29及更高版本上引入了作用域存储访问共享媒体文件推荐使用READ_MEDIA_IMAGES权限或直接通过Intent.ACTION_PICK而不需要声明权限。为了兼容性我们通常这样做在AndroidManifest.xml中声明权限如果需要!-- 如果targetSdkVersion 33可能需要这个权限来解析某些Uri -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / !-- Android 13 (API 33) 及以上访问图片的权限 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES /在Unity C#中动态申请权限 Unity提供了UnityEngine.Android.Permission类来处理运行时权限。可以在调用pickImage前进行检查和申请。public void OnPickImageButtonClicked() { #if UNITY_ANDROID // 检查并申请权限以READ_EXTERNAL_STORAGE为例 string permission UnityEngine.Android.Permission.ExternalStorageRead; if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(permission)) { UnityEngine.Android.Permission.RequestUserPermission(permission); // 权限请求是异步的这里需要处理用户授权后的回调。 // 一个简单的做法是弹窗提示用户并在用户再次点击按钮时检查。 resultText.text Please grant storage permission and try again.; return; } #endif // ... 原有的调用代码 }实操心得权限处理是Android开发的一大痛点。对于相册选择这种通过系统Intent完成的操作在大多数情况下你其实不需要声明READ_EXTERNAL_STORAGE权限。系统Intent会代表你的应用访问内容并返回一个临时的Uri访问权限给你。只有在你需要自己通过路径去解析这个Uri指向的文件时才可能需要权限。因此最优雅的方案是在Java侧通过ContentResolver打开Uri对应的输入流将图片数据直接处理如压缩、编码后传回Unity完全避免直接操作文件路径。这既安全又兼容。4. 高级交互模式与性能优化实战掌握了基础调用后我们来探讨更复杂、更高效的模式。直接使用AndroidJavaObject进行大量频繁的JNI调用性能开销很大。我们需要更优的架构。4.1 模式一单次调用复杂数据交互JSON桥梁当需要传递结构化数据时JSON是最通用的选择。例如你的Java插件需要返回设备信息品牌、型号、系统版本等。Java侧import org.json.JSONObject; public class DeviceInfoPlugin { public static String getDeviceInfo() { try { JSONObject json new JSONObject(); json.put(brand, android.os.Build.BRAND); json.put(model, android.os.Build.MODEL); json.put(sdkVersion, android.os.Build.VERSION.SDK_INT); json.put(versionName, 1.0.0); return json.toString(); } catch (Exception e) { return {\error\: \ e.getMessage() \}; } } }C#侧using UnityEngine; using System; // 需要引用System以使用Serializable特性 [Serializable] // 使类可被JsonUtility序列化/反序列化 public class DeviceInfo { public string brand; public string model; public int sdkVersion; public string versionName; } public class DeviceInfoFetcher : MonoBehaviour { void Start() { using (var plugin new AndroidJavaClass(com.yourcompany.plugin.DeviceInfoPlugin)) { string jsonStr plugin.CallStaticstring(getDeviceInfo); DeviceInfo info JsonUtility.FromJsonDeviceInfo(jsonStr); Debug.Log($Device: {info.brand} {info.model}, SDK: {info.sdkVersion}); } } }优势接口清晰数据格式灵活易于扩展。C#侧可以利用JsonUtility高效解析。4.2 模式二Android侧持久的服务/线程与回调有些任务需要在Android后台持续运行比如监听传感器、播放音乐、维持网络长连接。这时我们需要在Android侧创建一个Service或长期运行的线程并通过某种机制如广播、接口回调将数据持续地发送给Unity。核心挑战UnitySendMessage必须在主线程调用且依赖于一个活跃的Unity游戏对象。如果Unity游戏暂停如App切到后台这个回调可能失效。解决方案使用Android的Handler和Looper在Android插件初始化时获取Unity主线程的Looper并创建一个关联到该Looper的Handler。这样在后台线程中可以通过这个Handler将任务post到Unity的主线程中执行再安全地调用UnitySendMessage。数据缓存与状态恢复在Android Service中缓存数据。当Unity侧重新激活如从后台回到前台时主动调用一个Java方法如fetchCachedData来获取累积的数据。示例简化// Java侧一个模拟的传感器服务 public class SensorService { private static Handler mainThreadHandler; private static String callbackGameObject; private static String callbackMethod; public static void startListening(Activity activity, String gameObj, String method) { unityActivity activity; callbackGameObject gameObj; callbackMethod method; // 获取Unity主线程的Handler mainThreadHandler new Handler(Looper.getMainLooper()); // 启动一个后台线程模拟传感器数据 new Thread(() - { while (listening) { final float simulatedData (float) Math.random(); // 通过Handler切换到主线程回调Unity mainThreadHandler.post(() - { if (callbackGameObject ! null) { UnityPlayer.UnitySendMessage(callbackGameObject, callbackMethod, String.valueOf(simulatedData)); } }); try { Thread.sleep(100); } catch (InterruptedException e) {} } }).start(); } }4.3 性能优化黄金法则减少JNI调用次数这是最重要的原则。每次Call或Get都是一次昂贵的跨语言调用。尽量在一次调用中完成更多工作或者将多次调用的结果在Java侧聚合后一次性返回。避免在循环或每帧中调用绝对不要在Update()方法里频繁调用JNI方法。如果确实需要高频数据如传感器应采用上述的“Android侧持久服务回调”模式。及时释放引用务必使用using语句或在finally块中调用Dispose()来释放AndroidJavaObject和AndroidJavaClass。未释放的引用会导致Java侧的对象无法被垃圾回收造成内存泄漏。谨慎处理字符串和数组传递大量字符串或数组数据性能开销极大。考虑使用更高效的序列化方式如Protocol Buffers或者直接传递原始字节数组byte[]的指针通过IntPtr但这属于高级话题需要处理复杂的内存管理。预热对于需要频繁使用的Java类可以在初始化阶段就创建好AndroidJavaClass的静态引用并缓存起来避免每次调用都去查找类。5. Kotlin与Unity交互的特殊考量随着Kotlin成为Android开发的官方首选语言越来越多的原生SDK和插件使用Kotlin编写。Unity与Kotlin的交互原理上与Java完全相同因为Kotlin最终也是编译成JVM字节码。但在一些细节上需要注意。5.1 调用Kotlin对象的方法Kotlin对Java的互操作性做得非常好。你可以像调用Java类一样调用Kotlin类。但需要注意Kotlin的一些特性伴生对象Companion Object相当于Java的静态成员。调用伴生对象中的方法需要使用类名.伴生对象名的格式但在JNI视角下它被编译成了一个名为Companion的内部类静态实例。通常你可以直接用类名调用其静态方法编译器会处理。为保险起见在Unity中调用时可以先获取Companion对象。// Kotlin class MyKotlinClass { companion object { fun doSomethingStatic(): String Hello from Kotlin Companion } }// C# using (var kotlinClass new AndroidJavaClass(com.example.MyKotlinClass)) using (var companion kotlinClass.GetStaticAndroidJavaObject(Companion)) { string result companion.Callstring(doSomethingStatic); Debug.Log(result); } // 或者如果编译器生成了对应的静态Java方法也可以直接调用 // string result kotlinClass.CallStaticstring(doSomethingStatic);顶层函数Top-level FunctionsKotlin文件中的顶级函数会被编译成一个以文件名Kt为类名的类中的静态方法。例如在Utils.kt中定义的fun getVersion(): Int在Unity中需要这样调用using (var utilsClass new AndroidJavaClass(com.example.UtilsKt)) { int version utilsClass.CallStaticint(getVersion); }5.2 空安全与默认参数Kotlin的空安全特性在JNI边界会失效。从C#侧传递给Kotlin一个null如果Kotlin参数声明为非空String可能会在Java/Kotlin侧引发NullPointerException。同样Kotlin函数的默认参数在通过JNI调用时是无效的你必须显式地传递所有参数。建议在设计给Unity调用的Kotlin接口时尽可能将参数类型声明为可空String?并在函数体内做空值检查。避免使用默认参数或者为Unity封装一个重载的Java友好版本。5.3 协程Coroutines的回调处理如果你想在Kotlin插件中使用协程执行异步任务如网络请求然后回调Unity需要确保回调发生在主线程。可以使用Dispatchers.Main调度器。// Kotlin suspend fun fetchDataFromNetwork(): String { return withContext(Dispatchers.IO) { // 模拟网络请求 delay(1000) Data from network } } fun fetchDataAndCallback(gameObject: String, method: String) { // 启动一个协程并在主线程回调 CoroutineScope(Dispatchers.Main).launch { val result fetchDataFromNetwork() UnityPlayer.UnitySendMessage(gameObject, method, result) } }在Unity C#侧调用fetchDataAndCallback这个函数即可。协程内部的线程切换对Unity是透明的。6. 实战避坑指南与疑难问题排查即使理解了所有原理实际开发中依然会遇到各种诡异的问题。下面是我总结的常见“坑”及其解决方案。6.1 编译与打包问题问题1ClassNotFoundException或NoClassDefFoundError原因Unity没有找到你的Java/Kotlin类。排查检查类名完全正确包括包名。区分大小写。确保你的.java、.kt或.aar、.jar文件放在Assets/Plugins/Android目录下。.aar是首选它包含了编译后的代码和资源。如果使用.jar确保它是在Android SDK环境下编译的而不是普通的Java SE JAR。可以使用Android Studio的File - New - New Module - Android Library来生成。检查AndroidManifest.xml中是否因为合并冲突导致你的类被混淆或排除。问题2打包时Gradle构建失败原因依赖冲突、SDK版本不匹配、Manifest合并错误等。排查查看错误日志Unity打包失败时错误信息往往在Editor.log或控制台输出的最后几行。寻找BuildFailedException或Gradle build failed后面的具体错误。检查Gradle版本在Player Settings - Publishing Settings中可以尝试切换Build System为Gradle推荐或Internal。使用Gradle并指定一个稳定的版本如6.1.1。解决依赖冲突如果你引入了多个.aar插件它们可能依赖了同一个库的不同版本。需要在主模板Gradle文件mainTemplate.gradle中强制指定版本。// 在 dependencies 块之前添加 configurations.all { resolutionStrategy { force com.squareup.okhttp3:okhttp:4.9.0 // 强制使用指定版本 } }导出Android工程在Build Settings中勾选Export Project然后使用Android Studio打开导出的工程进行编译和调试可以获取更详细的错误信息。6.2 运行时崩溃与异常问题3在Editor中运行正常打包后崩溃原因这是最常见的问题。Editor环境是Windows/Mac没有Android的JNI环境。任何JNI调用在Editor中都会失败但Unity可能静默处理或返回默认值。你的代码可能没有对UNITY_ANDROID做平台判断。解决所有调用Android原生代码的地方都必须用#if UNITY_ANDROID !UNITY_EDITOR包裹。对于Editor模式需要提供模拟实现或跳过。public string GetDeviceId() { #if UNITY_ANDROID !UNITY_EDITOR // 真实的Android JNI调用 using (var plugin ...) { return plugin.CallStaticstring(getDeviceId); } #else // Editor或其它平台返回模拟值 return Editor-Device-ID; #endif }问题4UnitySendMessage回调不执行原因游戏对象名或方法名错误大小写、拼写必须完全一致。游戏对象必须是激活的Active。脚本未启用挂载脚本的组件可能被禁用。方法不是void类型或参数不匹配UnitySendMessage调用的方法必须是private void MethodName(string message)形式。Android侧未在主线程调用UnitySendMessage必须在Android主线程UI线程调用。Unity游戏对象已被销毁如果调用UnitySendMessage时对应的GameObject已经被Destroy则回调无效。排查在Android侧调用前加Log确认方法被触发并打印出要回调的游戏对象名和方法名。在Unity侧确保游戏对象在场景中且处于激活状态。使用Debug.Log在目标方法的第一行打印看是否执行。问题5传递复杂数据时出现乱码或崩溃原因字符串编码问题或JNI类型映射错误。解决对于中文字符串确保Java侧使用UTF-8编码。在C#侧接收时通常是没问题的。避免传递包含特殊字符如换行符\n、\r的字符串如果必须传递可以考虑进行Base64编码。对于非基本类型坚持使用JSON进行序列化/反序列化。6.3 架构设计建议建议1设计一个统一的“桥接”类不要在每个需要交互的C#脚本里都写AndroidJavaClass和AndroidJavaObject。创建一个单例或静态的AndroidBridge类来统一管理所有与Java侧的交互。这个类负责初始化、方法调用、错误处理和日志记录。建议2使用接口或委托进行回调UnitySendMessage依赖于游戏对象名和字符串形式的方法名不够灵活且容易出错。可以在C#侧定义一个接口让需要回调的类实现这个接口然后在AndroidBridge中注册这个接口实例。当Java侧回调时通过AndroidBridge调用接口方法。这需要更复杂的线程同步机制因为JNI回调可能在非主线程但架构更清晰。建议3做好日志记录在Java侧使用android.util.Log输出日志在C#侧使用Debug.Log。在真机调试时可以通过adb logcat命令查看Android日志这是定位问题最强大的工具。确保你的Java代码在关键路径如函数入口、异常捕获处都有日志输出。7. 调试技巧与工具链高效的调试能极大提升开发效率。以下是针对Unity-Android交互的专用调试技巧。7.1 使用adb logcat查看原生日志这是调试Android插件的必备技能。Unity的Debug.Log输出在logcat中也有但Java插件的Log.d()输出只能在这里看到。连接你的Android设备或启动模拟器。打开命令行终端输入以下命令adb logcat -s Unity DEBUG-s表示过滤标签Unity是Unity引擎的日志标签DEBUG是你自己在Java代码中定义的标签如Log.d(DEBUG, message)。运行你的Unity应用观察命令行输出的日志。你可以编写一个简单的Java类将日志同时输出到logcat和通过UnitySendMessage回传给Unity方便在Unity Editor中也能看到关键信息当然只有真机运行时才有原生日志。7.2 在Android Studio中调试原生代码如果你将插件做成了Android Library模块.aar你可以进行源码级调试。在Unity中打包时勾选Build Settings中的Export Project。使用Android Studio打开导出的项目位于[Project]/Build/下的文件夹。在Android Studio中找到你的插件源码设置断点。选择Run - Debug ‘app’。Android Studio会安装APK并启动调试会话。当Unity调用到你的Java/Kotlin代码时断点就会触发。这种方法能让你深入跟踪原生代码的执行流程查看变量状态是解决复杂问题的终极武器。7.3 Unity Profiler 与 JNI 性能分析频繁的JNI调用是性能杀手。使用Unity Profiler的CPU Usage模块你可以看到AndroidJNI.CallStaticMethod或类似调用所占用的时间。如果发现某个JNI调用耗时异常就要考虑优化能否减少调用频率能否将多次调用合并为一次能否将计算逻辑移到C#侧记住跨语言调用的开销不仅仅是函数执行时间还包括参数编组、线程切换等。在性能敏感的代码路径上如每帧执行的Update中必须极力避免JNI调用。交互的稳定性和性能是衡量一个功能是否合格的关键。我个人的经验是在项目初期就搭建一个稳健、可扩展的交互框架远比在后期到处打补丁要高效得多。从简单的字符串传递开始逐步过渡到复杂的JSON数据交换和后台服务通信每一步都做好错误处理和日志记录这样构建出来的功能才能经得起真机复杂环境的考验。最后多测试尤其是在低端Android设备上测试你会发现很多在高端设备或模拟器上发现不了的问题。