ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity与Android原生应用深度集成:从aar库集成到双向通信实战

Unity与Android原生应用深度集成:从aar库集成到双向通信实战 1. 项目概述为什么要把Unity塞进原生Android App如果你是一个Android原生开发者或者是一个Unity游戏开发者可能都曾遇到过这样的场景一个成熟的Android应用需要加入一个3D商品展示、一个AR试妆功能或者干脆就是一个完整的游戏模块。这时候你面临两个选择要么用OpenGL ES或Vulkan从零开始手搓一个3D渲染引擎这无异于重新造轮子周期长、门槛高要么就是考虑将已经非常成熟的Unity引擎作为一个“模块”或“组件”集成到你的原生Android应用里。这听起来很美好一个App既能享受原生开发在性能、系统API调用和UI灵活性上的优势又能利用Unity强大的跨平台3D渲染、物理和资源管理能力。但实际操作起来你会发现这远不是简单地把两个APK打包在一起。它涉及到两个完全不同的运行时环境Android的Java/Kotlin与Unity的C#/IL2CPP如何共存、通信、共享生命周期以及资源如何管理、内存如何分配等一系列复杂问题。我接手过好几个需要做这种集成的项目从简单的3D模型查看器到复杂的AR导航应用。踩过不少坑也总结出了一套相对稳定可靠的集成方案。今天我就把这套从环境搭建、项目配置、双向通信到性能优化的完整流程结合我个人的实战经验毫无保留地分享出来。无论你是想为电商App增加3D展示还是为工具类App嵌入小游戏这篇文章都能给你提供一条清晰的路径。2. 集成方案选型与核心思路拆解在动手之前我们必须明确一点Unity集成到Android不是简单的“启动另一个App”而是要让Unity的UnityPlayerActivity或其子类成为你原生App的一个组成部分。主流方案有以下几种各有优劣。2.1 方案一Unity作为Libraryaar集成推荐这是目前最主流、最官方的集成方式。Unity允许你将整个项目导出为一个Android Archive.aar库文件。然后在你的原生Android Studio项目中像引入其他第三方库一样引入这个aar并在合适的时机启动一个承载了Unity运行时的Activity。核心优势耦合度低Unity部分被打包成独立的库原生项目结构清晰依赖管理方便。构建独立Unity项目和Android项目可以分开开发、分开构建。Unity开发者只需定期导出aar给Android团队即可。灵活性高原生App可以完全控制何时启动、如何配置Unity模块甚至可以同时管理多个不同的Unity模块。核心挑战通信桥梁Java/Kotlin与C#之间的双向通信需要自己搭建通常通过Android的JNI和Unity的AndroidJavaClass/AndroidJavaObject来实现。资源管理Unity场景、预制体、纹理等资源需要正确打包进aar并确保在运行时能被Unity运行时正确加载。生命周期同步需要确保Unity Activity的生命周期如暂停、恢复与宿主App的逻辑正确同步避免黑屏、卡死或资源泄露。2.2 方案二Unity导出Gradle项目后合并在Unity的Build Settings中选择Gradle作为构建系统并勾选Export Project。这会导出一个完整的Android Gradle项目。然后你可以手动将这个项目作为模块module导入到你的主Android Studio项目中。适用场景需要对Unity生成的Android项目进行深度定制例如修改AndroidManifest.xml添加自定义的Gradle插件或任务。项目结构相对简单且Android开发团队愿意直接维护这个合并后的项目。缺点项目合并过程繁琐容易产生依赖冲突。后续Unity项目更新时合并工作可能需要重做维护成本较高。2.3 方案三通过进程间通信IPC启动独立Unity应用将Unity部分编译成一个独立的APK通过Android的Intent显式或隐式来启动它。两者通过Intent传递参数通过文件、Socket或ContentProvider共享数据。为什么不推荐体验割裂启动另一个APK会有明显的延迟和界面切换用户体验不连贯。数据共享复杂进程间通信开销大且共享大量数据如3D模型非常困难。状态管理困难两个独立进程的状态难以同步例如用户从Unity模块返回时原生App的状态可能已经改变。结论对于绝大多数需要深度集成的场景方案一aar库集成是最佳实践。它平衡了灵活性、可维护性和用户体验。下文也将主要围绕此方案展开。3. 环境准备与Unity项目导出配置磨刀不误砍柴工一个正确的初始配置能避免后续80%的奇怪问题。3.1 Unity侧配置生成可集成的aar库安装必要的Unity模块确保你的Unity Hub中为当前项目使用的Unity版本安装了Android Build Support模块并且至少包含OpenJDK和Android SDK NDK Tools。Player Settings关键配置Other SettingsIdentification:Package Name: 这里填写的包名必须与你主Android应用的包名一致或者作为其子包如主应用com.company.appUnity模块可为com.company.app.unity。这是后续通信和资源访问的基础。VersionBundle Version Code: 与主应用协调即可。Configuration:Scripting Backend: 强烈建议使用IL2CPP。虽然Mono构建更快但IL2CPP能带来更好的性能、更小的包体通过代码裁剪以及更强的代码混淆保护。对于发布版本这是必选项。Target Architectures: 根据你的用户群体选择。通常ARMv7和ARM64是必选的。如果追求最小包体可以只选ARM64但会失去对老旧32位设备的支持。勾选Split APKs by target architecture可以帮助Google Play进行分包分发。Publishing Settings找到Minify选项。对于Release构建建议使用Proguard或R8如果导出Gradle项目来混淆代码减小包体积并增加反编译难度。你需要为Unity的C#代码准备相应的混淆规则文件。构建导出打开File Build Settings。选择Android平台点击Switch Platform。在Build System中选择Gradle。关键一步勾选底部的Export Project复选框。这是生成aar所需结构的前提。不要点击Build或Build And Run。点击旁边的Export按钮。选择一个空文件夹作为导出路径。注意直接使用Build生成的APK无法用于集成。必须通过Export方式生成Gradle项目才能从中提取aar库。3.2 从导出项目中提取aar库导出完成后进入你选择的导出文件夹。你会看到一个标准的Android项目结构。我们需要的aar文件位于[导出路径]/unityLibrary/build/outputs/aar/在这个目录下你会找到名为unityLibrary-release.aarRelease版或unityLibrary-debug.aarDebug版的文件。这个unityLibrary.aar就是我们需要集成到原生Android项目中的核心库。实操心得我习惯在Unity项目根目录创建一个Export文件夹每次导出都生成带日期版本号子文件夹如Export/20231027_MyModule并将对应的aar文件复制到原生项目的libs目录下。这样版本管理非常清晰。3.3 Android Studio项目侧准备创建或打开你的主Android应用项目。将上一步得到的unityLibrary.aar文件复制到主项目的app/libs/目录下如果没有libs文件夹就新建一个。修改app模块下的build.gradle文件通常是app/build.gradleandroid { // ... 其他配置 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: libs, include: [*.jar, *.aar]) // 确保包含aar implementation androidx.appcompat:appcompat:1.3.1 // ... 其他依赖 // 如果Unity项目使用了AndroidX确保版本兼容。Unity 2019 LTS及以上版本通常已支持AndroidX。 }同步Gradle。完成后你应该能在项目的External Libraries中看到unityLibrary的相关引用。4. 核心集成Activity封装与双向通信这是集成的核心部分我们要创建一个“容器”来承载Unity并建立两者对话的“桥梁”。4.1 创建自定义的Unity Player Activity你不能直接使用Unity导出的那个默认Activity。我们需要继承它并加入我们自己的逻辑。// 文件名CustomUnityActivity.kt package com.yourcompany.yourapp import android.content.Intent import android.os.Bundle import com.unity3d.player.UnityPlayerActivity class CustomUnityActivity : UnityPlayerActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 在super.onCreate之前可以传递一些参数给Unity // 例如通过Intent获取启动参数 val sceneName intent.getStringExtra(UNITY_SCENE_NAME) ?: MainScene // 这里可以将参数设置到Unity的全局变量中但需要通过C#脚本来读取更常用的方式是后续的通信方法。 super.onCreate(savedInstanceState) // onCreate之后Unity视图已经被添加到ContentView中 } override fun onResume() { super.onResume() // 处理恢复逻辑例如重新连接通信通道 } override fun onPause() { super.onPause() // 处理暂停逻辑 } override fun onDestroy() { // 释放资源断开通信 super.onDestroy() } // 一个示例方法从原生端调用发送消息到Unity fun sendMessageToUnity(gameObject: String, method: String, message: String) { // 使用UnityPlayer的静态方法 com.unity3d.player.UnityPlayer.UnitySendMessage(gameObject, method, message) } }同时需要在AndroidManifest.xml中声明这个Activity并为其配置主题和硬件加速等。activity android:name.CustomUnityActivity android:themestyle/UnityThemeSelector !-- Unity导出的主题 -- android:screenOrientationfullSensor android:hardwareAcceleratedtrue android:configChangesfontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen android:launchModesingleTask !-- configChanges很重要防止Unity在配置改变时重启 -- /activity4.2 建立双向通信桥梁通信是集成的灵魂。核心原理是利用Unity提供的AndroidJavaClass和AndroidJavaObject在C#中调用Java方法以及利用UnitySendMessage从Java/Kotlin调用C#方法。从Unity (C#) 调用 Android (Java/Kotlin)在Unity的C#脚本中using UnityEngine; public class NativeBridge : MonoBehaviour { // 调用Android原生Toast public void ShowNativeToast(string message) { // 获取当前Activity的上下文 AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); // 在主线程UI线程上运行 currentActivity.Call(runOnUiThread, new AndroidJavaRunnable(() { AndroidJavaClass Toast new AndroidJavaClass(android.widget.Toast); AndroidJavaObject context currentActivity.CallAndroidJavaObject(getApplicationContext); Toast.CallStaticAndroidJavaObject(makeText, context, message, Toast.GetStaticint(LENGTH_SHORT)).Call(show); })); } // 调用自定义的Native方法 public void FetchDataFromNative() { AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); // 假设你的CustomUnityActivity有一个getUserData方法 string userData currentActivity.Callstring(getUserData); Debug.Log(Received from Native: userData); // 处理接收到的数据... } }从 Android (Java/Kotlin) 调用 Unity (C#)在CustomUnityActivity.kt中我们已经有了sendMessageToUnity方法。调用方式如下// 在原生代码的某个地方例如一个按钮点击事件中 val intent Intent(this, CustomUnityActivity::class.java) intent.putExtra(UNITY_SCENE_NAME, ProductViewer) startActivity(intent) // 假设在CustomUnityActivity中我们想在加载完成后初始化Unity override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { // 延迟一帧发送消息确保Unity已初始化完毕 Handler(Looper.getMainLooper()).postDelayed({ sendMessageToUnity(SceneController, LoadProduct, product_123) }, 100) } }在Unity中需要一个名为SceneController的GameObject并且上面挂载了包含LoadProduct方法的脚本。// Unity C# Script: SceneController.cs using UnityEngine; public class SceneController : MonoBehaviour { public void LoadProduct(string productId) { Debug.Log($Loading product from native: {productId}); // 根据productId加载对应的3D模型、场景等 } }4.3 数据传递的进阶方案简单的字符串消息传递对于复杂数据如结构体、列表、JSON是不够的。通常有两种进阶方案JSON序列化这是最通用的方法。在Android端将数据转换为JSON字符串通过UnitySendMessage传递。在Unity端使用JsonUtilityUnity自带或Newtonsoft.Json需导入进行反序列化。反之亦然。文件或SharedPreferences共享对于更大的数据或需要持久化的配置可以约定一个文件路径或SharedPreferences键名。Android端将数据写入Unity端通过AndroidJavaClass调用Android API去读取。需要处理好同步和线程安全问题。重要提示UnitySendMessage是异步且弱引用的。这意味着调用后不保证立即执行。如果目标GameObject已经被销毁消息会丢失且不会报错。它只能在主线程被Unity处理。 因此对于关键的状态同步建议使用一种“确认-回调”机制或者通过轮询某个由C#脚本更新的Android静态变量来实现。5. 资源管理与性能优化实战集成后应用变成了“双核”系统内存和性能管理至关重要。5.1 内存管理避免OOM内存溢出Unity运行时和Android原生部分共享同一个进程的堆内存。一个常见的崩溃原因是Unity加载大型资源导致OOM连带整个App崩溃。策略监控与预警在CustomUnityActivity中实现ComponentCallbacks2接口监听onTrimMemory(int level)回调。当系统发出内存警告TRIM_MEMORY_RUNNING_CRITICAL等时主动通知Unity侧卸载不必要的资源。override fun onTrimMemory(level: Int) { super.onTrimMemory(level) if (level ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) { sendMessageToUnity(MemoryManager, OnLowMemory, level.toString()) } }在Unity中MemoryManager脚本可以据此释放AssetBundle、销毁未激活的GameObject、调用Resources.UnloadUnusedAssets()。纹理压缩格式选择在Unity的Player Settings中Texture Compression设置对APK大小和运行时内存占用影响巨大。针对AndroidETC2支持OpenGL ES 3.0以上设备是ASTC普及前的默认推荐对RGBA纹理支持好。ASTC如果目标设备支持大多数现代设备ASTC在质量和压缩率上通常优于ETC2。在Unity中可以选择不同块大小如ASTC 6x6来平衡质量和大小。回退策略务必设置ETC2 fallback为不支持ETC2的旧设备GLES2指定一个回退格式如16位RGB565否则在这些设备上纹理会显示错误。AssetBundle的使用与卸载不要将所有资源都放在Resources文件夹。对于从原生端动态加载的3D模型、场景使用AssetBundle。严格管理AssetBundle的加载(LoadAsset)和卸载(Unload(false)或Unload(true))。记住AssetBundle.Unload(false)只卸载AssetBundle文件镜像已加载的资产还在内存中Unload(true)会连资产一起卸载可能导致材质丢失。5.2 启动速度优化用户从原生界面点击按钮跳转到Unity模块如果黑屏时间过长体验会很差。优化点预加载与保活这是最有效的方案。在App启动后、用户可能进入Unity模块前在一个后台线程或IntentService中提前初始化一个轻量级的Unity运行时环境。这可以通过启动一个透明的、1x1像素的Unity Player Activity到后台或者使用更高级的UnityPlayer.init()在后台线程执行。这样当用户真正打开Unity界面时只需要进行场景加载跳过了引擎初始化的耗时。当然这会增加常驻内存占用需要权衡。场景轻量化Unity模块的启动场景第一个场景尽可能简单。只包含最必要的摄像机、灯光和一个加载管理器。其他内容通过AssetBundle异步加载。使用IL2CPP且启用引擎代码剥离在Player Settings - Publishing Settings - Code Stripping选择High。这能显著减小引擎代码体积加快加载。但要做好充分的测试确保没有用到被剥离的代码如通过反射调用的。构建时压缩方式在Build Settings中Compression Method选择LZ4HC而非默认的Default (LZMA)。LZ4HC压缩率更高生成的包体更小虽然构建时间稍长但用户下载和安装后解压加载的速度会更快。5.3 渲染与功耗平衡Unity模块可能持续运行3D渲染导致手机发热耗电快。策略帧率限制在Unity中通过Application.targetFrameRate 30;来限制帧率。对于非游戏类的3D展示30FPS通常已足够流畅并能大幅降低GPU负载和功耗。适时暂停当Unity Activity不可见onPause时除了调用UnityPlayer.pause(true)还可以通过发送消息通知Unity脚本主动降低更新频率、暂停粒子系统等。图形API选择在Player Settings中可以指定Graphics APIs的顺序。优先使用Vulkan如果设备支持它在许多Android设备上比OpenGL ES 3.x更高效。将OpenGL ES 3.0作为备选OpenGL ES 2.0作为保底。6. 调试、打包与疑难杂症排查集成开发中问题往往比纯Unity或纯Android开发更诡异。6.1 调试技巧Android Logcat是生命线在Android Studio中打开Logcat过滤Unity标签。Unity的所有Debug.Log、错误和异常都会输出到这里。这是诊断Unity侧问题的第一现场。启用Development Build和Script Debugging在Unity Build Settings中勾选Development Build和Script Debugging。这样构建出的aar可以通过Android Studio的Attach Debugger to Android Process功能选择你的App进程然后在Unity的Visual Studio或VS Code中打断点调试C#代码。Profiler远程连接勾选Development Build和Autoconnect Profiler。在Unity Editor中打开Profiler窗口选择你的移动设备就可以实时查看运行在手机上的Unity模块的性能数据CPU、GPU、内存、渲染等这对于性能优化至关重要。6.2 打包发布流程Unity侧使用Release配置确保IL2CPP、代码剥离、合适的纹理压缩都已设置。执行Export。提取aar从导出目录获取unityLibrary-release.aar。Android侧将新的aar替换app/libs/下的旧aar。在build.gradle中可以考虑将依赖从implementation files(libs/unityLibrary-release.aar)改为具体文件名以便版本控制。构建主APK或App Bundle在Android Studio中正常构建你的应用。如果使用App BundleUnity模块的所有so库和资源会自动被包含在相应的动态功能模块中。6.3 常见问题与解决方案实录问题1集成后启动Unity模块就崩溃Logcat报错dlopen failed: library libmain.so not found。原因Unity导出的aar中原生库.so文件的路径不对或者主项目的CMake/ndk配置冲突。排查解压aar文件查看jni/[abi]/目录下是否有libmain.so等库。检查主项目build.gradle中是否有packagingOptions排除了某些so文件。解决在主模块的build.gradle的android块内添加android { packagingOptions { pickFirst lib/armeabi-v7a/libmain.so pickFirst lib/arm64-v8a/libmain.so pickFirst lib/x86/libmain.so // 添加其他可能冲突的Unity so库 } }问题2从Unity调用Android方法时报Java.Lang.NoSuchMethodError。原因方法签名不匹配。Java/Kotlin中的方法名、参数类型、返回值必须与C#中调用时完全一致。特别是重载方法。排查仔细核对方法名。注意Kotlin中String?可空类型与JavaString的差异。对于静态方法确保使用CallStatic。解决在Android端将方法声明为JvmStaticKotlin伴生对象或staticJava并确保使用基础类型如int而非Int。问题3通信消息丢失Unity收不到UnitySendMessage的调用。原因 a) 目标GameObject在场景中不存在或名称拼写错误。 b) 目标GameObject是未激活的。 c) 目标脚本没有挂载在该GameObject上或者方法名拼写错误。 d) 在Unity未初始化完成如Awake、Start未执行时就发送了消息。排查在Android端发送消息后在Unity的Update里打印日志确认消息接收。简化通信先确保最简单的消息能收到。解决建立一个常驻的、永不销毁的GameObject如DontDestroyOnLoad专门负责通信。使用单例模式的消息管理器来中转所有通信确保目标始终存在。问题4退出Unity模块返回原生界面后内存没有下降多次进出后最终OOM。原因Unity场景中的资源没有被正确释放。可能是静态变量持有引用、未卸载的AssetBundle、未销毁的GameObject、事件监听未取消等。排查使用Unity Profiler的内存快照功能对比进入Unity模块前和退出后的内存差异查看哪些资产残留。解决在Unity Activity的onDestroy()中除了调用super.onDestroy()可以尝试强制调用System.gc()不保证立即生效但可提示系统。在Unity场景中实现一个明确的清理阶段。在收到原生端“即将退出”的消息后手动销毁所有动态生成的物体、卸载所有AssetBundle、将静态引用置空。确保所有Coroutine在场景销毁时被正确停止StopAllCoroutines。问题5在部分设备上Unity界面显示异常黑屏、花屏但Logcat没有明显错误。原因图形API兼容性问题。可能是该设备默认的Graphics API如Vulkan驱动有bug或者Shader不支持。排查查看Logcat中Unity初始化图形设备时的日志。尝试在另一台同型号设备上复现。解决在Unity Player Settings的Graphics APIs列表中调整顺序。将更稳定、兼容性更广的OpenGL ES 3.0甚至OpenGL ES 2.0提到Vulkan前面。虽然可能损失一些性能但能换来更好的兼容性。集成工作就像搭桥细节决定成败。每一个参数配置、每一次方法调用、每一处资源释放都需要仔细考量。这个过程虽然繁琐但当你看到原生应用流畅地唤出炫酷的3D内容时那种成就感是无可替代的。希望这份结合了多次实战经验的指南能帮你少走弯路顺利地把Unity的强大能力带入你的Android应用。
返回列表