ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity调安卓原生:从环境配置到实战避坑指南

Unity调安卓原生:从环境配置到实战避坑指南 简介面向Unity开发者的安卓原生交互入门工具集汇集UnityActivity、UnityAppContext、PackageManager、RunOnUIThread等基础调用方法并覆盖Toast提示、Log输出、Java与C#字符串互转等高频操作。除基础能力外还封装了获取应用列表、判断服务或应用是否运行、打开/安装/卸载App、发送广播以及查询WiFi状态、安卓版本、原生类型ID、内置SD卡路径等系统信息的方法适合刚接触Unity调用原生安卓的开发者快速上手、按需查阅。整个压缩包仅48KB共45个文件以meta与asset工程文件为主另有7个C#脚本、2个xml配置、1个Unity场景和1个jar包结构精简便于直接导入Unity工程参考或抽取核心方法内置测试场景也能帮助理解调用流程。目前已有743人学习使用对于希望打通Unity与安卓原生层、减少重复造轮的开发者来说是一份轻量实用的代码工具箱。 Unity项目做久了你迟早会碰到一个逃不掉的需求——调安卓原生。不管是接第三方 SDK、打开系统设置、做串口通信还是单纯弹个 Toast纯 C# 搞不定必须跟安卓底层打交道。偏偏这块资料又散官方文档讲得笼统社区帖子各说各话新手很容易卡在“先装什么、再配什么、最后怎么调”的第一公里。我整理这份工具集就是为了给第一次接触“Unity 调原生安卓”的开发者一条能直接踩实的路环境准备、核心 API 用法、五个高频实战案例、一份报错排查手册全部按入门顺序排好。内容不算深但够你从零把功能跑起来之后遇到问题也知道该往哪儿查。1. 环境准备与版本选择1.1 Unity 安装与 Android 构建链路动手之前先把环境弄干净。我见过太多人代码写对了结果包都打不出来根子就在 Unity 安装时候没把 Android 模块装全。用 Unity Hub 安装 Unity 的时候记得在模块选择页勾上Android Build Support下面三个子项建议全选SDK NDK Tools、OpenJDK、Android SDK NDK Tools不同版本叫法略有差异。这样 Unity 会自带一套 Android SDK、NDK 和 JDK省去自己配环境变量的麻烦。如果你机器上已经装了 Android Studio也可以后续在 Player Settings 里手动指定 SDK 路径但没必要Unity 自带的那套足够用于常规打包。装完以后有个容易被忽略的点License 没激活Android 模块装了也白装。新手经常会遇到这样的提示No valid Unity editor license found. Please activate your license.这不是项目出问题就是 Unity 编辑器许可证没激活。打开 Unity Hub 登录账号把许可证激活好再回编辑器这个问题自然就消失了。环境就绪后建议先理解一下 Unity 到安卓的构建链路后面排查问题全靠它C# 脚本 → IL2CPP / Mono 编译 → 生成 Android 工程 → Gradle 打包 APK/AAB ↑ NDK 负责编译原生 C/C 库.so这个链路里有两个关键点。第一你的 C# 代码最终是变成 IL2CPP 的 C 代码再编成 so还是直接用 Mono 跑这会影响后面 JNI 调用和动态库的兼容性。第二Gradle 打包阶段会把Assets/Plugins/Android下的 AAR、so、Manifest 合并进最终 APK。很多“Unity 调安卓”的报错追根究底都是这两个环节出了问题。1.2 API Level 到底选多少从目标 35 说起环境配好以后你会看到 Player Settings 里有一堆版本号最常被问到的是Minimum API Level和Target API Level。Minimum API Level最低支持到哪个安卓版本低于这个版本的系统装不了你的 App对应的是清单文件里的minSdkVersion。Target API Level告诉系统你的应用在当前目标版本上经过测试对应的是targetSdkVersion。高版本系统会根据这个值决定是否开启兼容性行为。现在新项目里Unity 默认把 Target 设得比较高热门搜索里也常看到“unity 提高 minimum api level target api level 到api35”。这个 35 对应的就是安卓 15。为什么要提上去核心原因是应用商店的审核要求逐年收紧Google Play 会要求新应用和更新包在一定时间后必须把 Target API Level 升到指定版本否则不给过审。其次是权限模型变化安卓 6 开始动态权限、安卓 8 的通知渠道、安卓 10 的存储分区、安卓 13 的通知权限这些新行为都跟 Target API Level 绑定。你 Target 定得低老用户可以不受影响地跑旧逻辑但新用户在新系统上会碰到一堆“兼容模式”最典型的就是文件路径访问权限被限制还有后台弹窗直接被系统拦截。我的建议很直接中小型项目、或者只面向固定设备比如一体机应用的Minimum API Level 拉到 API 28Android 9起步比较稳Target API Level 直接设成当前能支持的最高值比如 35。如果项目已经有存量用户非要兼容 Android 8 以下的设备那 Minimum 可以放低但你得承担每升一个 Target 版本就要重新适配一轮权限和行为的隐形成本。这个取舍立项时就该定下来别等包打完了再返工。另外提一句很多设备是 Android 9 的定制 ROM热词里那个“安卓 9 原生系统设置下载”大概就是指想在 Unity 里调起这类设备上的系统设置页这个后面实战案例里会给出具体跳转方法。2. 核心 APIUnity 调安卓的三种姿势2.1 AndroidJavaObject / AndroidJavaClass最常用的 JNI 桥Unity 之所以能调安卓原生靠的是一套封装好的 JNI 桥接类AndroidJavaObject和AndroidJavaClass。原理不复杂——Unity 反射拿到 Java 类的信息通过 JNI 在 C 层调用 Java 层的方法再回传结果给 C#。你不需要自己写 C/C 的 JNI 代码只要用这两个类就行。AndroidJavaClass对应 Java 里的静态类用于调用静态方法、访问静态字段AndroidJavaObject对应某个实例对象用于调用实例方法。区分很简单你要操作的东西是“类”还是“对象”。先看最常见的例子Unity 里弹一个安卓原生 Toast。using UnityEngine; public class NativeToast : MonoBehaviour { public void ShowToast(string message) { // 1. 拿到 Unity 当前的 Activity using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) // 2. 引用 Toast 类调用静态方法 makeText using (var toastClass new AndroidJavaClass(android.widget.Toast)) { // 3. 拼接参数 var toast toastClass.CallStaticAndroidJavaObject( makeText, activity, message, toastClass.GetStaticint(LENGTH_SHORT)); // 4. 弹出来 toast.Call(show); } } }这里有几个细节值得拆一下。第一Unity 的 Activity 怎么拿com.unity3d.player.UnityPlayer类的currentActivity静态字段Unity 已经帮你存好了当前的 Activity 引用。几乎所有需要 Context 的调用第一步都是先拿它。第二参数的传递CallStaticT(方法名, 参数...)T 是返回值类型。如果你写CallStaticAndroidJavaObjectUnity 会帮你把 Java 返回的对象包装成 AndroidJavaObject 继续用如果方法返回 void就写Call不写泛型。参数同样会自动做类型转换Toast.makeText(Context, CharSequence, int)对应 C# 就是(AndroidJavaObject, string, int)。第三using 释放AndroidJavaObject 和 AndroidJavaClass 都是 IDisposable用完记得释放。方法里面用using包起来避免在这个类实例持有较多、循环调用时产生不必要的引用堆积。不过注意currentActivity是拿到的引用不能直接 Dispose 掉——它是 Unity 的全局对象释放了后续调用全崩。我在代码里没有对它单独 using就是为了避免这个坑。2.2 用 AndroidJavaProxy 接住 Java 回调有“Unity 调 Java”就一定有“Java 调回来”。安卓好多 API 是回调式的比如定位监听、权限申请结果、SDK 初始化结果C# 这边得提供一个对象让 Java 层能回调。Unity 给的方案是AndroidJavaProxy你在 C# 里写一个类继承它传入要实现的 Java 接口名然后按 Java 接口的方法签名在 C# 里定义对应方法。Java 回调过来时Unity 会通过 JNI 反向调进 C#。using UnityEngine; public class NativeCallback : AndroidJavaProxy { public NativeCallback() : base(com.example.MySdkCallback) { } // Java 接口里声明了 void onResult(String result) public void onResult(string result) { Debug.Log(Java 回调结果: result); } }使用的时候var sdkClass new AndroidJavaClass(com.example.MySdk); sdkClass.CallStatic(init, gameObject.name, new NativeCallback());重点是那个base(com.example.MySdkCallback)这个字符串必须是 Java 接口的完整类名。onResult的方法名、参数类型也必须和 Java 接口定义完全一致Unity 的 AndroidJavaProxy 是通过方法名 参数签名去匹配的不一致就会抛java.lang.NoSuchMethodException或者AndroidJavaException。我在实际项目里用这个方法接过蓝牙 SDK 的回调踩过一个典型坑Java 回调发生在非主线程而回调里我又去更新 Unity 的 UI结果 Unity 的 UI 系统直接罢工。原因很简单C# 回调回来后还在 Java 那个线程里Unity 的 UI 更新必须在主线程。解决办法是回调里只存数据、或者用UnityMainThreadDispatcher之类的工具切回主线程再操作 UI。2.3 Activity、Context 与主线程的坑原生安卓开发有三大概念Unity 开发者第一次接触时最容易混淆Activity、Context、主线程。Context 是安卓的上下文启动 Activity、弹 Toast、拿系统服务都需要它。Unity 的currentActivity其实就是一个带 Context 能力的 Activity所以绝大多数系统级调用你拿它当 Context 传进去就对了。如果遇到某些接口要求getApplicationContext()也可以通过activity.CallAndroidJavaObject(getApplicationContext)获取。主线程问题是新手崩溃重灾区。安卓的 UI 只能在主线程操作Unity 的主线程和安卓的主线程在 JNI 桥接时通常是一致的但如果你自己开了 C# 的Thread或者 Java 回调发生在别的工作线程再直接去操作 UI 或者调用某些只允许主线程的 API就会闪退或静默失败。标准做法是让安卓帮我们切线程activity.Call(runOnUiThread, new AndroidJavaRunnable(() { // 确保在主线程执行 ShowToast(我在主线程); }));AndroidJavaRunnable是 Unity 提供的另一个代理类专门用来包装 Runnable 接口。这个组合拳记牢了能省掉后面一大半跟线程相关的 crash。3. 实战案例从系统能力到 SDK 接入3.1 基础系统能力Toast、震动、剪贴板这一节主要是抄作业用的我把日常最常用的几个系统能力封装思路直接列出来都是基于前面的currentActivity。震动安卓有个Vibrator服务Unity 里可以这样调using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var vibrator activity.CallAndroidJavaObject(getSystemService, vibrator)) { vibrator.Call(vibrate, 300L); // 震动 300ms }注意API 31Android 12之后震动有了更严格的权限和新的VibrationManager接口老接口在高版本上可能失效或者需要声明权限。如果你的 Target API Level 已经到 35这块得按新版接口适配。剪贴板往系统剪贴板写内容using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var clipboard activity.CallAndroidJavaObject(getSystemService, clipboard)) using (var clipDataClass new AndroidJavaClass(android.content.ClipData)) using (var clipData clipDataClass.CallStaticAndroidJavaObject(newPlainText, label, text)) { clipboard.Call(setPrimaryClip, clipData); }这里有个小陷阱getSystemService传的字符串参数必须和安卓系统服务的常量名一致比如剪贴板是clipboard震动是vibrator你写错系统也不会提醒你Runtime 阶段直接返回 null 或者抛异常。我的习惯是先把这些常量在 C# 里定义成常量类注释上对应安卓版本方便以后查。3.2 跳转原生系统设置Android 9 的适配很多工具型应用需要在 Unity 里一键跳进系统设置页面比如打开 Wi-Fi 设置、应用详情页、定位权限页。原理是通过 Intent 拉起系统的 Activityusing (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var intent new AndroidJavaObject(android.content.Intent, android.settings.SETTINGS)) { intent.CallAndroidJavaObject(addFlags, 0x10000000); // FLAG_ACTIVITY_NEW_TASK activity.Call(startActivity, intent); }几个常用的 action 常量android.settings.SETTINGS系统设置主页android.settings.WIRELESS_SETTINGSWi-Fi 设置页android.settings.APPLICATION_DETAILS_SETTINGS应用详情页需要带上package:包名的 Uriandroid.settings.LOCATION_SOURCE_SETTINGS定位服务开关页Android 9API 28之后部分设置页跳转返回行为有变化但核心 Intent 机制没动这套代码在 Android 9 到 15 上都能用。热词里“安卓 9 原生系统设置下载”那个问题本质上不是下载系统设置而是想调起系统设置页一个startActivity就搞定了。需要提醒的是addFlags(0x10000000)这一行别省略。Unity 的 Activity 启动栈比较复杂不从新任务栈启动 Activity有些设备上会直接没反应。3.3 串口通信Unity 的外设之路Unity 调硬件设备绕不开串口。工业一体机、Pico 这类设备、扫码枪、传感器基本都是串口通信。Unity 纯 C# 是没法直接操作串口的得走安卓的串口方案。业界最通用的做法是用开源的android-serialport-api那套方案它包含一个用 NDK 编译的 libserial_port.so以及SerialPort.java、SerialPortFinder.java两个 Java 类。你的任务是把这套东西编译成 AAR或者把 Java 编译成 dex、so 拷进去放到 Unity 的Assets/Plugins/Android下。Unity 这边通过AndroidJavaObject直接调用SerialPort类的open()、read()、write()方法。C# 侧示意的封装是这样using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var serialPortClass new AndroidJavaClass(android_serialport_api.SerialPort)) { var serialPort serialPortClass.CallStaticAndroidJavaObject( open, activity, /dev/ttyS1, 9600, 0); serialPort.Call(write, System.Text.Encoding.ASCII.GetBytes(AT\r\n)); var buffer new byte[1024]; var len serialPort.Callint(read, buffer); }实际上SerialPort.open未必是静态方法这里只是表达思路具体要看你的封装怎么写的。串口开发最大的坑是设备路径和权限一体机上串口往往在/dev/ttyS0、/dev/ttyAML1这类节点普通应用没有读写权限得 root 或者用chmod提权这就不是你 Unity 代码能解决的事了。我在实际项目里的做法是让运行环境先通过系统服务把串口节点权限放开Unity 这边只负责收发数据权限问题单独跟设备厂商沟通解决。还有就是读串口千万别在主线程死等。串口数据是持续的你在 Update 里阻塞读Unity 主线程直接卡死。正确姿势是放到 C# 的Thread里循环读读到数据就丢到主线程队列里去处理。3.4 第三方 SDK 接入以抖音侧边栏为例热词里那个“unity 抖音 侧边栏 接入流程”是很多做互娱、营销类项目的开发者会遇到的场景。抖音开放平台提供了侧边栏 SDK允许第三方 App 在抖音侧边栏挂入、并进行内容跳转和分享。问题是抖音的 SDK 是给安卓原生工程用的Unity 工程没法直接用必须走桥接。标准的接入流程分四步在抖音开放平台创建应用拿到 client key配置好包名和签名信息。用 Android Studio 建一个库工程集成抖音侧边栏 SDK在这个库里写一个薄薄的封装类把初始化、校验、拉起等操作暴露成静态方法。把这个库导出成 AAR连同 SDK 的 AAR 一起放到 Unity 工程的Assets/Plugins/Android目录。Unity 打包时会把它们合并进最终 APK。C# 侧通过AndroidJavaClass调用你写的封装类初始化时把 Unity 的 Activity 传进去后续调用就按原生接口来。写到这里要提醒一点第三方 SDK 的初始化大都是异步回调回调里如果需要刷新 Unity 侧的状态记得回到主线程。另外这类 SDK 的混淆要特别小心经常出现“Debug 包正常、Release 包一调就崩”的情况十有八九是 ProGuard 把 SDK 的类混淆掉了需要在混淆规则里把抖音 SDK 相关的包名 keep 住。如果你不想每次接 SDK 都写一遍封装修理就建立一个内部统一的“Unity 调原生模块”仓库把常用的 SDK 和系统能力按同样风格封装项目多了以后复用率相当高。3.5 “系统托盘”在安卓上的正确打开方式热词里还有个“在 unity 中实现系统托盘”。这里必须先纠正一个概念安卓没有 Windows 意义上的系统托盘。你看到的桌面端系统托盘在安卓上的对应物是通知栏常驻通知和悬浮窗。Unity 应用如果在后台需要保持运行、或者要告诉用户“我还活着”正确做法是发一条常驻通知配合前台服务Foreground Service。通过 JNI 创建通知的基本流程API 26Android 8以上先建通知渠道NotificationChannel再构造Notification.Builder最后通过NotificationManager.notify()发布。using (var manager activity.CallAndroidJavaObject(getSystemService, notification)) { if (GetApiLevel() 26) // Android 8 { var channel new AndroidJavaObject(android.app.NotificationChannel, unity_service, Unity Service, 2); // IMPORTANCE_HIGH 2 manager.Call(createNotificationChannel, channel); } var builder new AndroidJavaObject(android.app.Notification$Builder, activity, unity_service); builder.CallAndroidJavaObject(setContentTitle, Unity 运行中); builder.CallAndroidJavaObject(setContentText, 点击返回应用); builder.CallAndroidJavaObject(setSmallIcon, activity.Callint(getApplicationInfo).Getint(icon)); var notification builder.CallAndroidJavaObject(build); manager.Call(notify, 1001, notification); }悬浮窗则需要SYSTEM_ALERT_WINDOW权限而且国内各家 ROM 对这种权限管控非常严格体验一般。如果你的需求是“Unity 退到后台还能保持逻辑运行”优先考虑前台服务 常驻通知的组合别一上来就整悬浮窗。4. 常见报错与排查手册4.1 DLLNotFoundException: Unable to load DLL slua热词里出现频率极高的一个报错DLLNotFoundException: Unable to load DLL slua。这通常不是 Unity 调原生安卓的 JNI 问题而是你的热更新方案如 slua、tolua、xlua所用的 native 库没加载进来。报错的意思是运行时要加载slua.so或者libslua.so失败。排查思路按顺序来确认 so 文件是否在包里。用解压工具打开 APK看lib/arm64-v8a/目录下有没有对应的.so文件。没有的话说明 so 没被打进包检查它是不是在Assets/Plugins/Android/下的正确 ABI 目录里。确认 ABI 是否匹配。现在的安卓机基本都是 64 位如果包里只有 armeabi-v7a 的 so在 64 位进程上加载会失败。Unity 的 Player Settings → Other Settings → Target Architectures 里确认勾了 ARM64而且 so 有对应的 ARM64 版本。确认 IL2CPP / Mono 是否一致。某些热更方案只支持 Mono 编译切到 IL2CPP 就会出问题。反过来也有只支持 IL2CPP 的你要根据方案的文档来。这类问题有个共性Debug 时编辑器里跑得好好的一上真机就找不到 so因为编辑器和真机的运行环境差别太大。建议项目早期就先在真机上跑通一次热更方案的“空转”流程不要等到业务代码都写完再来解决这个基础问题。4.2 混淆把 JNI 调用搞挂Unity 打包发布时如果开了代码混淆ProGuard / R8你从 C# 侧调用的那个 Java 封装类很可能被混淆改名JNI 反射就找不到原来的类和方法了。表现是Debug 包一切正常Release 包一调你的原生方法就抛ClassNotFoundException、NoSuchMethodException这类异常或者直接崩。解决办法是在混淆规则里把这些类 keep 住-keep class com.yourcompany.unitybridge.** { *; } -keep class com.unity3d.player.UnityPlayer { *; } -keep class android_serialport_api.** { *; }具体来说凡是 Unity C# 侧通过字符串类名引用到的 Java 类建议全部 keep。我有一次就是漏了某 SDK 的一个内部回调类Release 包在初始化时静默失败查了很久才定位到混淆头上从那以后每次接第三方 SDK 的第一件事就是先把混淆规则写好。4.3 宏定义与多平台代码调原生安卓肯定绕不开平台判断。Unity 提供了内置宏UNITY_ANDROID表示安卓平台UNITY_EDITOR表示编辑器环境。这样写Android 打包时走原生调用编辑器里走模拟分支避免在编辑器里一跑就崩。#if UNITY_ANDROID !UNITY_EDITOR // 真实安卓设备上的 JNI 调用 #else Debug.Log(编辑器环境下跳过原生调用); #endif这里还要警惕 Unity 里另一种“宏定义”——Scripting Define Symbols。用过的开发者会在里添加自定义宏比如USE_NATIVE_BRIDGE来开关原生桥接逻辑。它是一个字符串列表在 Player Settings → Scripting Define Symbols 里配置多个宏用分号分隔。这个属于项目管理层的规划建议尽早统一别每个开发者本地加一套合并代码时冲突不断。4.4 构建与打包IL2CPP、ABI 与 API Level 的联动排查Target API Level和Minimum API Level会影响你能调用的系统 API 集合但真正让初学者崩溃的是它跟 IL2CPP 的联动关系。比如你的代码在高版本 API 上调用了一个新方法但Minimum API Level设置得低打包时如果没做好版本保护低版本设备上调用就会遇到VerifyError或者直接崩溃。对策有两个方向一是用Build.VERSION.SDK_INT在 C# 里判断系统版本低版本走旧逻辑二是把Minimum API Level定在实际需要支持的最低版本之上别为了“看起来兼容更多机型”硬压到 API 26 以下——那会让你的代码写起来处处掣肘。IL2CPP 方面新版 Unity 默认会生成 ARM64 和 ARMv7 两套二进制体积翻倍但兼容性更好。如果只是投放到指定设备比如 Pico 一体机、特定型号的安卓盒子可以把 Target Architectures 只留一套APK 体积能小不少。4.5 报错速查表最后汇总一份我平时排查时常用的对照表遇到同类问题可以直接对着查报错或现象常见原因处理方向No valid Unity editor license foundUnity 许可证未激活Unity Hub 登录激活许可证DLLNotFoundException: xxxnative 库缺失或 ABI 不匹配检查 APK 内 so、ABI 设置、IL2CPP/Mono 匹配AndroidJavaException: Class not found类名写错 / 混淆 / 封装类未打包检查类名全称、混淆 keep、AAR 放置位置Method xxx not found方法名或签名不匹配、混淆核对 Java 方法签名检查混淆规则UI 无响应或闪退Release 包混淆导致 SDK 类调用失败补充 ProGuard keep 规则Toast / 系统调用无反应Activity 未加 NEW_TASK 标记IntentaddFlags(0x10000000)真机串口无数据串口节点权限不足系统层放开/dev/tty*权限后台被系统杀掉没有前台服务用前台服务 常驻通知这个表不算全但覆盖了新手前三个月会踩到的大部分坑。我在实际项目里最深的体会是Unity 调原生安卓这件事本身 API 并不多难的是那套跨语言、跨线程、跨构建环境的“隐形规则”。环境装好、API level 定明白、主线程管住、混淆规则提前写好剩下的大部分问题都能在几分钟内定位。最后再分享一个小习惯每次接到新项目我都会先在空场景里跑通一个“原生调用冒烟测试”Toast、震动、跳设置页三件套各来一次确认链路通了再开始写业务这能帮你把“环境问题”和“业务问题”彻底隔开。本文还有配套的精品资源点击获取
返回列表