Unity接入第三方SDK:纯命令行Gradle打包Java源码为aar全攻略
2026/9/18 3:36:57 网站建设 项目流程

最近在给项目组接一个第三方的Android SDK,对方不给aar也不给jar,直接甩过来一堆Java源码和一份“自行集成”的说明。Unity这边的情况很尴尬:一方面不想为了维护一个小库就引入整个Android Studio工程,另一方面业务层还要兼顾iOS,所以最干净的做法,就是把这份Java代码打成aar,扔进Assets/Plugins/Android里,让Unity在构建APK时自动打包进去。

网上搜下来,讲“Android Studio里怎么创建Library Module”的文章车载斗量,但真正记录怎么不打开Android Studio、纯靠命令行和Gradle手动把Java源码打成aar的内容少得可怜。我翻了不少资料,也踩了几次编译失败和运行时ClassNotFound的坑,最后终于把这条链路彻底打通了。这篇文章就按我在实际项目里跑通的流程,拆开揉碎地讲一遍,从为什么非要“手动”打包,到环境准备、Gradle工程怎么写、aar怎么接入Unity、C#和Java怎么互相调用,再到一堆绕过不掉的坑,全部记录下来,给后面被同样需求折磨的人省点时间。

1. 为什么要把Java手动打成aar

1.1 aar就是一个“装了class的zip包”

很多Unity开发者分不清jar和aar的差别,简单说:jar里面只有纯Java的class,没法带Android资源文件;aar本质上是zip压缩包,里面除了classes.jar(Java源码编译出的class压缩包),还有一个AndroidManifest.xml、R.txt、res资源目录、jni目录,偶尔还有assets。Android系统级的清单合并、资源编译逻辑,全部依赖这些结构。

所以如果第三方SDK里不仅有Java代码,还带了布局文件、图片资源、so库,那就只能用aar,不能用jar。还有一点很关键,aar的AndroidManifest.xml在Unity构建APK时会被合并进最终包的AndroidManifest里。也就是说,你在aar里声明的权限、Activity、Application,最终都生效。很多Unity开发者以为aar只是个“高级jar”,是完全的误解。

1.2 哪些场景适合用手动打包

不是所有Unity项目都需要手动折腾,但在下面几类场景里,这条路径的价值特别明显:

  • 第三方服务商只提供了Java源码,不提供预编译好的aar或jar,这是最典型的情况。
  • 项目需要把多个内部公共模块统一成一套库,想在Android和iOS逻辑层做隔离,Android侧只留一个aar入口。
  • 公司内网或CI流水线不允许每台打包机器都装Android Studio,只提供命令行环境。
  • Unity构建机是Docker或远程Linux环境,图形界面缺失,Android Studio根本没法跑。
  • 想严格控制aar的Gradle版本、依赖项,防止Unity主工程模板被自动改乱。

在这些情况下,手动用Gradle+Android SDK打包,不只是“可以用”,而是最干净、最可控的方式。

1.3 选Gradle而不是aapt2的核心理由

有人会问,Android SDK里不是有aapt2和javac吗?能不能直接绕开Gradle,用命令行把class和资源压成aar?理论上可以,但实际工作量会让你崩溃。资源编译、R.java生成、AAR标准化结构、多Module依赖、混淆规则,这些环节全靠手工处理太容易出错。

最关键的是,Unity本身构建APK时就会读取Gradle模板,你手动打包出来的aar应该和Unity用的Gradle体系保持兼容。用Gradle打aar,相当于让产物从出生开始就符合Unity构建框架的预期,避免后续各种未知的兼容性问题。另外,Gradle的依赖传递机制能帮你把第三方库处理好,手敲命令行可没有这个待遇。

2. 打包前的环境准备:一套能稳定复现的工具链

2.1 JDK、Android SDK、Gradle三件套怎么配

手动打包,最核心的工具链就是JDK、Android SDK、Gradle。先说JDK版本,Unity在不同版本对应的JDK要求差别很大,Unity 2020及以前通常配Java 8,Unity 2021以后某些版本推荐Java 11。这个版本必须和你本机Unity设置里的JDK版本一致,否则你本地编译没问题,Unity一构建就疯狂报版本错。

Android SDK方面,只需要两样东西:platforms/android-XX下的android.jar,以及build-tools里的aapt2d8zipalign这些基础工具。不用装Android Studio,直接去Android官网下载Command Line Tools,然后用sdkmanager把需要的platform和build-tools拉下来就可以。如果公司有内网镜像,速度会更快。

