资讯动态

Unity调用原生安卓入门:JNI、AAR与AndroidJavaClass实战指南

发布时间:2026/10/18 13:56:32 来源:尧图企业网站定制
简介这份工具集面向希望打通 Unity 与原生安卓交互的开发者尤其适合刚接触 JNI 与 Android 插件封装、需要可运行参考案例的入门到进阶人群。内容围绕 Unity 调用原生安卓的常见需求展开涵盖 UnityActivity、UnityAppContext、PackageManager、RunOnUIThread 等基础方法以及 Toast、Log、Java 与 C# 字符串互转等实用封装。在此基础上还整理了获取 App 列表、判断服务与应用是否运行、打开/安装/卸载 App、发送广播以及获取 WiFi 状态、安卓版本、原生类型 ID、内置 SD 卡路径等系统信息的方法部分来自网络收集部分为作者自行封装。资源包共 45 个文件以 cs 脚本、asset 配置、meta 文件为主另含 xml、unity 场景、txt 与 jar 包整体约 48KB结构轻量便于直接导入工程验证。目前已有 743 人学习可作为快速理解 Unity 与安卓原生通信链路的参考起点。1. 从一次打包翻车说起Unity 调用原生安卓到底在解决什么去年帮一个做智能硬件的团队排查问题他们的 Unity 应用在编辑器里跑得好好的一打到安卓真机上就黑屏。日志里只有一行ClassNotFoundException追了半天发现是打包时把自定义的AndroidManifest.xml覆盖没了原生模块的类根本没被加载进去。这类问题在 Unity 接安卓原生的场景里太常见了——不是代码写错而是工程结构和调用链路没理清楚。这份「Unity 调用原生安卓入门参考工具集」要解决的就是这条链路上从零到跑通的完整问题。它适合两类人一类是刚接触 Unity 安卓开发、需要调系统能力蓝牙、串口、相机、文件权限的新手另一类是被 JNI 通信、AAR 打包、Gradle 版本冲突折腾过的熟手想找一份能对照排查的参考。核心价值不在于教你写 Java而在于把 Unity 侧和安卓侧的边界、通信方式、打包配置讲透让你少走几遍我踩过的弯路。2. 三种调用方式怎么选JNI、AndroidJavaClass 与 AAR 插件Unity 调用原生安卓本质上只有三条路直接通过AndroidJavaClass/AndroidJavaObject反射调用系统 API、把 Java 代码打成 AAR 或 JAR 插件再调用、以及通过 JNI 写 C/C 中间层。选错方式后面全是坑。2.1 反射调用最快上手但边界明显反射调用适合调安卓系统自带的类比如Toast、Vibrator、Settings。不需要额外配置Unity 引擎自带AndroidJavaClass封装。常见做法是// 获取当前 Activity 的上下文这是调用系统服务的前提 using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { // 拿到震动服务 using (AndroidJavaObject vibrator activity.CallAndroidJavaObject(getSystemService, vibrator)) { // 参数 0 表示震动时长单位毫秒 vibrator.Call(vibrate, 200L); } }这段代码的逻辑是Unity 的UnityPlayer类持有当前 Activity 引用所有需要 Context 的系统服务都得从这里拿。参数上注意vibrate传的是long类型传int会找不到方法。反射调用的边界在于它只能调已存在的类没法调你自己写的 Java 代码而且每次调用都有反射开销高频调用场景比如每帧读传感器不建议走这条路。2.2 AAR 插件自定义逻辑的标准做法当你需要写自己的 Java/Kotlin 逻辑——比如对接一个第三方 SDK、封装串口协议——就得打成 AAR。流程是在 Android Studio 里建一个 Library 模块写完后Build Make Module生成 AAR丢进 Unity 的Assets/Plugins/Android/目录。这里有个关键点AAR 里的AndroidManifest.xml会和 Unity 自动生成的清单合并。如果你的 AAR 里声明了权限或 Activity合并规则是「并集」但application节点的属性会冲突。我一般会在 AAR 的清单里只写必要权限Activity 声明放到 Unity 侧统一管理避免合并时出现tools:replace报错。2.3 JNI 中间层性能敏感场景才考虑JNI 是在 C/C 层做桥接Unity 侧通过DllImport调.so.so再通过 JNI 调 Java。这条路适合高频数据交换比如音频处理、图像算法。代价是调试成本高一个签名对不上就是崩溃而且不同 CPU 架构要分别编译。新手不建议一上来就走这条路先用 AAR 把逻辑跑通确实遇到性能瓶颈再下沉到 JNI。调用方式适用场景配置成本调试难度反射调用系统 API、简单功能低低AAR 插件自定义 Java 逻辑、第三方 SDK中中JNI 中间层高频数据、性能敏感高高3. 从零跑通一个 AAR 调用工程配置与通信实现光讲选型不够这一章把 AAR 从创建到 Unity 调通的完整流程走一遍。我用的环境是 Unity 2021 LTS Android Studio Arctic FoxGradle 版本 6.7.1这套组合在多数项目里比较稳。3.1 安卓侧写一个带回调的 Java 类先在 Android Studio 新建一个Empty Activity项目然后File New New Module选Android Library模块名比如叫nativebridge。在里面写一个类package com.example.nativebridge; import android.app.Activity; import android.widget.Toast; public class Bridge { private Activity activity; // 构造函数接收 Unity 传过来的 Activity public Bridge(Activity activity) { this.activity activity; } // 无返回值方法弹一个 Toast public void showToast(String message) { activity.runOnUiThread(() - Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() ); } // 带返回值方法做加法 public int add(int a, int b) { return a b; } }这里有两个细节showToast必须切到 UI 线程否则在非主线程调用会抛异常构造函数持有 Activity 引用是为了后续调系统服务方便。写完执行Build Make Module nativebridge在nativebridge/build/outputs/aar/下拿到nativebridge-release.aar。3.2 Unity 侧目录结构与调用脚本把 AAR 复制到Assets/Plugins/Android/下。如果 AAR 依赖了其他库比如androidx还需要把依赖的 JAR 或 AAR 一并放进去否则运行时会报NoClassDefFoundError。然后写调用脚本using UnityEngine; public class NativeBridgeTest : MonoBehaviour { private AndroidJavaObject bridgeInstance; void Start() { // 获取 Unity 当前 Activity using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { // 实例化 Java 侧的 Bridge 类包名和类名必须完全一致 bridgeInstance new AndroidJavaObject(com.example.nativebridge.Bridge, activity); } // 调用无返回值方法 bridgeInstance.Call(showToast, 来自 Unity 的问候); // 调用带返回值方法 int result bridgeInstance.Callint(add, 3, 5); Debug.Log($原生加法结果{result}); } }逻辑说明AndroidJavaObject的构造函数第一个参数是完整类名后面跟的是 Java 构造函数的参数。Callint里的泛型指定返回类型如果 Java 方法返回void就用Call不带泛型。参数类型要严格对应Java 的int对应 C# 的intlong对应long字符串对应string。3.3 打包配置Gradle 模板与清单合并Unity 默认用的是内置 Gradle 模板但接了 AAR 之后经常需要自定义。在Player Settings Publishing Settings里勾选Custom Main Gradle Template和Custom Launcher Gradle TemplateUnity 会在Assets/Plugins/Android/下生成mainTemplate.gradle和launcherTemplate.gradle。常见做法是在mainTemplate.gradle的dependencies块里加上dependencies { implementation fileTree(dir: libs, include: [*.jar]) // 如果 AAR 依赖了 androidx需要显式声明 implementation androidx.appcompat:appcompat:1.3.1 **DEPS**; }**DEPS**是 Unity 的占位符别删。如果 AAR 用了androidx但 Unity 项目没启用需要在gradleTemplate.properties里加上android.useAndroidXtrue和android.enableJetifiertrue否则打包会报资源找不到。4. 避坑指南JNI 崩溃、Gradle 冲突与清单覆盖这一章是我自己踩过的坑每条都按「现象 → 原因 → 解决」写你对照排查能省不少时间。4.1 现象真机运行闪退日志报ClassNotFoundException原因AAR 没被打进 APK或者包名/类名写错。Unity 在打包时会扫描Plugins/Android/下的 AAR但如果 AAR 放在子目录里且没有正确标记平台可能被忽略。解决选中 AAR 文件在 Inspector 里确认Platform Settings勾选了AndroidCPU架构按需选。另外检查AndroidJavaObject里的类名是否和 Java 文件里的package一致大小写敏感。4.2 现象打包报错Duplicate class androidx.xxx found原因多个 AAR 依赖了同一个库的不同版本或者 Unity 内置的库和你的 AAR 冲突。解决在mainTemplate.gradle里用exclude排除重复依赖implementation(com.example.nativebridge:1.0) { exclude group: androidx.appcompat, module: appcompat }如果冲突来自 Unity 内置库可以在Player Settings Publishing Settings里勾选Custom Gradle Properties Template加上android.enableJetifiertrue让 Gradle 自动迁移旧版支持库。4.3 现象自定义AndroidManifest.xml里的 Activity 没生效原因Unity 打包时会用自己的清单覆盖或者合并时优先级搞反了。解决把自定义清单放在Assets/Plugins/Android/AndroidManifest.xml并且在Player Settings Publishing Settings里把Custom Main Manifest勾上。如果只是加权限直接在 Unity 的Player Settings Other Settings里勾选对应权限更省事避免清单合并冲突。4.4 现象Java 侧回调 Unity 没反应原因UnityPlayer.UnitySendMessage要求 GameObject 名字、方法名完全匹配且方法所在脚本必须挂在场景里的物体上。解决Java 侧这样写import com.unity3d.player.UnityPlayer; public void notifyUnity(String message) { // 参数依次是GameObject 名字、方法名、消息内容 UnityPlayer.UnitySendMessage(NativeBridgeReceiver, OnNativeMessage, message); }Unity 侧对应public class NativeBridgeReceiver : MonoBehaviour { // 方法名必须和 Java 侧传的一致 public void OnNativeMessage(string message) { Debug.Log($收到原生消息{message}); } }注意这个脚本要挂在一个名为NativeBridgeReceiver的 GameObject 上名字不能有空格或特殊字符。4.5 现象调用原生方法时主线程卡顿原因反射调用本身有开销如果每帧调用或者传大量数据会拖慢主线程。解决把高频调用改成批量传递比如传感器数据攒够 10 帧再传一次或者把逻辑下沉到 JNI 层用 C 处理。我一般会在 Java 侧开一个工作线程做耗时操作完成后通过runOnUiThread回调 Unity。5. 进阶技巧用 AndroidJavaProxy 做双向通信与性能验证前面讲的都是 Unity 主动调 Java但实际项目里经常需要 Java 主动通知 Unity——比如蓝牙收到数据、下载进度更新。除了UnitySendMessage更优雅的方式是AndroidJavaProxy它能在 C# 侧实现 Java 接口让 Java 直接回调 C# 对象。5.1 用 AndroidJavaProxy 实现接口回调先在 Java 侧定义一个接口package com.example.nativebridge; public interface ProgressListener { void onProgress(int percent); void onComplete(String result); }然后在 Java 类里持有这个接口并触发public class Downloader { private ProgressListener listener; public void setListener(ProgressListener listener) { this.listener listener; } public void startDownload() { // 模拟下载过程 for (int i 0; i 100; i 20) { if (listener ! null) { listener.onProgress(i); } } if (listener ! null) { listener.onComplete(下载完成); } } }Unity 侧用AndroidJavaProxy实现这个接口using UnityEngine; public class DownloadProxy : AndroidJavaProxy { // 构造函数传入 Java 接口的完整类名 public DownloadProxy() : base(com.example.nativebridge.ProgressListener) { } // 方法名和签名必须和 Java 接口一致 void onProgress(int percent) { Debug.Log($下载进度{percent}%); } void onComplete(string result) { Debug.Log($结果{result}); } } public class DownloadTest : MonoBehaviour { void Start() { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { AndroidJavaObject downloader new AndroidJavaObject(com.example.nativebridge.Downloader); downloader.Call(setListener, new DownloadProxy()); downloader.Call(startDownload); } } }这里的关键是AndroidJavaProxy的构造函数参数必须是 Java 接口的完整类名C# 侧的方法名和参数类型要和接口声明严格对应。如果 Java 接口方法有返回值C# 侧也要返回对应类型否则会抛NoSuchMethodError。5.2 验证调用是否真的生效光看日志不够我一般会用三个手段交叉验证第一在 Java 侧加Log.d(NativeBridge, method called)用adb logcat -s NativeBridge过滤第二在 Unity 侧用Debug.Log打时间戳确认调用顺序第三用 Android Studio 的 Profiler 看方法耗时如果单次调用超过 5ms就要考虑优化。5.3 一个容易忽略的细节线程与生命周期AndroidJavaProxy的回调可能发生在任意线程如果回调里要操作 Unity 的 GameObject必须切回主线程。常见做法是用一个线程安全的队列缓存消息在Update里消费using System.Collections.Concurrent; using UnityEngine; public class DownloadProxy : AndroidJavaProxy { // 线程安全队列Java 线程写入主线程读取 public ConcurrentQueuestring messageQueue new ConcurrentQueuestring(); public DownloadProxy() : base(com.example.nativebridge.ProgressListener) { } void onProgress(int percent) { messageQueue.Enqueue($进度{percent}%); } void onComplete(string result) { messageQueue.Enqueue($完成{result}); } } public class DownloadTest : MonoBehaviour { private DownloadProxy proxy new DownloadProxy(); void Update() { // 主线程消费队列 while (proxy.messageQueue.TryDequeue(out string msg)) { Debug.Log(msg); } } }另外Activity 销毁时记得在 Java 侧把 listener 置空否则可能内存泄漏。我习惯在 Unity 的OnDestroy里调一下downloader.Call(setListener, null)虽然多一行代码但能避免很多玄学崩溃。从那以后我每次接原生模块都会先把 AAR 单独打一个空场景跑通确认类加载、方法调用、回调链路都没问题再往业务里集成。这个习惯帮我省下了至少三次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