Unity 2021 Android打包res冲突解决方案:创建统一Android Library
1. 项目概述:一次典型的Unity版本升级“阵痛”
如果你正在从Unity 2019或2020版本升级到Unity 2021,并且你的项目需要发布到Android平台,那么你大概率会遇到一个让人头疼的打包错误。这个错误信息通常指向Android的res文件夹,提示资源文件冲突或重复,导致APK构建失败。这不仅仅是Unity 2021的“特色”,更是Unity引擎在Android构建管线、Gradle版本以及Android SDK/NDK工具链整合上的一次重大调整所带来的连锁反应。我最近在将一个中型商业项目从Unity 2019 LTS迁移到Unity 2021 LTS时,就深陷此坑。表面上看,错误信息指向一个具体的文件路径,但背后牵扯到的是Unity对Android Library(AAR)依赖管理方式的改变、Gradle构建脚本的升级,以及我们开发者自身项目结构的历史遗留问题。
简单来说,Unity 2021默认使用了更新的Gradle和Android Gradle Plugin(AGP)版本,这些新版本对Android项目的资源合并规则更加严格。过去在旧版本Unity或旧版Gradle下可能被忽略或“蒙混过关”的资源冲突,现在会被严格检查并报错。最常见的场景就是:你项目中可能通过Plugins/Android目录引入了一个或多个第三方AAR库(例如广告SDK、支付SDK、社交分享SDK),这些AAR库内部都包含了自己的res资源文件(如图标、布局、字符串)。当Unity打包时,它会尝试将所有依赖库的res文件夹合并到主APK的res中。如果不同库之间,或者库与你的主项目之间,存在同名的资源文件(比如都叫ic_launcher.png的图标,或者都定义了app_name的字符串),新版本的构建系统就会果断报错,而不是像以前那样可能随机选择一个覆盖另一个。
解决这个问题的核心思路,不再是简单地删除某个文件(虽然有时临时生效),而是系统地理解Unity 2021的Android构建流程,并学会如何正确地创建、配置和管理你自己的Android Library模块。通过创建一个主Android Library来统一管理所有第三方依赖和自定义Android代码,你可以精确控制资源的合并过程,从根本上避免冲突。接下来,我将手把手带你拆解这个报错,并附上从零创建一个兼容Unity 2021的Android Library的完整流程,以及如何将其无缝集成到你的Unity项目中。无论你是Unity新手还是有一定经验的开发者,这套方法都能帮你建立起清晰的Android构建知识体系,从容应对未来的版本升级。
2. 核心问题拆解:为什么res文件夹会冲突?
要解决问题,必须先理解问题是如何产生的。Unity在构建Android应用时,本质上是在幕后启动了一个标准的Gradle构建过程。你的Unity项目会被转换成一个Android项目模板,而你放在Assets/Plugins/Android目录下的所有文件(包括AndroidManifest.xml,res,libs,*.aar等)都会被复制到这个模板的对应位置,参与构建。
2.1 资源合并冲突的根源
在Android开发中,res目录下的资源(如图片、布局、字符串、颜色等)都通过其文件名和所在的限定符目录(如drawable-hdpi,values-zh)来唯一标识。构建系统(Gradle + AAPT2)的任务之一就是将所有模块(包括主app模块和所有依赖库模块)的资源收集起来,合并到一个统一的资源表中。
冲突发生的典型场景:
- 多个AAR库包含同名资源:这是最常见的情况。例如,你同时接入了AdMob和Unity Ads的SDK,它们可能都提供了一个名为
admob_app_icon.png或ic_notification.png的文件,且都放在drawable目录下。在旧版本中,构建系统可能只会发出警告,或者后引入的库资源覆盖先引入的。但在Unity 2021搭配的新版AGP下,这会直接导致构建失败。 - 你的项目与AAR库包含同名资源:你在自己的
Plugins/Android/res下放置了自定义图标或字符串,恰好与某个第三方库内的资源重名。 AndroidManifest.xml中的属性引用冲突:android:icon,android:label等属性引用了@drawable/icon或@string/app_name。如果多个库或主项目定义了同名的icon或app_name,也会引发冲突。
Unity 2021版本将内置的Gradle版本升级到了6.1.1以上,AGP版本也同步更新。新版本为了构建的确定性和可重现性,加强了对资源冲突的检查。它要求开发者必须明确处理这些冲突,而不是依赖构建系统的默认行为。
2.2 Unity构建Android项目的流程简析
理解以下流程,能帮你定位问题发生在哪个环节:
- 导出Gradle项目:当你在Unity Editor中选择
Build Settings->Build或Export Project时,Unity会生成一个完整的Android Gradle项目目录。 - 整合
Plugins/Android:该目录下的所有内容会被“扁平化”地拷贝到导出的Gradle项目的app模块(主模块)对应目录中。注意:所有AAR文件都会被解压,其内容(classes.jar, res, AndroidManifest.xml等)会与app模块的原有内容直接混合。 - 执行Gradle构建:Unity调用你指定的Gradle(或使用它自带的)执行
assembleRelease或assembleDebug任务。这时,Gradle开始解析所有依赖,合并资源,编译代码,最终生成APK。 - 报错点:资源合并(
mergeReleaseResources或mergeDebugResources任务)是构建早期的一个步骤。如果在此阶段检测到冲突,构建就会停止,并在Unity Console或Gradle日志中输出详细的错误信息。
错误信息示例:
> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade > Android resource linking failed ...\build\intermediates\merged_manifests\release\AndroidManifest.xml:86: error: resource string/app_name (aka com.yourcompany.yourapp:string/app_name) duplicated.或者更直接地指向某个具体的res文件路径。
注意:很多开发者遇到错误后的第一反应是去错误日志里提到的临时构建目录下删除冲突文件。这可能在单次构建中侥幸成功,但下次构建时文件又会被重新生成,问题依旧。这是一种“掩耳盗铃”的做法,无法根治问题。
3. 治本方案:创建统一的Android Library
最优雅、最彻底的解决方案,不是去“打补丁”,而是重新组织你的Android部分代码和依赖。我们将创建一个独立的Android Library模块(最终打包成AAR文件),在这个模块中统一管理所有第三方AAR依赖、自定义res资源、AndroidManifest.xml以及Java/Kotlin代码。然后,在Unity项目中只引用这一个“主AAR”。这样做的好处是:
- 隔离与封装:所有Android端的逻辑和依赖被封装在一个模块内,与Unity的C#代码解耦。
- 解决资源冲突:在Android Library模块内部,你可以使用Gradle提供的标准工具(如
resourcePrefix)来避免资源命名冲突,或者手动处理第三方库之间的冲突。 - 构建控制权:你可以使用Android Studio和Gradle脚本精细控制这个库的构建过程,例如启用混淆、指定依赖版本等。
- 维护性高:当需要更新某个SDK或添加新的Android功能时,你只需要修改这个Library项目,然后生成新的AAR替换到Unity中即可。
下面,我们开始手把手创建这个Android Library。
3.1 环境准备与项目创建
所需工具:
- Android Studio:建议使用较新版本(如Arctic Fox 2020.3.1或更高),以确保对最新AGP的良好支持。中文设置不是必须的,但如果你需要,可以在
File -> Settings -> Appearance & Behavior -> Appearance中勾选Use custom font并选择中文字体,或安装中文语言包插件。 - Java JDK:Unity 2021通常需要JDK 8或JDK 11。建议安装JDK 11并确保
JAVA_HOME环境变量指向它。你可以在Android Studio的File -> Project Structure -> SDK Location中查看和设置JDK路径。
创建新项目:
- 打开Android Studio,选择
New Project。 - 在模板选择界面,不要选择
Phone and Tablet下的Empty Activity。请选择Empty Views Activity。 - 配置项目:
Name: 给你的库起个名,例如UnityAndroidPlugin。Package name: 使用你的应用程序包名,后面加上.plugin后缀以示区分,例如com.yourcompany.yourapp.plugin。Save location: 选择一个你喜欢的目录。Language: 选择Java(如果你熟悉Kotlin也可以选,但本文以Java为例,Unity传统插件也多以Java编写)。Minimum SDK: 选择API 21: Android 5.0 (Lollipop)或与你Unity项目中Player Settings里设置的最低API级别一致。
- 点击
Finish,Android Studio会创建并打开项目。
3.2 将App模块转换为Library模块
默认创建的是一个可运行的App模块。我们需要将其改为Library模块。
打开项目根目录下的
settings.gradle文件。你会看到类似的内容:dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "UnityAndroidPlugin" include ':app'这表示当前项目包含一个名为
app的模块。接下来,修改
app模块的构建配置。打开app目录下的build.gradle文件(通常是app/build.gradle.kts或app/build.gradle)。找到
plugins部分,将:plugins { id 'com.android.application' // 这是应用模块 id 'org.jetbrains.kotlin.android' }修改为:
plugins { id 'com.android.library' // 改为库模块 id 'org.jetbrains.kotlin.android' }如果你用的是Groovy DSL(
build.gradle)且没有plugins块,则找到apply plugin: 'com.android.application'并改为apply plugin: 'com.android.library'。删除
android块中的applicationId这一行。Library模块不需要应用ID。(可选但推荐)为了减少最终AAR的大小并避免不必要的冲突,可以删掉
dependencies块中非必需的依赖,特别是implementation 'androidx.core:core-ktx:...'和implementation 'androidx.appcompat:appcompat:...'。但是,如果你的插件代码需要用到这些库(例如使用了AppCompat的控件),则必须保留。一个纯粹的、只做JNI桥接或简单系统调用的插件可能不需要它们。同步Gradle:点击Android Studio右上角的
Sync Now,或选择File -> Sync Project with Gradle Files。
现在,app模块已经变成了一个Android Library模块。你可以运行Build -> Make Module ‘app’来测试是否能成功构建。构建产物(AAR文件)会生成在app/build/outputs/aar/目录下,通常有debug和release两个版本。
3.3 配置Library以兼容Unity并避免资源冲突
这是最关键的一步,确保你的Library能被Unity正确引用且自身无资源冲突。
1. 修改AndroidManifest.xml:打开app/src/main/AndroidManifest.xml。作为Library,它的Manifest最终会被合并到主App的Manifest中。你需要做以下调整:
- 移除
<application>标签的所有属性(如android:theme,android:label,android:icon等)。这些属性应该由主App(即你的Unity游戏)来定义,否则会引起合并冲突。 - 通常,一个Library的Manifest只包含它需要的权限(
<uses-permission>)、组件声明(<activity>,<service>,<receiver>)以及<uses-feature>等。确保这些声明的android:name是唯一的。 - 一个极简的Library Manifest可能长这样:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.yourapp.plugin"> <!-- 声明插件需要的权限 --> <uses-permission android:name="android.permission.INTERNET" /> <!-- 声明插件提供的Activity --> <application> <activity android:name=".MyPluginActivity" android:exported="false" /> <!-- exported属性根据实际情况设置 --> </application> </manifest>
2. 为资源添加前缀(强烈推荐):为了避免你的Library资源与主App或其他库资源重名,Gradle提供了一个简单有效的方法:为所有资源自动添加前缀。 在app/build.gradle文件的android块内添加:
android { // ... 其他配置 resourcePrefix "uap_" // 你可以自定义前缀,如 uap_ (Unity Android Plugin) }添加此后,你在res目录下创建的所有资源(布局文件除外),其名称在编译时都会被自动加上uap_前缀。例如,你定义了一个@drawable/icon,在最终的R文件中会变成@drawable/uap_icon。这从根本上杜绝了与外部资源的命名冲突。注意:这个前缀只对新创建的资源文件生效,对于已经存在的资源,你需要手动重命名。
3. 处理第三方AAR依赖:这是解决原始res冲突问题的核心。将所有你需要在Unity中使用的第三方SDK(如广告、分析、支付等),都作为依赖添加到这个Library模块中,而不是直接放到Unity的Plugins/Android文件夹里。
在app/build.gradle的dependencies块中添加:
dependencies { // 示例:添加一些常见的SDK依赖 implementation 'com.google.android.gms:play-services-ads:21.5.0' // AdMob // implementation files('libs/some-local-sdk.aar') // 如果是本地的AAR文件 // 确保使用较新且兼容的版本,旧版本SDK可能本身存在资源冲突或与新AGP不兼容 }关键点:当多个库在同一个Gradle模块中被声明为依赖时,Gradle会尝试解决它们之间的传递性依赖和资源冲突。如果两个库(比如A和B)都依赖了不同版本的同一个库C,Gradle通常会选择更高的版本。对于资源冲突,Gradle的行为可以通过更精细的规则控制,但首先确保所有SDK都是最新稳定版,能减少很多问题。
4. 编写你的插件Java代码:在app/src/main/java/com.yourcompany.yourapp.plugin/目录下,创建你的Java类。例如,创建一个UnityBridge.java,用于和Unity的C#端进行通信(通过UnityPlayer.UnitySendMessage)。
package com.yourcompany.yourapp.plugin; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import com.unity3d.player.UnityPlayer; // 注意:这个类需要额外处理,见下文 public class UnityBridge { private static Activity getUnityActivity() { // 如何获取Unity的Activity是一个常见问题 // 一种常见做法是通过一个初始化方法,由C#端将Activity实例传过来 // 或者通过反射调用UnityPlayer.currentActivity // 这里假设我们通过C#设置 return UnityPluginActivity.instance; } public static void showNativeView() { Activity activity = getUnityActivity(); if (activity != null) { Intent intent = new Intent(activity, MyPluginActivity.class); activity.startActivity(intent); } } // 供Android调用的方法,用于回调到Unity public static void sendMessageToUnity(String gameObject, String method, String message) { UnityPlayer.UnitySendMessage(gameObject, method, message); } }关于UnityPlayer类:这个类并不在标准的Android SDK中,它来自Unity的运行时库。你有两种方式处理:
- 方式一(推荐,保持Library纯净):不在Library中直接引用
UnityPlayer。所有需要与Unity交互的接口,通过一个独立的“接口层”来定义,具体的UnityPlayer.UnitySendMessage调用由Unity项目中的Java代码(放在Plugins/Android)来实现。Library只负责原生功能,通过回调接口通知调用者。 - 方式二:将Unity安装目录下的
classes.jar(位于{Unity安装路径}/Editor/Data/PlaybackEngines/AndroidPlayer/Variations/mono或il2cpp/Development/Classes/)拷贝到Library模块的libs目录,并将其添加为compileOnly依赖:compileOnly files('libs/classes.jar')。这样Library代码可以编译,但最终打包AAR时不会包含这个jar,需要Unity在运行时提供。
3.4 构建与生成AAR
- 在Android Studio左侧的
Build Variants工具窗格中,选择release构建变体。 - 点击菜单栏的
Build -> Make Module ‘app’。 - 构建成功后,在
app/build/outputs/aar/目录下找到app-release.aar文件。将其重命名为一个更有意义的名字,例如unity-android-plugin-release.aar。
4. 在Unity中集成自定义Android Library
现在,我们有了一个“干净”的、统一管理了所有第三方依赖的AAR文件。接下来就是将其集成到Unity项目中。
- 准备Unity项目:在Unity项目的
Assets目录下,创建标准的插件文件夹结构:Assets/Plugins/Android。 - 放置AAR文件:将上一步生成的
unity-android-plugin-release.aar文件拷贝到Assets/Plugins/Android目录下。 - 处理主
AndroidManifest.xml:在Assets/Plugins/Android目录下,创建或放置你的应用主AndroidManifest.xml文件。这个文件会与你Library中的Manifest合并。你需要在这里声明应用级别的属性,如android:icon,android:label,android:theme,以及应用所需的权限(如果Library中已经声明了,这里可以不用重复声明,但声明了也没关系,合并规则会处理)。<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.yourapp" > <application android:icon="@drawable/app_icon" <!-- 确保此资源存在于你的Unity项目或主资源中 --> android:label="@string/app_name" android:theme="@style/UnityThemeSelector"> <!-- Unity Player Activity --> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:exported="true"> <!-- ... --> </activity> <!-- 你的Library中声明的Activity会自动合并进来 --> </application> <!-- 权限也会自动合并 --> </manifest> - 移除旧的第三方SDK文件:至关重要的一步!将原先直接放在
Assets/Plugins/Android下的所有第三方AAR、JAR以及它们可能附带的res文件夹、AndroidManifest.xml片段等,全部移除或备份到别处。现在,所有这些依赖都已经封装在你新的主AAR文件里了。只保留你自己的主AAR、主AndroidManifest.xml以及任何必须的、不与AAR冲突的自定义文件(例如proguard-user.txt)。 - 配置Player Settings:打开Unity的
Project Settings -> Player,切换到Android平台。- Other Settings:
Package Name: 设置为你的应用包名(com.yourcompany.yourapp),注意与Library的包名(com.yourcompany.yourapp.plugin)区分开。Minimum API Level: 与你在Android Library中设置的一致。
- Publishing Settings:
Keystore: 配置你的发布密钥。
- 最重要的一步:Build System:选择
Gradle。这是必须的,因为我们要使用Gradle来解析AAR依赖。 - 在
Build System下方,勾选Export Project。这会让Unity导出完整的Gradle项目,而不是直接构建APK,方便我们进行更深入的调试(如果需要)。
- Other Settings:
- 编写C#桥接代码:在Unity的
Assets/Scripts或类似目录下,创建一个C#脚本,用于调用Android插件。using UnityEngine; public class AndroidPluginManager : MonoBehaviour { private static AndroidJavaClass _pluginClass; private static AndroidJavaObject _pluginInstance; private const string PluginPackageName = "com.yourcompany.yourapp.plugin.UnityBridge"; void Awake() { // 初始化,获取Unity的Activity并传递给插件(如果插件需要) // 具体方法取决于你在Library中如何设计初始化接口 // 例如,如果Library需要一个Context初始化: // using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) // using (AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) // { // _pluginClass = new AndroidJavaClass(PluginPackageName); // _pluginClass.CallStatic("init", currentActivity); // } } public void CallNativeFunction() { // 调用Library中的静态方法 using (AndroidJavaClass pluginClass = new AndroidJavaClass(PluginPackageName)) { pluginClass.CallStatic("showNativeView"); } } // 供Java端回调的方法,必须为public public void OnNativeCallback(string message) { Debug.Log($"Received callback from Android: {message}"); // 处理回调逻辑 } }
5. 构建、测试与疑难排查
完成以上步骤后,你就可以尝试构建了。
- 首次构建:在Unity中,
File -> Build Settings,选择Android平台,点击Export(如果你勾选了Export Project)或Build And Run。首次构建可能会较慢,因为Gradle需要下载所有依赖。 - 解读构建错误:如果构建失败,请仔细阅读Unity Console中的错误日志。
- Gradle同步失败:检查
Assets/Plugins/Android目录下是否还有残留的旧版SDK文件,特别是那些可能带有自己res文件夹的。确保你的主AAR包含了所有必要依赖。 - 资源冲突依然存在:这可能意味着在你的主AAR内部,多个第三方库之间仍有冲突。这时你需要回到Android Studio的Library项目中,通过分析
./gradlew :app:dependencies命令的输出,检查依赖树,排除或升级有冲突的库版本。也可以在Library的build.gradle中使用exclude规则来排除特定的资源文件。android { packagingOptions { exclude 'res/drawable/conflicting_icon.png' // 排除特定冲突文件 // 或者合并资源时选择第一个 pickFirst 'res/values/strings.xml' } } - 类找不到(ClassNotFoundException):检查你的C#代码中调用的Java类名、方法名是否完全正确,包括包名。确保Library已正确打包到AAR中,并且没有使用
compileOnly依赖了Unity特有的类(如UnityPlayer)而导致运行时缺失。
- Gradle同步失败:检查
- 调试技巧:
- 使用
adb logcat:在真机或模拟器上运行应用,通过adb logcat -s Unity或adb logcat *:E来查看运行时日志,捕捉崩溃信息。 - 检查APK内容:使用
apkanalyzer(Android SDK自带)或APK Editor Studio等工具,打开构建成功的APK,查看res目录下的资源,确认没有重复文件,以及你的Library资源是否被正确添加(带有前缀uap_)。 - 导出Gradle项目手动构建:在Unity中勾选
Export Project,导出后使用Android Studio打开这个项目。你可以在Android Studio中直接进行Build或Debug,这能获得更详细的Gradle和Android构建日志,对于排查复杂的依赖问题非常有帮助。
- 使用
6. 进阶优化与扩展思考
当你成功解决了res冲突并建立起自定义Android Library的工作流后,可以考虑以下优化方向:
- 多构建变体(Flavors):在Android Library中配置
productFlavors,可以为不同环境(开发、测试、生产)编译不同版本的AAR,例如集成不同的SDK密钥或配置。 - 代码混淆(ProGuard/R8):在Library的
build.gradle中启用minifyEnabled true,并配置对应的ProGuard规则(proguard-rules.pro),可以保护你的Java代码并减小AAR体积。记得将需要被Unity C#端反射调用的类和方法规则设为-keep。 - 资源精简:定期检查你的Library中是否包含了不必要的资源文件(例如第三方SDK带来的多种语言图片但你的应用只支持中文)。可以通过Gradle的
resConfigs或手动删除res目录下不需要的资源限定符文件夹来精简。 - 持续集成(CI):将Android Library的构建脚本(
gradlew assembleRelease)集成到你的CI/CD流程中,确保每次Unity项目构建前,都能使用最新、最稳定的AAR。
这次从Unity 2021升级引发的res打包报错,虽然过程曲折,但迫使我去深入理解了Unity Android构建的黑盒,掌握了用标准化Android开发流程来管理Unity插件依赖的方法。这套方法不仅解决了眼前的问题,更为项目后续接入任何Android原生功能打下了坚实的基础。记住,面对构建错误,最忌讳的是盲目搜索错误信息并尝试各种“偏方”。静下心来,理解工具链的原理,用系统性的方法去重构,才是工程师的解决之道。