简介这是一套面向Unity开发者的安卓原生交互入门工具集聚焦UnityActivity、UnityAppContext、PackageManager、RunOnUIThread等基础调用方式并封装Toast/Log、Java与C#字符串互转、获取App列表、判断服务或应用运行、打开/安装/卸载App、发送广播以及获取WiFi状态、安卓版本、原生类型ID、内置SD卡路径等常用方法适合刚接触Unity调用原生安卓的开发者参考复用。资源共45个文件压缩包仅48KB以cs脚本和asset配置为主cs脚本提供核心封装与测试代码asset保存项目设置与测试场景另含jar包、xml配置和.unity场景结构清晰便于对照学习。目前已有743人浏览学习工具集不仅整理网上零散方案还自带可运行的测试场景与示例脚本方便验证封装方法是快速上手Unity与安卓原生交互的实用入门包。1. Unity调用原生安卓这条链路到底在解决什么问题做Unity对接安卓原生SDK时最常遇到的场景是支付、广告、扫码、硬件外设这类必须走Java层的能力。Unity的C#层拿不到系统级的安卓接口直接写AndroidJavaObject又经常被类名、方法签名和打包流程卡住。很多项目第一步就翻车aar放进去不生效、Gradle合并冲突、Java回调Unity时闪退最后只能靠Android Studio单独调原生再绕一层本地Socket把数据传回来。这个标题里的“工具集”指的是一套完整的集成路线从工程引依赖、写反射调用、编aar、查产物、抓日志到回归验证把Unity与安卓的边界打通。它解决的不是“能不能调”而是“怎么调得稳、调得快、出了问题怎么定位”。适合要接入原生SDK、把现有安卓工程迁到Unity、或要在Unity里做硬件交互的开发者。前置要求不高懂一点C#、会配Android环境剩下的是套路和坑的问题。2. 开工前的环境与依赖准备版本匹配与Gradle模板2.1 JDK、SDK、NDK版本与Unity的匹配关系Unity打包安卓时Gradle、JDK、SDK、NDK四者的版本是绑定关系不是越新越好。我见过太多项目把环境配到最新结果Unity自带Gradle模板跑不动报错信息全是英文长堆栈最后发现是JDK 17和Gradle 6.x不兼容。按Unity版本选环境比按“最新版”选环境靠谱得多。常见匹配关系可以参考下面这张表。注意Unity小版本更新后具体值会变这里给的是大多数人能稳定跑的区间。Unity大版本JDKAndroid SDK APIGradleNDK2020.3 LTSJDK 8/11API 29-306.1.1 左右r21e 前后2021.3 LTSJDK 11API 30-316.1.1 左右r21e-r232022.3 LTSJDK 11/17API 31-337.x-8.xr23-r25判断自己该用哪一套的最快方法先开一个空项目直接Build一次看Unity自动生成的Temp/UnityTempFile里写的是什么版本然后手动把SDK和NDK对齐。Unity在Editor里会提示“recommended NDK version”照着它装最省事不用自己拍脑袋选。这里有个容易忽视的细节Java版本与Gradle版本必须匹配。JDK 11配Gradle 6.x基本没问题但JDK 17配Gradle 7.0以下会直接报Unsupported class file major version 61。类似问题在Unity 2022以后会少一些因为新版Unity推荐JDK 17但如果你在Unity里同时导出工程去Android Studio继续开发Android Studio要求的Gradle版本又可能和Unity不一致需要单独处理。2.2 先改Gradle模板再动手打包前必须做的一件事Unity默认生成的Gradle模板不会帮你搞定原生依赖与AndroidX冲突所以开始写任何Java代码之前先把Assets/Plugins/Android/目录下的mainTemplate.gradle、gradleTemplate.properties和baseProjectTemplate.gradle准备好。没有模板文件时在Build Settings里勾选Custom Main Manifest、Custom Gradle Template、Custom Base Gradle Template、Custom Gradle Properties TemplateUnity会自动生成。我的做法是先备份原始模板再统一调整命名空间、依赖方式和AndroidX开关。一个可用的mainTemplate.gradle依赖段大致长这样dependencies { implementation fileTree(dir: libs, include: [*.jar, *.aar]) implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 }这段配置有几个关键点。implementation fileTree(dir: libs, include: [*.jar, *.aar])表示Unity工程里的jar和aar都会参与打包这是多数Unity原生插件的默认方式。androidx.appcompat和material是很多第三方SDK的隐式依赖如果SDK内部依赖AndroidX而你这里没引Gradle会报“Failed to resolve: androidx.appcompat:appcompat”之类的错误那时再回头加就晚了。gradleTemplate.properties里最值得改的是AndroidX开关android.useAndroidXtrue android.enableJetifiertrue android.overridePathChecktrueuseAndroidXtrue表示当前工程使用AndroidX库enableJetifiertrue会把老的支持库依赖自动转换成AndroidX版本接入第三方SDK时几乎必开。overridePathCheck是防止路径过长导致构建失败Windows环境下特别容易触发。如果你的项目完全没有老Support库依赖Jetifier可以关掉能省不少构建时间。2.3 先做一次最小导出验证再开始写代码把模板和SDK配好之后不要急着写任何Java代码。先建一个空场景加上唯一的“BridgeHost”空物体直接Build一个release包装到手机上。这一步看着多余实际价值极高它能确认“纯Unity工程”的构建链路没问题后续所有报错都能排除环境因素。常见做法是在Build Settings里选“Export Project”导出后用Android Studio打开再编译一次。好处是能看到Gradle的完整输出出错时定位更快。Unity内直接Build则更贴近最终发布流程。我的习惯是两种都跑一次Unity内Build做最终验证导出工程找问题细节。如果导出工程能过Unity内Build失败去查Unity日志里的“CommandInvokationFailure”那段多半是Unity的Gradle和本地Gradle版本冲突。3. 两条集成路线反射调用Java与导入Android Library工程3.1 轻量场景用AndroidJavaObject反射调用Java静态方法Unity反射安卓Java层核心是AndroidJavaClass和AndroidJavaObject这两个类。AndroidJavaClass对应Java里的静态类AndroidJavaObject对应实例对象。最常见的用法是调Java的静态方法返回结果比如读设备型号、调系统Toast或者触发某个SDK的初始化。下面这个最小例子演示C#层调用一个Java静态方法并拿到返回值using UnityEngine; public class NativeBridgeInvoker : MonoBehaviour { void Start() { #if UNITY_ANDROID !UNITY_EDITOR using (AndroidJavaClass nativeClass new AndroidJavaClass(com.example.nativebridge.NativeBridge)) { int sum nativeClass.CallStaticint(AddNumbers, 2, 3); Debug.Log($Unity 收到原生返回结果: {sum}); } #else Debug.Log(当前环境不是安卓跳过原生调用); #endif } }代码背后的逻辑不复杂new AndroidJavaClass(com.example.nativebridge.NativeBridge)通过反射拿到Java类字符串必须是完整包名加类名CallStaticint是调用静态方法并指定返回类型为int两个数字作为方法参数传入。这里最容易踩的坑是方法名或签名对不上Java侧方法名改了C#侧不会在编译期报错只有运行时才抛出异常所以类名和方法名最好集中放在常量文件里不要散落各处。调用实例方法的情况也常见。Java侧先new一个对象再调方法时用AndroidJavaObject创建实例之后Call就是实例方法using (AndroidJavaObject nativeObject new AndroidJavaObject(com.example.nativebridge.NativeBridge)) { nativeObject.Call(ShowToast, 来自Unity的调用); }如果你的Java方法接收多个类型不同的参数C#侧的参数顺序必须和Java方法签名完全一致包括int、float、String、boolean这些基础类型的映射关系。传错了不像C#那样编译报错而是运行时给一句“method not found”只能靠日志一点点对。3.2 中重场景把已有Android工程做成aar接入Unity反射调用只适合Java代码逻辑简单、不需要UI和复杂依赖的场景。一旦Android侧有Activity、Fragment、大量第三方SDK建议直接创建一个Android Library模块编译成aar后扔进Unity。Android Library模块的build.gradle配置大致如下apply plugin: com.android.library android { compileSdkVersion 33 buildToolsVersion 33.0.0 defaultConfig { minSdkVersion 21 targetSdkVersion 33 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation com.some.sdk:core:2.5.1 implementation androidx.appcompat:appcompat:1.6.1 }这里apply plugin: com.android.library是关键它表示编译产物是aar而不是apkaar会包含AndroidManifest、jar/res/so等Unity需要的资源。把生成的aar文件复制到Unity工程的Assets/Plugins/Android/目录下Unity打包时会自动合并aar里的Manifest和资源。aar方案比反射方案多了一层编译隔离。Android侧能独立测试依赖关系清晰Unity侧只暴露少量静态方法给C#代码也好维护。代价是每改一次Java代码都要重新编译aar、替换文件迭代节奏比反射慢但换来的是稳定性和可维护性对接正式SDK时我基本都走这条路。3.3 反射和aar怎么选按这几个维度判断很多新手纠结到底用哪种方案给一个比较实用的选择逻辑。对比维度反射调用aar导入集成速度快改代码即生效慢要编译、替换、构建依赖管理弱AndroidX等依赖还得在Gradle模板里配强依赖写在模块自己的build.gradle里复杂UI/Activity不擅长需要侵入式操作支持完整调试体验不好定位只能打日志Android Studio直接断点调试适合场景简单工具类、少量方法调用正式SDK接入、业务逻辑复杂补充一句实际项目往往两种方案混合用。aar里放复杂逻辑对外暴露几个静态入口C#侧用反射调这几个入口兼顾了两者的优点。混合方案的核心是设计稳的Java接口原则是传JSON字符串而不是大量独立参数返回结果也统一用JSON这样Java侧再怎么改内部实现Unity侧调用方式不变。4. 工具集的核心工具依赖解析、构建产物检查与日志桥接4.1 依赖去哪找定位jar/aar/so文件的三条路径原生依赖文件并非只有“导入Assets/Plugins/Android”这一种方式。第三方SDK通常会给出几种分发格式Maven坐标、aar文件、jar加so的组合。集成之前先想清楚依赖从哪里来能省很多后续排查时间。最常见路径是直接把aar放进Assets/Plugins/Android/。另一种是通过Gradle依赖远程拉取在mainTemplate.gradle里写implementation com.example:some-sdk:2.5.1构建时自动从Maven仓库下载。第三种是只给jar加so库的形式这时jar放Assets/Plugins/Android/so文件放Assets/Plugins/Android/libs/armeabi-v7a和arm64-v8a目录下。这三种方式可以混用但要清楚它们的优先级优先级。Unity文档没有严格说明但实践发现aar里的Manifest信息会合并进最终包的ManifestGradle远程依赖的解析优先级高于本地同名文件so文件放置路径错误不会报错只是运行时会找不到库而闪退。定位这类问题直接解压apk检查lib目录和dex里是否包含目标类比读一堆日志快得多。4.2 类名不匹配排查用javap反查编译产物反射调用最恶心的问题就是Java类和方法“明明存在但Unity说找不到”。这时候不要瞎猜直接对编译产物做检查。aar本质上是个zip解压后能看到classes.jar用javap可以反编译出所有类和方法签名# 解压aar unzip -o NativeBridge.aar -d NativeBridge_aar/ # 从classes.jar里查看目标类的方法签名 javap -classpath NativeBridge_aar/classes.jar -s com.example.nativebridge.NativeBridge-s参数会输出方法签名的内部格式比如int AddNumbers(int, int)对应输出(II)I。Unity的AndroidJavaObject在做反射时内部就是按这个签名去匹配方法。如果Java侧方法带了泛型、可变参数或者默认参数实际生成的签名可能和你想的不一样javap一查就能真相大白。这个工具最大的价值在于终结“凭感觉排查”。很多时候问题不是Unity不会调而是Java方法被混淆器改名了或者因为代码裁剪被移除。提前用javap把签名记录下来写C#调用时照着抄基本不会再出NoSuchMethodError。4.3 日志桥把安卓侧日志接到Unity的Debug.LogUnity自己的Debug.Log在release包里默认不一定输出到Logcat而原生Java层的Log输出Unity又看不见。两边日志各看各的排查问题像在两个黑匣子之间反复切换。我的做法是在Java侧加一个日志桥把关键日志同时发到Unity侧。Java侧的桥接代码如下public class NativeLogger { public static void sendToUnity(String tag, String message) { Log.d(tag, message); UnityPlayer.UnitySendMessage(BridgeHost, OnNativeLog, [ tag ] message); } }Unity侧接收日志的方法public class BridgeHost : MonoBehaviour { public void OnNativeLog(string message) { Debug.Log($[Native] {message}); } }UnityPlayer.UnitySendMessage是Unity安卓运行时提供的静态方法第一个参数是场景中GameObject的名字第二个是GameObject上挂的脚本方法名第三个是字符串消息。注意GameObject必须在场景中处于激活状态否则Unity收不到消息。桥接的粒度不要做太细每次调用UnitySendMessage都会有性能开销日志量大时会影响帧率建议在Java侧加一个开关release包默认关掉出问题时再开。4.4 构建产物自检清单每次打包完成不要急着装到手机上先按下面这张清单做一轮快速检查能拦截大部分低级错误。检查项检查方式期望结果包内包含目标jar/aar的类解压apk查看classes.dex能找到目标类so文件架构完整解压apk查看lib目录至少包含arm64-v8aManifest权限和Activity存在用apkanalyzer或aapt查看与预期一致版本号与签名Unity Player Settings核对与测试包一致是否被混淆/裁剪对比javap输出方法签名完整存在这套检查流程的核心思路是不相信“编译成功”这个结果只相信“包里确实有这个类、这个权限、这个so”。Unity构建成功不代表aar被正确合并了更不代表反射调用一定能命中方法尤其在用了代码裁剪或第三方混淆规则的情况下。5. 原生调用链路避坑指南从打包失败到手机白屏5.1 打包报错uses-sdk:minSdkVersion与AndroidX冲突现象Unity构建时报错提示uses-sdk:minSdkVersion 19 cannot be smaller than version 21 declared in library或者AndroidX相关的Failed to resolve。原因Unity默认的minSdkVersion偏低而aar模块里的minSdkVersion要求更高。另一个常见原因是SDK内部依赖了AndroidX但Unity工程没有开启useAndroidX导致依赖解析失败。解决在gradleTemplate.properties里把android.useAndroidXtrue和android.enableJetifiertrue打开。minSdkVersion不一致时在mainTemplate.gradle里显式声明defaultConfig { minSdkVersion 21 targetSdkVersion 33 }这个优先级的坑在于aar里声明的minSdkVersion如果比Unity的高Gradle会报错反过来则没问题。所以主工程统一提一个较高版本是最稳的既兼容aar也避免后续接入其他SDK继续翻车。5.2 Java方法明明存在运行时却抛NoSuchMethodError现象Unity侧的AndroidJavaObject调用一直正常升级了一个SDK版本后运行时报NoSuchMethodError但用javap查classes.jar方法还在。原因大概率是classpath里存在两个同名类或同名方法。Unity把jar放在Assets/Plugins/Android/同时aar里也带同样的类构建时Gradle按顺序合并最终打包进去的是旧版本。此时javap查的是aar里的classes.jar而Unity实际反射到的是另一个类。解决用apktool或jadx解压最终apk找到目标class并确认其方法签名。如果发现重复类删掉Assets/Plugins/Android/里的旧jar尽量只用一处依赖来源。另一点是R8代码裁剪debug包正常、release包抛NoSuchMethodError多半是裁剪规则把方法裁掉了在Android工程里加keep规则-keep class com.example.nativebridge.** { *; }5.3 Java端回调Unity时直接闪退现象Java主动向Unity发消息时Unity闪退Logcat里没有明显Java异常有时能看到SIGSEGV。原因最常见的是回调发生在非Unity主线程。Unity的UnitySendMessage在大多数Android设备上要求在主线程调用而Java侧很多SDK回调默认在子线程。子线程直接发消息轻则丢失消息重则崩溃。另一个原因是GameObject不活跃或已被销毁桥接方法找不到接收者。解决在Java侧做一次线程切换确保UnitySendMessage跑在主线程public static void sendToUnitySafe(final String gameObject, final String method, final String message) { UnityPlayer.currentActivity.runOnUiThread(new Runnable() { Override public void run() { UnityPlayer.UnitySendMessage(gameObject, method, message); } }); }runOnUiThread在Unity里对应的是Unity主线程这个方案基本能消除子线程回调导致的闪退问题。前提是UnityPlayer.currentActivity不为null所以不要在Application级别、Activity尚未创建时调这个方法。5.4 64位so不完整部分机型安装后闪退现象新手机装包后启动即闪退Logcat里报dlopen failed: library xxx.so not found但检查apk里的lib目录so文件确实存在。原因apk里带了armeabi-v7a的so却没有arm64-v8a版本。现代Android设备优先加载64位so如果找不到会尝试32位目录但系统本身是64位架构、Unity又是IL2CPP 64位编译全套链路下来变成“Unity主体64位、so只有32位”反射调so库时直接崩。解决统一让Unity打出arm64-v8a版本第三方SDK的so也要提供64位版本。如果SDK只给了32位那Unity的Scripting Backend要改成Mono但Mono本身不少新架构又有兼容问题。我一般会在项目启动前就向SDK方确认64位是否完整避免集成到一半才发现so缺失。检查工具很简单解压apk看lib目录有arm64-v8a文件基本放心没有就得换引入方式或找SDK提供商要新版。5.5 release包反射类名被混淆debug包一切正常现象debug包所有功能正常release包一调用原生功能就抛ClassNotFoundException或NoClassDefFoundError但自己明明没开混淆。原因Unity项目导出Android工程时Android模块默认开启了R8代码压缩尤其是打release包时aar里的类如果没有任何引用会被直接移除。Unity反射用的是字符串类名R8不认识这种动态引用于是把“看起来没用”的类裁掉了。解决最激进也最简单的方案是在Android工程里关闭R8release { shrinkResources false minifyEnabled false }但多数第三方SDK要求release包开启混淆优化。更合理的做法是保留反射入口的keep规则按类路径配置-keep class com.example.nativebridge.** { *; } -keepclassmembers class com.example.nativebridge.** { public *; }每次升级SDK或改Java代码后重新确认keep规则仍然有效。这条规则最大的问题是没有“报错提醒”类被裁掉只有在运行时才会暴露所以我会把“release包回归测试”固定在每次打包的验收清单里不偷懒。6. Java回调Unity主线程的调度与Editor下的模拟验证6.1 用Handler和Looper约束回调线程原生侧逻辑统一收敛到主线程并不总是可行。有些SDK回调是高频的比如播放进度、扫码识别结果每次都runOnUiThread切主线程Unity侧会感觉卡顿。更稳的做法是Java侧做队列约束低频消息直接发高频消息合并后发private final Handler mainHandler new Handler(Looper.getMainLooper()); public void onProgress(int progress) { mainHandler.post(() - UnityPlayer.UnitySendMessage(BridgeHost, OnProgress, String.valueOf(progress)) ); }Handler(Looper.getMainLooper())强制回调跑在UI线程不会频繁触发Unity侧的线程检查。高频场景建议在Unity侧做帧缓冲在Update里统一取最新值而不用每帧都处理消息能显著降低GC压力。一个更彻底的办法是Java侧维护一个最新值缓存Unity侧按需拉取而不是推模式。6.2 没有安卓手机时用编辑器模拟预处理指令跑通逻辑Unity编辑器里没有安卓运行时AndroidJavaClass构造会直接抛异常因此所有原生调用代码都要做平台隔离。常用做法是原生部分抽象成接口Unity侧实现一套模拟逻辑public interface INativeBridge { int Add(int a, int b); } #if UNITY_ANDROID !UNITY_EDITOR public class AndroidNativeBridge : INativeBridge { public int Add(int a, int b) { using (var cls new AndroidJavaClass(com.example.nativebridge.NativeBridge)) { return cls.CallStaticint(AddNumbers, a, b); } } } #else public class EditorNativeBridge : INativeBridge { public int Add(int a, int b) a b; // 编辑器下的模拟实现 } #endif这样在编辑器里也能运行完整逻辑流程不用每次都在真机上验证。编辑器模拟方案的重点是“模拟足够真实”Java层抛什么错、返回什么JSON模拟实现里尽量保持一致否则编辑器跑通了真机照样翻车。我通常会把Java侧的JSON样例直接硬编码到模拟实现里确保Unity侧UI逻辑面对的输入格式和真机一致。上面这一套下来整个原生调用链路其实是夹在Unity与Android之间的一个缩小型中间件线程怎么切、消息怎么传、类怎么保活、依赖怎么解析、产物怎么验证全部都是围绕这个边界在做文章。技术层面没有玄学每一条异常都有对应的检查手段和修复路径。做完这个工具集之后我最大的教训是不要轻信任何一次“编译成功”也不要轻信任何一次“真机正常”。每一次改动都跑一遍自检清单把构建产物、类签名、线程状态、回调节奏都确认过问题最多也就是SDK本身的逻辑bug而不会再出现“不知道哪一环出了问题”的抓瞎状态。希望帮到你。本文还有配套的精品资源点击获取