Gradle版本就要谨慎一点了,不同AGP(Android Gradle Plugin)版本打包出来的产物行为有差异,我建议直接参考Unity自带的Gradle版本,稍后展开。

注意:环境变量里的JAVA_HOMEANDROID_HOME必须提前配好。如果有多套JDK,务必确保当前shell里生效的JDK与Unity设置里的一致,这是避免一堆诡异问题的最省心做法。

2.2 直接拿Unity自带Gradle,版本不会乱

Unity安装目录里其实自带了一份Gradle,位置大致在:

Windows: Editor\Data\PlaybackEngines\AndroidPlayer\Tools\Gradle macOS: Unity.app/Contents/PlaybackEngines/AndroidPlayer/Tools/Gradle

用这份Gradle的好处很直接:Unity构建APK用的就是它,你手动打包时用同款版本,出来的aar在兼容性上几乎不会出问题。下载第三方Gradle当然也可以,但版本一旦和Unity内置的AGP对不上,就会出现一些“本地能编译,Unity构建时却炸了”的现象,排查起来费神。

更稳妥的做法是打开Unity项目,找到Assets/Plugins/Android/mainTemplate.gradle(如果没有就确认是否启用了自定义Gradle模板),直接看里面的com.android.tools.build:gradle:xxx版本号,然后用匹配的AGP写你自己的Gradle配置文件。

2.3 项目目录结构与初始化形态

手动打包之前,先把目录结构规划好。我在项目中喜欢建一个和Unity工程分离的独立文件夹,比如android-libs/mylib/,避免资源混在一起:

android-libs/ ├── mylib/ │ ├── src/main/ │ │ ├── java/com/example/mylib/MyLib.java │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── proguard-rules.pro │ └── gradle.properties ├── build.gradle ├── settings.gradle └── gradle.properties

这个结构和Android Studio里的Library Module形态是一致的,但不需要app层、不需要kotlin模块、不需要各种IDE自动生成的文件,只留下Gradle打包必须的东西。这样整个工程可以提交到Git里,任何一台机器clone后都能直接跑命令打包。

2.4 坑:版本不匹配的连锁反应

版本不匹配是新手最常踩的坑,举一个真实例子:Unity主工程模板用的是AGP 4.0.1,Gradle 6.1.1,如果你手动打包时用了AGP 7.x,打出来的aar结构可能混入新格式,Unity构建时旧Gradle解析不了新版资源格式,直接报AAPT2 error,出错位置还不在你的aar,在Unity的Gradle Build日志深处。

所以我的建议是:不要追新。手动打包的核心目的是稳定交付,不是尝新,无脑对齐Unity内置Gradle和AGP版本就对了。下表是我整理的常见Unity版本和Gradle/AGP对应关系,供参考:

Unity版本默认Gradle默认AGP推荐JDK
2019.45.1.13.4.0JDK 8
2020.36.1.14.0.1JDK 8
2021.36.1.14.0.1JDK 11
2022.36.7.14.2.0JDK 11

当然,你的Unity项目可能通过Gradle模板手动升级过AGP版本,一切以实际项目的mainTemplate.gradle为准。

3. 手把手打包出aar:从Java源码到可验证产物

3.1 Java源码:写一个带回调的工具类

为了让示例足够贴近真实场景,我这里写一个稍微复杂一点的工具类,包含静态方法、实例方法,以及一个最简回调接口。这个类后面会一步步被编译进aar,并在Unity侧调用。

package com.example.mylib; import android.app.Activity; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.widget.Toast; public class MyLib { private static Handler sMainHandler = new Handler(Looper.getMainLooper()); public interface ICallback { void onResult(String message); } public static void showToast(Context context, String message) { if (context instanceof Activity) { Activity activity = (Activity) context; if (!activity.isFinishing() && !activity.isDestroyed()) { Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); } } else { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); } } public static int add(int a, int b) { return a + b; } public void postDelayedResult(final String input, final ICallback callback) { final long delayMs = 1000; sMainHandler.postDelayed(new Runnable() { @Override public void run() { if (callback != null) { callback.onResult("result: " + input); } } }, delayMs); } }

这段代码里有两点值得注意。第一,Toast逻辑里判断了Activity是否正在finish或destroyed,避免在Unity执行切场景操作时弹出崩溃性异常。第二,回调接口用Handler切到主线程执行,确保Java侧如果有耗时任务,回调会回到Android UI线程,避免多线程同步问题。

