简介这份工具集面向需要在 Unity 中调用原生安卓能力的开发者尤其适合刚接触 Unity 与 Android 交互、希望快速跑通基础链路的入门者。内容围绕 UnityActivity、UnityAppContext、PackageManager、RunOnUIThread 等基础方法展开并覆盖 Toast、Log 以及 Java 与 C# 字符串互转等常用操作同时整理了获取 App 列表、判断服务与应用是否运行、打开/安装/卸载 App、发送广播以及获取 WiFi 状态、安卓版本、原生类型 ID、内置 SD 卡路径等实用封装。资源共 45 个文件以 cs 脚本、asset 配置、meta 与 xml 文件为主另含 jar 包和 unity 场景压缩包约 48KB结构轻量便于直接导入参考。目前已有 743 人学习适合作为 Unity 调用原生安卓的入门跳板与日常开发速查工具。1. 从一次打包翻车说起这套 Unity 调用原生安卓工具集到底解决什么去年帮一个做跨平台应用的小团队看包Unity 侧功能都跑通了一打安卓真机就出问题权限弹窗不弹、Toast 不显示、拿不到系统电量日志里全是ClassNotFoundException。他们一开始想用纯 C# 硬扛结果发现安卓那套东西绕不过去最后还是得回到 Unity 调用原生安卓这条路上。这套「Unity 调用原生安卓 入门参考 工具集」就是干这个的——它把 Unity 和 Android 之间那层 JNI 桥怎么搭、AAR 怎么打、权限怎么回调、常见 API 怎么封装整理成一套能直接照着复现的参考工程和工具脚本。适合两类人刚接触 Unity 安卓混合开发、被 JNI 报错劝退的新手以及想把手头零散调用代码收敛成规范工具集的熟手。下面我按「原理立住 → 动手复现 → 避坑排查 → 进阶封装」的顺序拆一遍中间该贴的代码和参数一个不省。2. Unity 与 Android 的通信底座JNI、AAR 与 AndroidJavaClass 怎么串起来2.1 为什么不能只用 C#非得走 JNIUnity 的 C# 运行在 Mono/IL2CPP 上安卓系统 API 是 Java/Kotlin 写的两者不在一个运行时里。想让 Unity 拿到安卓原生的能力——读取Build信息、调用Vibrator、申请运行时权限、拉起系统分享——中间必须有一座桥这座桥就是 JNIJava Native Interface。Unity 把这层桥封装成了AndroidJavaClass、AndroidJavaObject、AndroidJavaProxy三个类你写 C# 的时候感觉像在调 Java底层其实是 JNI 在传引用、转类型。常见做法是把要用的安卓逻辑写成一个 Android Library 模块编译成 AAR丢进 Unity 的Assets/Plugins/Android目录然后在 C# 里用AndroidJavaClass找到对应的 Java 类去调。这套工具集的价值就在于它把「Java 侧怎么写、AAR 怎么出、C# 侧怎么调」这条链路固化成了模板不用每次从零配 Gradle。2.2 环境与目录先把工程骨架搭对动手前先确认版本边界这是最容易翻车的地方。Unity 2019 之后安卓插件目录结构变过2021 LTS 和 2022 LTS 对 Gradle 模板的处理也不一样。我一般按下面这套来项目建议值说明Unity 版本2021.3 LTS 及以上低于此版本 Gradle 模板差异大AAR 冲突多Android Gradle Plugin7.0与 Unity 内置 Gradle 对齐避免重复类minSdkVersion24覆盖绝大多数真机权限 API 稳定targetSdkVersion33注意 Android 13 的通知权限变化AAR 存放路径Assets/Plugins/Android放错目录 Unity 不会打进包目录骨架大致是这样工具集里也是这个结构Assets/ Plugins/ Android/ unitycallandroid.aar # 编译好的安卓库 AndroidManifest.xml # 权限与组件声明 Scripts/ AndroidBridge.cs # C# 调用入口 PermissionHelper.cs # 权限回调封装提示AndroidManifest.xml如果和 AAR 里的清单冲突Unity 打包时会合并合并规则以主工程为准权限声明建议统一写在主工程的清单里别散落在各处。2.3 Java 侧写一个能被 Unity 找到的类安卓库这边核心是暴露一个静态方法给 C# 调。工具集里给的是一个UnityBridge类负责返回设备信息和触发 Toastpackage com.example.unitybridge; import android.app.Activity; import android.content.Context; import android.widget.Toast; import android.os.Build; public class UnityBridge { // 静态方法C# 侧通过 AndroidJavaClass.CallStatic 调用 public static String getDeviceModel() { return Build.MANUFACTURER Build.MODEL; } // 需要 Activity 上下文的方法用 runOnUiThread 保证在主线程弹 Toast public static void showToast(final Activity activity, final String msg) { activity.runOnUiThread(new Runnable() { Override public void run() { Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); } }); } }逻辑说明getDeviceModel是纯静态、无上下文依赖的方法C# 侧调用最简单showToast涉及 UI安卓要求必须在主线程操作所以用runOnUiThread包一层否则会抛CalledFromWrongThreadException。参数上Activity要从 Unity 侧传进来不能在里面 new因为安卓的 Activity 生命周期由系统管。编译这个模块用 Gradle./gradlew :unitybridge:assembleRelease # 产物在 unitybridge/build/outputs/aar/unitybridge-release.aar把 AAR 复制到Assets/Plugins/AndroidUnity 会自动识别。注意 AAR 里如果带了classes.jar之外的依赖要么一起打进去要么在 Unity 侧用mainTemplate.gradle补依赖否则运行时报NoClassDefFoundError。2.4 C# 侧AndroidJavaClass 的正确打开方式C# 这边调用要区分「静态方法」和「实例方法」工具集里AndroidBridge.cs把两种都封了using UnityEngine; public class AndroidBridge { private const string BRIDGE_CLASS com.example.unitybridge.UnityBridge; // 调用静态方法用 AndroidJavaClass public static string GetDeviceModel() { using (var bridge new AndroidJavaClass(BRIDGE_CLASS)) { return bridge.CallStaticstring(getDeviceModel); } } // 调用需要 Activity 的方法先拿当前 Activity再传进去 public static void ShowToast(string msg) { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var bridge new AndroidJavaClass(BRIDGE_CLASS)) { bridge.CallStatic(showToast, activity, msg); } } }逻辑说明AndroidJavaClass对应 Java 的类AndroidJavaObject对应实例。currentActivity是 Unity 暴露的静态字段拿到它才能做需要上下文的操作。using包裹是为了及时释放 JNI 引用长时间不释放会累积导致内存问题这点新手很容易忽略。参数上CallStaticT的泛型是返回类型传参顺序必须和 Java 方法签名严格一致类型对不上会直接崩。3. 权限、回调与线程把运行时权限和异步结果接回 C#3.1 运行时权限为什么不能只写清单Android 6.0 之后危险权限相机、定位、存储、录音光在AndroidManifest.xml里声明没用必须在运行时动态申请用户点了「允许」或「拒绝」之后还要拿到回调。Unity 自己不带这套回调机制得靠原生侧实现onRequestPermissionsResult再通过 JNI 把结果传回 C#。这是 Unity 调用原生安卓里最容易断链的一环。工具集的做法是Java 侧写一个PermissionHelperC# 侧用AndroidJavaProxy接收回调。先看 Java 侧package com.example.unitybridge; import android.app.Activity; import android.content.pm.PackageManager; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public class PermissionHelper { public static final int REQ_CODE 1001; public static boolean hasPermission(Activity activity, String permission) { return ContextCompat.checkSelfPermission(activity, permission) PackageManager.PERMISSION_GRANTED; } public static void requestPermission(Activity activity, String permission) { ActivityCompat.requestPermissions(activity, new String[]{permission}, REQ_CODE); } }逻辑说明hasPermission先查当前状态避免重复申请requestPermission用ActivityCompat发起申请REQ_CODE是自定义请求码回调时靠它区分是哪个请求。注意这里依赖了androidx.coreAAR 打包时要把这个依赖带上否则运行时报找不到ActivityCompat。3.2 用 AndroidJavaProxy 接住权限回调C# 侧要接住onRequestPermissionsResult得继承AndroidJavaProxy把 Java 接口映射过来using UnityEngine; using System; public class PermissionCallback : AndroidJavaProxy { private Actionstring, bool _onResult; // 构造时传入要实现的 Java 接口全名 public PermissionCallback(Actionstring, bool onResult) : base(androidx.core.app.ActivityCompat$OnRequestPermissionsResultCallback) { _onResult onResult; } // 方法名和签名必须和 Java 接口一致 void onRequestPermissionsResult(int requestCode, string[] permissions, int[] grantResults) { for (int i 0; i permissions.Length; i) { bool granted grantResults[i] 0; // 0 PERMISSION_GRANTED _onResult?.Invoke(permissions[i], granted); } } }逻辑说明AndroidJavaProxy的构造参数是 Java 接口的全限定名$用于表示内部接口。方法名、参数类型、返回类型必须和 Java 侧完全对应大小写都不能错否则回调静默失败——这是最阴的坑不报错但就是不触发。grantResults[i] 0里的 0 对应PackageManager.PERMISSION_GRANTED别硬编码成别的值。3.3 线程与生命周期回调回来时 Activity 可能已经没了安卓的权限回调是异步的用户可能过几秒才点这期间如果 Unity 场景切换、Activity 重建回调回来时上下文就失效了。血泪经验是回调里不要直接操作 UI 或假设 Activity 还在先判空再处理。工具集里在 C# 侧加了一层保护public static void RequestCamera(Actionbool callback) { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var helper new AndroidJavaClass(com.example.unitybridge.PermissionHelper)) { if (activity null) { callback?.Invoke(false); return; } bool has helper.CallStaticbool(hasPermission, activity, android.permission.CAMERA); if (has) { callback?.Invoke(true); return; } helper.CallStatic(requestPermission, activity, android.permission.CAMERA); // 实际回调通过 PermissionCallback 注册此处省略注册细节 } }逻辑说明先判activity是否为空再查权限已有权限直接回调没有才发起申请。参数上权限字符串必须用安卓标准常量写错一个字母就永远申请不到。这套流程跑通后相机、定位、录音都能套同一个模板。4. 避坑与排查Unity 调用原生安卓最常见的五类翻车4.1 现象运行时报 ClassNotFoundException原因通常是 AAR 没打进包或者包名和 C# 里写的对不上。Unity 只识别Assets/Plugins/Android下的 AAR放别处不生效另外 Java 包名大小写敏感com.example.UnityBridge和com.example.unitybridge是两个类。解决确认 AAR 路径正确用AndroidJavaClass前先打印一次类名核对打包后用 Android Studio 的 APK 分析器看classes.dex里有没有这个类。4.2 现象Toast 不显示日志无报错多半是没在主线程调用。安卓的 UI 操作强制在主线程Unity 的 C# 回调线程不一定是主线程。解决Java 侧统一用runOnUiThread包一层或者 C# 侧用AndroidJavaObject拿到 Activity 后runOnUiThread。别指望它自己切线程它不会。4.3 现象权限弹窗弹了但 C# 收不到回调AndroidJavaProxy的方法签名和 Java 接口对不上是最常见原因。比如 Java 接口参数是int[]C# 写成int[]没问题但写成AndroidJavaObject就静默失败。解决对照 Java 接口源码逐字核对方法名和参数类型requestCode也要对上回调里加日志确认是否进入。4.4 现象打包报 duplicate class 冲突AAR 里带的依赖和 Unity 内置的androidx版本不一致或者多个 AAR 引了同一个库。解决在mainTemplate.gradle里用exclude group排掉重复依赖或者统一用implementation而非api减少传递依赖。工具集里建议只保留一份androidx.core。4.5 现象真机上偶发崩溃模拟器正常常见于 JNI 引用泄漏或 Activity 生命周期错配。AndroidJavaObject用完不Dispose长时间运行引用表爆掉。解决所有AndroidJavaClass、AndroidJavaObject都用using包裹回调里判空别在OnDestroy之后还去调原生方法。5. 进阶把零散调用收敛成可复用工具集5.1 用泛型封装统一的调用入口调多了会发现每个功能都写一遍AndroidJavaClass很啰嗦。我一般会封一个泛型入口把类名和方法名参数化public static T CallStaticT(string className, string method, params object[] args) { using (var clazz new AndroidJavaClass(className)) { return clazz.CallStaticT(method, args); } } // 用法 string model CallStaticstring(com.example.unitybridge.UnityBridge, getDeviceModel);逻辑说明params object[]让参数数量可变T指定返回类型。注意泛型不能是void无返回值的方法单独封一个CallStaticVoid。这样新增功能时只改类名和方法名不用重复写 JNI 样板。5.2 验证工具集是否真的生效别只在编辑器里跑编辑器根本不走 JNI。验证必须打真机包按这个清单过一遍验证项方法预期类能否找到调getDeviceModel返回真实机型字符串权限流程首次安装申请相机弹窗出现允许后回调 true拒绝分支拒绝后再次申请回调 false不崩溃线程安全连续弹 10 次 Toast全部显示无错乱内存循环调用 1000 次无持续增长无崩溃5.3 一个我踩过的坑和后来的习惯早期我图省事把AndroidJavaObject当普通 C# 对象缓存起来复用结果跑久了必崩查了半天才发现是 JNI 引用没释放。从那以后我每次写调用都强制走using回调里先判空再干活打包前必用真机跑一遍权限拒绝分支。这套工具集我建议你拿到后先别急着加功能把示例里的设备信息、Toast、相机权限三个跑通再照模板扩自己的 API。希望帮到你。本文还有配套的精品资源点击获取