☰
Unity调用原生安卓工具集:从反射到aar的完整集成与避坑指南
2026/10/12 6:33:21 网站建设 项目流程

简介:这是一套面向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 APIGradleNDK
2020.3 LTSJDK 8/11API 29-306.1.1 左右r21e 前后
2021.3 LTSJDK 11API 30-316.1.1 左右r21e-r23
2022.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 Template,Unity会自动生成。

我的做法是先备份原始模板,再统一调整命名空间、依赖方式和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.useAndroidX=true android.enableJetifier=true android.overridePathCheck=true

useAndroidX=true表示当前工程使用AndroidX库;enableJetifier=true会把老的支持库依赖自动转换成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.CallStatic<int>("AddNumbers", 2, 3); Debug.Log($"Unity 收到原生返回结果: {sum}"); } #else Debug.Log("当前环境不是安卓,跳过原生调用"); #endif } }

代码背后的逻辑不复杂:new AndroidJavaClass("com.example.nativebridge.NativeBridge")通过反射拿到Java类,字符串必须是完整包名加类名;CallStatic<int>是调用静态方法并指定返回类型为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而不是apk,aar会包含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信息会合并进最终包的Manifest;Gradle远程依赖的解析优先级高于本地同名文件;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.Log

Unity自己的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-v8a
Manifest权限和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.useAndroidX=true和android.enableJetifier=true打开。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工程里关闭R8:

release { 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.CallStatic<int>("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,而不会再出现“不知道哪一环出了问题”的抓瞎状态。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询