3.2 AndroidManifest.xml:最小声明与合并规则

aar里的AndroidManifest.xml不能随便乱写,它会被Unity合并进最终的APK清单。最稳妥的策略是只做基础声明,不写任何多余的东西。以当前例子来说,MyLib的工具类没有Activity、没有Service、不需要自定义权限,所以Manifest只需要一个空壳。

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.mylib"> </manifest>

如果你在aar里声明了Activity,那么所有涉及这个Activity的android:nameexportedintent-filter这些属性都要在aar的Manifest里写对,否则Unity合并时或运行时可能报找不到Activity。

这里有一个容易踩的坑:如果你在aar的Manifest里声明了<application android:name=".MyApp">,而Unity主工程或另一个aar也声明了Application,两个会冲突。优先把Application相关声明放在Unity的mainTemplate.gradle或Unity的AndroidManifest.xml里,aar里不要碰。

3.3 build.gradle:把库模块配置到可用状态

关键文件来了。整个手动打包流程的核心就是build.gradle的配置。先看项目根目录下的build.gradle,它的作用是声明构建插件和仓库:

buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' } } allprojects { repositories { google() mavenCentral() } }

除非有必要,否则不要在allprojects.repositories里加jcenter(),早期很多教程里都有它,但现在JCenter已经进入只读状态,新依赖拉不到,能不用就不用。

接下来是模块级mylib/build.gradle

