1. 项目概述为什么要把Unity当库用如果你是一个Android原生开发的老手最近接到一个需求要在现有的、已经迭代了好几个大版本的App里加入一个3D商品展示或者一个AR试妆的小模块你会怎么做从头用OpenGL ES去撸一个3D引擎这显然不现实。这时候Unity作为全球最流行的实时3D内容创作平台就成了一个极具吸引力的选择。但问题来了我们不可能把整个App都用Unity重写我们需要的只是把Unity制作的3D/AR内容像集成一个SDK或者一个库Library那样无缝地嵌入到现有的Android原生工程里。这就是“Unity作为库导入Android原生工程”的核心场景。它不是一个简单的“Unity导出Android项目”而是让Unity运行时Runtime和你写的Unity逻辑变成一个.aar库文件被你的Android Studio主工程所依赖。你的App启动入口依然是原生的MainActivityUnity视图只是其中的一个Fragment或者一个View。原生部分和Unity部分可以双向通信数据共享事件传递。我经历过几次这样的项目从最早的手忙脚乱到后来的驾轻就熟中间踩过的坑不计其数。网上很多教程要么步骤不全要么版本过时遇到问题只能自己琢磨。今天我就把从环境准备、项目配置、代码编写到调试打包的完整流程结合我自己的实操心得给你彻底讲透。无论你是想给电商App加个3D看房还是给教育App加个AR化学实验这套指南都能让你少走至少一周的弯路。2. 环境准备与项目创建在开始任何代码工作之前把环境搭对是成功的一半。这里涉及两个核心环境Unity和Android Studio。2.1 Unity侧版本选择与模块安装首先打开Unity Hub创建一个新的3D项目。项目名称和位置随意但项目类型务必选择“3D (Core)”。不要用URP或HDRP模板除非你非常清楚它们的额外依赖和移动端性能影响对于集成库来说核心模板最干净问题最少。版本选择是第一个关键决策点。强烈建议使用Unity的LTS长期支持版本比如2021.3 LTS或2022.3 LTS。LTS版本经过更长时间的测试稳定性高与各种Android SDK、Gradle版本的兼容性问题最少。避免使用最新的Tech Stream版本你可能成为新Bug的第一批受害者。创建项目后你需要安装Android构建支持模块。点击菜单栏Window - Package Manager在左上角 Packages 下拉菜单中选择Unity Registry。搜索 “Android” 并找到 “Android Build Support”。点击安装务必勾选其下的 “Android SDK NDK Tools” 以及 “OpenJDK”。让Unity自动安装这些可以省去后续手动配置环境变量的一大堆麻烦。注意Unity内置的OpenJDK版本可能与你本地环境冲突。如果后续构建报Java版本错误可以尝试在Unity的Preferences - External Tools中将JDK路径指向你本地安装的JDK 8或JDK 11推荐JDK 11对Android Gradle插件兼容性更好。2.2 Android Studio侧主工程与依赖准备在Android Studio中准备好你的主工程。这个工程应该是一个标准的、可独立编译运行的Android应用项目。你需要检查主工程的build.gradle文件确保compileSdkVersion,minSdkVersion,targetSdkVersion这些版本号。一个常见的兼容性策略是让Unity项目中的Player Settings里的Android配置与主工程保持一致或略低。例如主工程targetSdkVersion是33那么Unity那边也最好设为33。minSdkVersion则取两者中较高的那个确保覆盖所有用户设备。接下来你需要一个关键的依赖项Unity官方提供的unity-classes.jar或对应的.aar。这个文件包含了Unity运行时需要的一些基础接口。获取它的最可靠方式是从一个空白的、导出为Gradle项目的Unity Android工程中提取。在Unity中随便创建一个空场景打开File - Build Settings选择Android平台点击Player Settings。在Player Settings - Publishing Settings下找到Build区域一定要勾选 “Export Project”。这个选项意味着Unity不会直接打包APK而是导出一个完整的Android Gradle工程。点击Build选择一个空文件夹导出。在导出的文件夹中路径通常是unityLibrary/build/intermediates/aar_main_jar/debug/或release你可以找到classes.jar。或者在unityLibrary/build/outputs/aar/下找到unityLibrary-release.aar文件。将找到的.jar或.aar文件复制到你的Android主工程的libs目录下如果没有就新建一个。然后在主工程的app模块的build.gradle文件中添加依赖dependencies { implementation fileTree(dir: libs, include: [*.jar, *.aar]) // 这行如果已有则不用加 implementation files(libs/classes.jar) // 或者 implementation files(libs/unityLibrary-release.aar) // 其他依赖... }3. Unity项目配置与导出环境就绪后重心回到Unity把我们的3D内容配置成一个合格的“库”。3.1 Player Settings关键配置打开File - Build Settings - Player Settings这里面的每一个选项都关乎集成能否成功。首先进入Resolution and Presentation选项卡Default Orientation: 建议设置为 “Landscape Left” 或 “Portrait”具体取决于你的Unity内容设计。这里设置的方向需要与Android原生中承载Unity视图的Activity/Fragment所声明的屏幕方向一致否则会出现旋转错乱。Fullscreen Mode: 务必选择“Fullscreen Window”。如果选择“Exclusive Fullscreen”Unity会尝试接管整个屏幕这在嵌入模式下会导致问题。Render Outside Safe Area: 勾选。确保内容能铺满全面屏设备的异形切割区域。然后进入Other Settings选项卡这是配置的重灾区Identification:Package Name: 这个必须修改不能使用默认的com.Company.ProductName。它需要与你Android主工程的包名完全不同。例如主工程是com.mycompany.myapp这里可以设为com.mycompany.myapp.unity。这是为了避免资源ID冲突。Version,Bundle Version Code: 按需设置与主工程独立。Configuration:Scripting Backend: 对于追求最佳启动速度和包体积的集成场景强烈推荐使用IL2CPP。虽然构建时间稍长但它能生成更高效的C代码并且支持64位ARM64架构这是上架Google Play的硬性要求。Mono虽然构建快但性能和安全性与IL2CPP有差距。API Compatibility Level: 通常保持.NET Standard 2.1或.NET 4.x即可确保你使用的C#语法和库被支持。Target Architectures:必须勾选ARMv7和ARM64。只选ARMv7将无法在64位设备上获得最佳性能且可能违反应用商店规定。Optimization:Strip Engine Code:务必勾选。这是减小最终.aar库体积最关键的一步。Unity会分析你的项目实际用到的代码移除不必要的引擎模块。但要注意如果通过反射动态调用某些API可能会被误删需要配置link.xml文件来保留。3.2 构建导出为Android Library关键步骤来了我们不是导出APK也不是导出普通工程而是导出供Android Studio使用的库模块。在Build Settings窗口确保Android平台被选中然后点击左下角的Player Settings...。在Player Settings - Publishing Settings区域找到Build。勾选“Export Project”前面提过。还有一个更重要的选项“Build App Bundle (Google Play)” 这个框千万不要勾选。我们要的是库不是应用束。点击Build选择一个目标文件夹例如UnityExport。导出完成后不要关闭这个文件夹。你会发现里面有一个unityLibrary目录。这个unityLibrary目录就是我们最终要集成到Android主工程里的核心。它本身就是一个完整的Android库模块里面有build.gradle、src、libs等标准结构。4. Android原生工程集成详解现在我们有了unityLibrary模块是时候把它“请进”我们的Android主工程了。4.1 导入Unity Library模块在Android Studio中确保你处在“Project”视图而不是Android视图这样能看到真实的目录结构。找到你的主工程根目录将上一步导出的整个unityLibrary文件夹复制粘贴到与主工程app模块同级的位置。在Android Studio中点击File - New - Import Module。在弹出的窗口中浏览并选择你刚刚复制过来的unityLibrary文件夹。Module name会自动填充为:unityLibrary点击Finish。导入完成后打开项目根目录的settings.gradle文件你应该能看到IDE自动添加了一行include :app, :unityLibrary如果没有请手动加上。4.2 配置Gradle依赖与编译选项模块导入后需要配置依赖关系。打开主工程app模块的build.gradle文件在dependencies块中添加dependencies { implementation project(:unityLibrary) // ... 其他依赖 }接下来是解决编译冲突这是集成过程中最常见的“拦路虎”。Unity库和你的主工程可能依赖了不同版本的同名库比如Android Support库或AndroidX库。你需要打开unityLibrary模块自己的build.gradle文件。通常Unity导出的库会声明一些过时的或版本固定的依赖。我们的策略是让Unity库移除它自带的、可能与主工程冲突的依赖转而使用主工程提供的版本。在unityLibrary/build.gradle的dependencies块中查找类似implementation androidx.appcompat:appcompat:1.0.0这样的行将它们改为compileOnly或者直接注释掉/删除。dependencies { // 将 implementation 改为 compileOnly避免传递依赖冲突 compileOnly androidx.appcompat:appcompat:1.0.0 compileOnly androidx.constraintlayout:constraintlayout:1.1.3 // 或者如果你确定主工程有也可以直接删除这些行 // Unity运行时所必须的依赖通常不能动 implementation fileTree(dir: libs, include: [*.jar]) implementation androidx.multidex:multidex:2.0.1 // 如果启用了MultiDex }原理是compileOnly表示该依赖仅在编译unityLibrary时使用不会打包进最终的APK也不会传递给依赖它的app模块。这样最终APK里只会有一份来自app模块的、版本统一的依赖库。然后检查并统一两个模块的compileSdkVersion,buildToolsVersion,minSdkVersion,targetSdkVersion以及kotlinVersion如果用了Kotlin尽可能让它们保持一致。4.3 处理清单文件(Manifest)合并冲突每一个Android模块都有自己的AndroidManifest.xml。当app模块和unityLibrary模块合并时可能会因为声明了相同的组件比如UnityPlayerActivity或权限而产生冲突。Unity库的AndroidManifest.xml通常位于unityLibrary/src/main/AndroidManifest.xml。它会声明一个名为com.unity3d.player.UnityPlayerActivity的Activity。在你的主工程app模块的AndroidManifest.xml中你不需要再去声明这个Activity。但是你需要确保主工程的Manifest包含了Unity所需的所有权限如网络权限、摄像头权限等。更优雅的做法是在主工程的Manifest中使用uses-permission标签而Unity库的Manifest里只保留必要的组件声明。如果遇到清单合并错误可以在主工程app模块的build.gradle中添加合并规则来排除冲突android { ... buildTypes { release { ... } } // 处理Manifest合并冲突 packagingOptions { exclude AndroidManifest.xml // 在某些极端冲突下可以排除库的Manifest但需谨慎 } }更常见的做法是使用tools:replace或tools:ignore属性在Manifest文件中精细控制。例如如果两者都定义了android:icon和android:theme可以在主工程Manifest的application标签里添加application ... tools:replaceandroid:icon, android:theme tools:ignoreGoogleAppIndexingWarning5. 核心代码启动、通信与生命周期管理集成成功编译通过只是第一步让两部分代码“活”起来才是真正的挑战。5.1 在原生界面中启动Unity视图Unity视图本质上是一个UnityPlayer对象它继承自FrameLayout。我们通常不会直接启动Unity的UnityPlayerActivity而是自己创建一个Fragment或一个自定义View来承载它。方案一使用Fragment推荐这是最灵活、最符合现代Android架构的方式。你可以像管理普通Fragment一样管理Unity视图。首先创建一个UnityFragment.javaimport com.unity3d.player.UnityPlayer; import android.content.Context; import android.os.Bundle; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; public class UnityFragment extends Fragment { protected UnityPlayer mUnityPlayer; private FrameLayout container; Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化UnityPlayer。注意不要在UI线程进行耗时操作这里可以放在后台线程 mUnityPlayer new UnityPlayer(getActivity()); } Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 创建一个简单的FrameLayout作为容器 this.container new FrameLayout(getActivity()); // 将UnityPlayer的视图添加到容器中 this.container.addView(mUnityPlayer.getView(), FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); return this.container; } Override public void onResume() { super.onResume(); if (mUnityPlayer ! null) { mUnityPlayer.resume(); } } Override public void onPause() { super.onPause(); if (mUnityPlayer ! null) { mUnityPlayer.pause(); } } Override public void onDestroy() { super.onDestroy(); if (mUnityPlayer ! null) { mUnityPlayer.quit(); // 退出Unity释放资源 mUnityPlayer null; } } }然后在你的主Activity中像添加普通Fragment一样添加它getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, new UnityFragment()) .commit();方案二在Activity中直接嵌入如果你只需要全屏显示Unity内容也可以直接在Activity的onCreate中设置public class MainActivity extends AppCompatActivity { private UnityPlayer mUnityPlayer; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 隐藏系统UI实现沉浸式可选 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); mUnityPlayer new UnityPlayer(this); setContentView(mUnityPlayer); mUnityPlayer.requestFocus(); } Override protected void onResume() { super.onResume(); mUnityPlayer.resume(); } Override protected void onPause() { super.onPause(); mUnityPlayer.pause(); } Override protected void onDestroy() { mUnityPlayer.quit(); super.onDestroy(); } }5.2 Android与Unity的双向通信集成的精髓在于数据互通。通信是双向的从Android调用Unity中的C#方法以及从Unity调用Android的Java方法。Android调用Unity (Java - C#)这是通过UnityPlayer.UnitySendMessage方法实现的。它有三个参数GameObject名称、方法名、参数字符串。// 在Android Java代码中 UnityPlayer.UnitySendMessage(GameObjectName, MethodName, Hello from Android);在Unity的C#脚本中你需要有一个挂载在名为GameObjectName的游戏对象上的脚本其中包含一个公有方法MethodName该方法接受一个字符串参数。// Unity C#脚本 using UnityEngine; public class MessageReceiver : MonoBehaviour { public void MethodName(string message) { Debug.Log(Received from Android: message); // 处理消息... } }注意UnitySendMessage是异步的且参数只能是字符串。对于复杂数据需要序列化为JSON字符串进行传递。Unity调用Android (C# - Java)这需要利用Android的JNIJava Native Interface。Unity提供了AndroidJavaClass和AndroidJavaObject来简化操作。假设你在Android主工程里有一个工具类package com.mycompany.myapp; public class NativeBridge { private static final String TAG NativeBridge; private final Activity mActivity; public NativeBridge(Activity activity) { this.mActivity activity; } // 供Unity调用的静态方法 public static void ShowToast(final String message) { // 注意Unity调用可能不在UI线程需要切回主线程执行UI操作 Activity activity UnityPlayer.currentActivity; activity.runOnUiThread(() - Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()); } // 供Unity调用的实例方法 public int AddNumbers(int a, int b) { return a b; } }在Unity C#脚本中你可以这样调用using UnityEngine; public class AndroidCaller : MonoBehaviour { void Start() { // 调用静态方法 using (AndroidJavaClass jc new AndroidJavaClass(com.mycompany.myapp.NativeBridge)) { jc.CallStatic(ShowToast, Hello from Unity!); } // 调用实例方法需要先获取实例 using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { using (AndroidJavaObject bridge new AndroidJavaObject(com.mycompany.myapp.NativeBridge, currentActivity)) { int result bridge.Callint(AddNumbers, 5, 3); Debug.Log(Result from Android: result); } } } } }5.3 统一的生命周期管理这是确保应用稳定的关键。Unity Player有自己的生命周期pause,resume,quit必须与Android的Activity/Fragment生命周期同步。启动/恢复在Android的onResume()中调用mUnityPlayer.resume()。暂停在Android的onPause()中调用mUnityPlayer.pause()。销毁在Android的onDestroy()中调用mUnityPlayer.quit()并置空引用。窗口焦点在Activity的onWindowFocusChanged中调用mUnityPlayer.windowFocusChanged(hasFocus)这对于处理输入如键盘弹出非常重要。配置变更当屏幕旋转等配置发生变化时默认情况下Activity会销毁重建。如果你不希望Unity重新初始化可以在AndroidManifest.xml中为该Activity配置android:configChanges属性并重写Activity的onConfigurationChanged方法通知UnityPlayer。一个健壮的Fragment或Activity应该像前面代码示例那样妥善处理这些生命周期回调。6. 构建、调试与性能优化当代码联调通过后我们最终要面对的是构建发布和性能问题。6.1 解决构建过程中的常见错误Program type already present或Duplicate class这是典型的依赖冲突。按照4.2节的方法检查并统一所有模块的依赖将unityLibrary中的非必要依赖改为compileOnly。使用./gradlew :app:dependencies命令可以查看详细的依赖树找出冲突源头。Failed to apply plugin ‘com.android.internal.application’检查Gradle插件版本和Gradle版本兼容性。Unity导出的库可能使用了较老的Android Gradle PluginAGP版本。尝试在主工程根目录的build.gradle中升级或降级classpath com.android.tools.build:gradle:x.x.x使其与你的Android Studio版本匹配。Unity could not find the Android SDK or NDK在Unity的Preferences - External Tools中正确设置SDK、NDK、JDK的路径。一个技巧是使用Unity Hub安装的Android Support模块自带的SDK/NDK路径通常位于Unity安装目录下这样兼容性最好。生成APK时包体积巨大首先确保在Unity的Player Settings中勾选了Strip Engine Code。其次检查导出的unityLibrary中是否包含了不必要的资源如图片、音频等。可以使用APK分析工具Android Studio的Build - Analyze APK查看.so动态库和资源文件占比。对于.so库确保只保留了必需的架构arm64-v8a和armeabi-v7a在unityLibrary的build.gradle中配置ndk { abiFilters arm64-v8a, armeabi-v7a }来过滤。6.2 调试技巧与日志查看调试混合工程比单一工程复杂。Unity日志查看在Android Studio的Logcat中过滤标签Unity可以看到Unity的Debug.Log输出。如果看不到检查Unity Player的初始化并确保在构建时没有禁用脚本调试在Unity Build Settings中不要勾选Development Build以外的调试选项有时反而需要勾选Development Build和Script Debugging。Android日志查看在Logcat中过滤你的应用包名或NativeBridge等自定义标签。断点调试对于Android Java代码可以直接在Android Studio中打断点调试。对于Unity C#代码你需要使用Visual Studio或JetBrains Rider附加到Unity编辑器进行调试。对于已经打包到Android设备上的Unity部分C#调试非常困难通常依赖于日志。使用ADB命令adb logcat -s Unity可以专门查看Unity日志。adb shell dumpsys meminfo package_name可以查看应用内存使用情况分析Unity部分的内存占用。6.3 性能优化与包体瘦身建议纹理优化Unity中使用的纹理务必进行压缩ASTC、ETC2并设置合适的Max Size。避免使用过大的UI Sprite图集。模型与动画简化模型面数使用LOD多层次细节。优化动画骨骼数量和关键帧。代码与资源剥离充分利用Strip Engine Code和Managed Stripping Level在Player Settings中。创建link.xml文件放在Assets目录下防止反射使用的代码被误删。例如linker assembly fullnameMyAssembly preserveall/ type fullnameMyNamespace.MyClass preserveall/ /linkerShader变体剔除使用Shader Variant Collection来收集并只打包项目实际用到的Shader变体避免ShaderLab生成所有可能的变体这能显著减少包体。异步加载与卸载在Unity场景切换或对象显隐时使用Addressable Asset System或Resources.LoadAsync进行异步加载并及时使用Resources.UnloadUnusedAssets或Addressables.Release来释放内存。保持帧率稳定在Android设备的UnityPlayer初始化后可以考虑限制帧率Application.targetFrameRate 30;特别是在展示静态3D模型时可以节省电量。7. 进阶话题与避坑指南掌握了基础集成后还有一些进阶场景和深坑需要注意。7.1 处理多场景与场景切换你的Unity内容可能不止一个场景。在集成环境下切换场景不能使用SceneManager.LoadScene因为这可能会干扰原生的界面栈。推荐做法将所有需要的内容做到一个Unity场景中通过激活/禁用不同的GameObject或使用子场景Additive Load来管理内容。如果必须切换场景确保在切换前后处理好与Android端的通信状态并做好资源管理避免内存泄漏。7.2 输入事件处理触摸、键盘、传感器默认情况下嵌入的UnityPlayer会接管所有的触摸和按键事件。这可能会导致它覆盖掉原生部分的输入比如一个覆盖在Unity视图上的原生按钮无法点击。解决方案可以通过重写UnityPlayer的onTouchEvent或相关方法或者通过Android的ViewGroup的事件分发机制onInterceptTouchEvent来进行精细控制。更常见的做法是在不需要Unity交互时例如显示一个全屏的Unity过场动画让其全权处理在需要混合交互时则通过通信告知Unity暂时禁用输入由原生层来分发事件。传感器如陀螺仪、加速度计通常由Unity直接访问一般没有问题。但如果你的原生App也在使用传感器需要注意可能存在的资源竞争。7.3 内存管理与泄漏预防混合开发最大的隐患之一是内存泄漏。Unity部分使用C#托管内存和Native插件非托管内存Android部分使用JavaJVM堆内存。任何一环的引用持有不当都会导致泄漏。Java与C#间的引用通过JNI从C#创建的AndroidJavaObject或AndroidJavaClass在使用完毕后必须调用.Dispose()方法或在using语句块中来释放JNI引用。长期持有Java对象会导致其无法被GC回收。UnityPlayer销毁务必在宿主Activity/Fragment的onDestroy中调用mUnityPlayer.quit()和mUnityPlayer.destroy()某些版本需要并将引用置为null。监听器与回调在Unity C#脚本中注册的Android回调如通过UnityPlayer.UnitySendMessage在脚本销毁或Unity部分卸载时要确保Android端也移除这些回调避免持有对已销毁上下文的引用。使用内存分析工具Android Studio的Profiler可以监控Java堆和Native堆内存。Unity Profiler通过Development Build连接可以监控Unity的托管堆和资源内存。定期进行测试特别是在进入/退出Unity视图多次后观察内存是否持续增长。7.4 针对不同Android版本的适配问题存储权限Scoped Storage从Android 10API 29开始作用域存储限制应用访问外部存储。如果Unity内容需要读写设备文件如下载资源包、保存截图需要在AndroidManifest中声明MANAGE_EXTERNAL_STORAGE权限Google Play审核严格或使用MediaStore API并将文件路径通过通信接口从Android原生端获取后传给Unity。后台限制Android 12对后台服务、闹钟、作业有更严格的限制。确保Unity的音频、网络请求等在应用进入后台时能正确暂停避免被系统强制停止。启动画面Splash ScreenAndroid 12引入了新的启动画面API。如果你的应用有原生的启动画面需要协调好与Unity初始化的时机避免出现两个启动画面重叠或黑屏时间过长。集成Unity到Android原生工程就像让两位顶尖的专家在同一个舞台上合作演出。前期繁琐的配置和调试是为了后期高效、稳定的协同开发。这套流程我已经在多个商业项目中验证过虽然每一步都需要细心但一旦跑通它带来的价值——让专业的团队做专业的事用Unity制作炫酷的3D/AR内容用原生代码构建稳固的应用框架——是无可替代的。记住多写测试代码善用日志遇到构建错误先查依赖和Gradle版本你就能驾驭这个强大的技术组合。