apply plugin: 'com.android.library' android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { minSdkVersion 22 targetSdkVersion 30 versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { // 这里放aar依赖的第三方库,例如: // implementation 'androidx.appcompat:appcompat:1.3.1' }

很多人会纠结compileSdkVersion到底写多少,我的建议是:看Unity项目里的mainTemplate.gradle,Unity默认的compileSdkVersion是多少,就写多少。版本号高了或低了都会在Unity构建时产生资源版本兼容问题。

如果Java代码里用到了AndroidX,那就在dependencies里加对应的依赖,同时在项目的gradle.properties里开启:

android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m

android.useAndroidX=true是必须的,否则在依赖AndroidX库时会报错。

3.4 执行打包命令并验证aar内容

配置完成后,在android-libs目录下执行打包命令:

# 如果使用系统全局Gradle gradle :mylib:assembleRelease # 如果使用Unity自带Gradle /path/to/Gradle/gradle :mylib:assembleRelease

首次执行时会下载Gradle依赖和插件,耗时取决于网络环境。如果网络不好,后面第5.2节有解决方案。

打包完成后,产物位于:

mylib/build/outputs/aar/mylib-release.aar

用解压工具打开这个aar,你会看到里面出现了classes.jarAndroidManifest.xmlR.txt这些标准文件。想验证classes.jar里的类是否存在,可以用jar命令:

jar tf mylib/build/outputs/aar/mylib-release.aar # 输出里会有 classes.jar # 解压后看class unzip mylib/build/outputs/aar/mylib-release.aar -d aar_unzip jar tf aar_unzip/classes.jar | grep MyLib

看到com/example/mylib/MyLib.class就算打包成功了。

3.5 是否引入Unity classes.jar的取舍

手动打包时,Java代码里如果要用到com.unity3d.player.UnityPlayer这个类,就需要在编译时引入Unity的classes.jar。常见引入方法是在mylib/build.gradledependencies里加一句:

compileOnly files("$UNITY_CLASSES_JAR")

这里的UNITY_CLASSES_JAR变量可以通过环境变量传入,或者直接写绝对路径,它通常位于Unity安装目录下的:

Editor/Data/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar

注意这里用的是compileOnly,不是implementationapi。原因很直接:Unity本身的classes.jar会出现在Unity构建的最终APK里,如果你的aar又把这个类打进去,就重复了,运行时大概率出现“duplicate class”的Crash。用compileOnly只参与编译,不进入aar产物,让最终Unity构建时各归各位,这个坑很多新手会踩。

4. 在Unity里接入aar并打通双向调用

4.1 aar放置位置和Unity的打包规则

将aar放入Unity工程目录:

Assets/Plugins/Android/

Unity在构建Android APK时,会自动扫描这个目录下的aar和jar文件,把它们作为依赖加入Gradle工程。如果aar附带有自己的AndroidManifest.xml,Unity也会把它合并进最终APK的Manifest。这个自动合并过程大多数时候没问题,但遇到Application冲突等情况就要手动处理了。

如果你的aar依赖了第三方库,比如androidx.appcompat,Unity构建时可能会因为主工程的Gradle模板里没有这些依赖而报错。解决办法是打开Unity的mainTemplate.gradle,把aar需要的依赖加进去。

4.2 C#调Java:AndroidJavaClass与AndroidJavaObject

Unity侧调用Java要借助AndroidJavaClassAndroidJavaObject这两个类。AndroidJavaClass用于访问Java静态方法或静态字段,AndroidJavaObject用于实例化Java对象和调用实例方法。

以下代码演示如何调用我们刚才打包的MyLib

using UnityEngine; public class MyLibBridge : MonoBehaviour { private AndroidJavaObject _myLib; private void Start() { AndroidJavaClass playerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject activity = playerClass.GetStatic<AndroidJavaObject>("currentActivity"); // 调用静态方法showToast AndroidJavaClass myLibClass = new AndroidJavaClass("com.example.mylib.MyLib"); myLibClass.CallStatic("showToast", activity, "Hello from aar"); // 调用静态方法add int sum = myLibClass.CallStatic<int>("add", 3, 5); Debug.Log("Sum = " + sum); // 实例化并调用实例方法postDelayedResult _myLib = new AndroidJavaObject("com.example.mylib.MyLib"); MyCallback callback = new MyCallback(); _myLib.Call("postDelayedResult", "UnityValue", callback); } private class MyCallback : AndroidJavaProxy { public MyCallback() : base("com.example.mylib.MyLib$ICallback") { } public void onResult(string message) { Debug.Log("Callback from Java: " + message); } } }

这里有个细节:内部接口ICallback在Java源码里是MyLib的内部类,JNI名称会是com.example.mylib.MyLib$ICallback,所以传给AndroidJavaProxy的base字符串里要写$符号。如果Java接口是独立顶层接口,就直接写完整类名,不要带$。这一点容易踩坑,JNI规则对内部类的命名和Java层不一样,漏掉$会直接抛NoSuchMethodError

4.3 Java回调C#:用AndroidJavaProxy和UnitySendMessage

在4.2的例子中,我用了AndroidJavaProxy让Java回调直接进入C#。AndroidJavaProxy的好处是类型安全,回调参数可以自动转成C#对象,适合参数较多、数据类型复杂的场景。

另一种常见的回调方式是Java侧调用UnitySendMessage,这个API要求场景里有一个名字对应的GameObject并挂载一个方法名对应的脚本:

UnityPlayer.UnitySendMessage("BridgeObject", "OnJavaCallback", "message from java");

UnitySendMessage的问题在于,Android UI线程和Unity主线程并不总是同一个线程,直接调用UnitySendMessage跨线程操作Unity API可能会导致崩溃。另外UnitySendMessage传递的字符串有长度限制,大概在几KB级别,不适用于大数据。所以如果回调逻辑复杂,优先用AndroidJavaProxy,它至少在Unity侧可以把回调派发到你期望的线程再执行。

4.4 AndroidManifest合并的细节

aar的Manifest和Unity工程本身的Manifest会在构建时自动合并。如果两个Manifest里都声明了同一个Activity,或者都有<application>的属性配置,合并规则不是你随便写写就行的。

我的经验是在打包aar之前,把该类库使用到的Activity、权限等声明全部集中在aar的Manifest里;如果确实和Unity主工程有冲突,先确认Unity侧能不能改。大多数插件SDK的情况,权限声明放aar里没有问题,但Application节点尽量避开。合并后的Manifest可以在Unity的Temp/StagingArea/AndroidManifest.xml路径下查看,Unity构建失败时查这里特别有用。

5. 常见问题与排查实录

5.1 打包报错:找不到SDK Platform

执行打包时如果出现:

Failed to find target with hash string 'android-30'

说明Android SDK目录下没有下载对应版本的Platform。用sdkmanager安装:

sdkmanager "platforms;android-30" "build-tools;30.0.3"

如果网络受限,可以去对应厂商的镜像源手动下载平台包并放到platforms目录下。

5.2 依赖下不动:换国内镜像源

Gradle下载依赖卡的场景太常见了。如果google()和mavenCentral()访问缓慢或超时,可以在根目录的build.gradle里临时加上国内镜像仓库:

buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } } }

这个方案只推荐在依赖拉不动时用,正式上CI前最好把依赖缓存好,或者直接配置公司内部的Maven私服。

5.3 Unity运行时报ClassNotFound

aar明明编译进APK了,运行却报ClassNotFoundException?最常见的三个原因:

  • 混淆把类名改掉了。
  • aar的package和你C#里写的类名不一致。
  • 编译时用错了implementation导致依赖没有传递到Unity主工程。

针对混淆,release包默认会开混淆,如果打包时不想混淆,可以在mylib/build.gradle里加:

android { buildTypes { release { minifyEnabled false } } }

如果确实需要混淆,就得准备proguard-rules.pro,把Unity会调用的类keep住:

-keep class com.example.mylib.** { *; }

把这条规则加到defaultConfigproguardFiles配置里。

注意:Unity侧用反射方式调用Java类时,如果Java类名在proguard阶段被改掉,Unity跑起来就直接找不到了,所以keep规则能写多宽写多宽。

5.4 回调线程与主线程之间的问题

我在真实的Pico 4和普通安卓手机上做测试时,遇到过一次“Unity页面卡死但Logcat有日志”的情况。后来定位到是Java侧回调的线程不固定,Unity侧直接操作了GameObject和Transform,导致Unity主线程冲突。解决方式很简单:Java侧统一用Handler切到主线程,或C#侧收集消息后放入队列,在Update里统一处理。

代码层面的典型写法:

private Queue<string> _messageQueue = new Queue<string>(); private void Update() { lock (_messageQueue) { while (_messageQueue.Count > 0) { string msg = _messageQueue.Dequeue(); // 在这里安全操作Unity API } } }

5.5 依赖冲突:support和androidx的修罗场

aar里如果带了支持库依赖,很容易和Unity或另一个aar的依赖冲突。常见报错是:

Duplicate class android.support.v4.app...

这类问题的根源是AndroidX和旧版Support库混用。Unity 2020及之后的版本默认使用AndroidX,所以如果手动打包的aar引用了旧的support库,尽量手动改成AndroidX。开启android.enableJetifier=true可以自动转换依赖中的Support库到AndroidX,但它不能解决所有问题,最好的办法还是源头上依赖就用AndroidX。

另外,如果同一个第三方库在多个aar里都打了包,Unity构建时会报重复类,这时需要判断哪个aar是权威来源,把另一个里面对应的classes.jar用exclude规则排除掉,或者干脆修改依赖方式,不要在aar里带第三方库,统一放到Unity的mainTemplate.gradle里。

5.6 打包时原Garble打印一堆警告

手动打包时经常看到类似warning: [options] bootstrap class path not set in conjunction with -source 8这样的警告,一般不影响产物,可以忽略。但如果警告里带error关键字,就要拿日志去查具体问题。这类警告的本质是因为JDK版本和Java源码版本不匹配,比如JDK 11编译sourceCompatibility 1.8的代码时就会提示。

6. 几个我后来才想明白的打包心得

打包aar这件事,表面上只是一条gradle assembleRelease命令,但涉及到版本一致性、依赖抽取、Manifest合并、JNI命名这类细节后,复杂度就上来了。我个人在实际操作中最重视的,永远是“和Unity构建环境保持对齐”这一条。

现在的Unity项目普遍使用Gradle作为Android构建工具链,aar作为中间产物,只有在和Unity的Gradle版本、AGP版本、JDK版本、compileSdk保持同步的情况下,才能在Unity里无缝工作。很多人只关注“能不能打出aar”,忽略了“这个aar在Unity里能不能顺利构建”,这两者之间隔着无数版本问题。

还有一点一定要记住:aar的构建环境尽量独立。不要把你Unity项目的Assets目录当成打包工程,最好单独建一个android-libs项目,里面只放Java源码、Gradle配置和aar产物,这样任何人在任何机器上Clone下来都能一条命令复现打包。而且也不会因为Unity升级、插件目录变化而误删或混淆源码。

我在后续项目里,还会把手动打包的Gradle命令接入CI,让提交代码后自动构建一份新的aar产物,推送发布到内网仓库。这个过程里,手动打包流程反而比Android Studio方案更有优势,因为它轻量、可脚本化、不依赖图形界面。如果你也在维护Unity插件或游戏SDK,建议尽早把手动打包这件事跑通,它带来的长期收益会远超第一眼看上去的麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询