Android Platform
文档用途
记录使用 Unity 做 Android 平台开发时的常用操作和和注意事项.
构建方案
INFO
这里不涉及通过 Unity Editor 中的 Build Settings 窗口中的按钮来构建的方案.
仅讨论通过命令行工具构建的方案, 以及如何使用 GitLab CI/CD 来集成.
方案对比
| 构建方案 | 简述 |
|---|---|
单步构建方案 | 通过 Unity 一步到位构建 apk 和 aab |
分步构建方案 | 先用 Unity 导出 Gradle 工程, 再通过 Gradle 工程构建 apk 与 aab |
单步构建方案 的好处在于:
配置简单, 流程少, 打单个包用时短.
而 分步构建方案 的好处在于:
- 能够利用 build cache 加快构建速度.
例如既需要构建 apk 也需要构建 aab, 所需时间非常接近于单次构建的用时. - 可以更灵活的定制构建管线, 例如针对 Gradle 工程做一些修改.
- 避免某些情况下 Unity 安卓版本构建失败的问题.
分步构建方案 的缺陷是: 流程更繁琐.
但配合 CI/CD 就完全不担心这个问题了.
单步构建方案
构建结果是 apk 还是 aab, 可以通过以下 C# 代码设置:
UnityEditor.EditorUserBuildSettings.buildAppBundle = true; // 构建 aab用于构建的核心 C# API 是 BuildPipeline.BuildPlayer 方法.
通过命令行调用以下脚本中的 PerformBuild() 方法就能构建:
BuildCommand.cs
由于其依赖于很多环境变量, 因此直接调用应该没法成功构建, 但源代码可以做参考.
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
public static class BuildCommand
{
/// <summary> Android keystore 的文件名 </summary>
const string KEYSTORE_NAME = "keystore.keystore";
/// <summary> Android keystore 的密码 </summary>
const string KEYSTORE_PASS = "KEYSTORE_PASS";
/// <summary> Android keystore alias 的名称 </summary>
const string KEY_ALIAS_NAME = "KEY_ALIAS_NAME";
/// <summary> Android keystore alias 的密码 </summary>
const string KEY_ALIAS_PASS = "KEY_ALIAS_PASS";
const string BUILD_TARGET = "BUILD_TARGET";
const string BUILD_PATH = "BUILD_PATH";
const string BUILD_NAME = "BUILD_NAME";
const string SCRIPTING_BACKEND = "SCRIPTING_BACKEND";
/// <summary> 构建选项 </summary>
const string BUILD_OPTIONS = "BUILD_OPTIONS";
/// <summary> 安卓的 bundle version code </summary>
const string ANDROID_BUNDLE_VERSION_CODE = "ANDROID_BUNDLE_VERSION_CODE";
/// <summary> 是否构建 aab (如果选择构建 aab 就不会构建 apk) </summary>
const string ANDROID_APP_BUNDLE = "ANDROID_APP_BUNDLE";
/// <summary> 安卓的版本号 </summary>
const string ANDROID_VERSION = "ANDROID_VERSION";
/// <summary> 用于决定是否导出 Gradle 工程 </summary>
const string EXPORT_GRADLE = "EXPORT_GRADLE";
/// <summary> iOS 的版本号 </summary>
const string IOS_VERSION = "IOS_VERSION";
#region utility methods
/// <summary> 尝试将 string 转换为指定的枚举类型 </summary>
static bool TryConvertToEnum<TEnum>(this string strEnumValue, out TEnum value)
where TEnum : Enum
{
if (!Enum.IsDefined(typeof(TEnum), strEnumValue))
{
value = default;
return false;
}
value = (TEnum)Enum.Parse(typeof(TEnum), strEnumValue);
return true;
}
/// <summary> 获取环境变量 </summary>
static bool TryGetEnv(string key, out string value)
{
value = Environment.GetEnvironmentVariable(key);
return !string.IsNullOrEmpty(value);
}
#endregion
/// <summary> 获取所有需要被构建的场景 </summary>
static string[] GetEnabledScenes()
{
return (
from scene in EditorBuildSettings.scenes
where scene.enabled
where !string.IsNullOrEmpty(scene.path)
select scene.path
).ToArray();
}
/// <summary> 获取目标平台 </summary>
static BuildTarget GetBuildTarget()
{
if (TryGetEnv(BUILD_TARGET, out string buildTargetName))
Console.WriteLine(":: Received BuildTarget " + buildTargetName);
else
throw new ArgumentNullException($"cannot find environment variable called {BUILD_TARGET}");
if (buildTargetName.TryConvertToEnum(out BuildTarget target))
return target;
else
throw new ArgumentException($":: {nameof(buildTargetName)} \"{buildTargetName}\" not defined on enum {nameof(BuildTarget)}");
}
/// <summary> 获取构建文件的存放路径 </summary>
static string GetBuildPath()
{
if (TryGetEnv(BUILD_PATH, out string buildPath))
Console.WriteLine(":: Received BuildPath " + buildPath);
else
throw new ArgumentNullException($":: cannot find environment variable called {BUILD_PATH}");
return buildPath;
}
/// <summary> 获取构建文件的自定义名称 </summary>
static string GetBuildName()
{
if (TryGetEnv(BUILD_NAME, out string buildName))
Console.WriteLine(":: Received BuildName " + buildName);
else
throw new ArgumentNullException($":: cannot find environment variable called {BUILD_NAME}");
return buildName;
}
/// <summary> 获得最终的构建文件路径和文件名 </summary>
static string GetFixedBuildPath(BuildTarget buildTarget, string buildPath, string buildName)
{
if (buildTarget.ToString().ToLower().Contains("windows"))
{
buildName += ".exe";
}
else if (buildTarget == BuildTarget.Android)
{
if (EditorUserBuildSettings.exportAsGoogleAndroidProject == false)
buildName += EditorUserBuildSettings.buildAppBundle ? ".aab" : ".apk";
}
return buildPath + buildName;
}
/// <summary> 获取构建设置 </summary>
static BuildOptions GetBuildOptions()
{
BuildOptions allOptions = BuildOptions.None;
if (TryGetEnv(BUILD_OPTIONS, out string envVar))
{
string[] allOptionVars = envVar.Split(',');
string optionVar;
int length = allOptionVars.Length;
Console.WriteLine($":: Detecting {BUILD_OPTIONS} env var with {length} elements ({envVar})");
for (int i = 0; i < length; i++)
{
optionVar = allOptionVars[i];
if (optionVar.TryConvertToEnum(out BuildOptions option))
allOptions |= option;
else
Console.WriteLine($":: Cannot convert {optionVar} to {nameof(BuildOptions)} enum, skipping it.");
}
}
else
{
Console.WriteLine($":: cannot find environment variable {BUILD_OPTIONS}");
Console.WriteLine($":: BuildOptions is now set by this script \"{nameof(BuildCommand.GetBuildOptions)}\"");
allOptions |= BuildOptions.CompressWithLz4;
// allOptions |= BuildOptions.Development;
}
return allOptions;
}
static void SetScriptingBackend(BuildTarget platform)
{
BuildTargetGroup targetGroup = BuildPipeline.GetBuildTargetGroup(platform);
if (TryGetEnv(SCRIPTING_BACKEND, out string scriptingBackend))
{
if (scriptingBackend.TryConvertToEnum(out ScriptingImplementation backend))
{
Console.WriteLine($":: Setting ScriptingBackend to {backend}");
PlayerSettings.SetScriptingBackend(targetGroup, backend);
}
else
{
string possibleValues = string.Join(", ", Enum.GetValues(typeof(ScriptingImplementation)).Cast<ScriptingImplementation>());
throw new Exception($"Could not find '{scriptingBackend}' in ScriptingImplementation enum. Possible values are: {possibleValues}");
}
}
else
{
// 如果不通过 SCRIPTING_BACKEND 指定,那么就按照 PlayerSettings 中的设置来决定
ScriptingImplementation defaultBackend = PlayerSettings.GetScriptingBackend(targetGroup);
Console.WriteLine($":: Using project's configured ScriptingBackend (should be {defaultBackend} for targetGroup {targetGroup})");
}
}
/// <summary> 根据目标平台决定版本号 </summary>
private static void SetBundleVersion(BuildTarget buildTarget)
{
string versionVar = buildTarget switch
{
BuildTarget.Android => ANDROID_VERSION,
BuildTarget.iOS => IOS_VERSION,
_ => throw new NotSupportedException($"buildTarget {buildTarget} doesn't have any compatible env var")
};
if (TryGetEnv(versionVar, out string bundleVersion))
{
Console.WriteLine($":: Setting bundleVersion to '{bundleVersion}' (Length: {bundleVersion.Length})");
PlayerSettings.bundleVersion = bundleVersion;
}
else
Console.WriteLine($":: cannot find environment variable {versionVar}" + Environment.NewLine
+ $":: bundleVersion is set by PlayerSettings");
}
/// <summary> 决定是否生成 Gradle 工程(而不是直接出包)</summary>
private static void HandleGradle()
{
if (TryGetEnv(EXPORT_GRADLE, out string value))
{
if (bool.TryParse(value, out bool isGradle))
{
EditorUserBuildSettings.exportAsGoogleAndroidProject = isGradle;
Console.WriteLine($":: {EXPORT_GRADLE} evn var detected, " +
$"set {nameof(EditorUserBuildSettings.exportAsGoogleAndroidProject)} to {value}");
}
else
throw new ArgumentException($":: {EXPORT_GRADLE} evn var detected, but the value \"{value}\" is not a boolean");
}
else
Console.WriteLine($":: cannot find environment variable {EXPORT_GRADLE}");
}
/// <summary> 决定是要构建 apk 还是 aab </summary>
private static void HandleAndroidAppBundle()
{
if (TryGetEnv(ANDROID_APP_BUNDLE, out string value))
{
if (bool.TryParse(value, out bool buildAppBundle))
{
EditorUserBuildSettings.buildAppBundle = buildAppBundle;
Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected, set buildAppBundle to {value}.");
}
else
throw new ArgumentException($":: {ANDROID_APP_BUNDLE} env var detected but the value \"{value}\" is not a boolean.");
}
else
{
Console.WriteLine($":: cannot find environment variable {ANDROID_APP_BUNDLE}");
Console.WriteLine($":: using EditorUserBuildSettings (should be {EditorUserBuildSettings.buildAppBundle} to build aab)");
}
}
/// <summary> 决定安卓版本的 Bundle Version Code </summary>
private static void HandleAndroidBundleVersionCode()
{
if (TryGetEnv(ANDROID_BUNDLE_VERSION_CODE, out string value))
{
if (int.TryParse(value, out int version))
{
PlayerSettings.Android.bundleVersionCode = version;
Console.WriteLine($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected, set the bundle version code to {value}.");
}
else
throw new ArgumentException($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected but the version value \"{value}\" is not an integer.");
}
else
{
Console.WriteLine($":: cannot find environment variable {ANDROID_BUNDLE_VERSION_CODE}");
Console.WriteLine($":: bundleVersionCode is set by PlayerSettings (should be {PlayerSettings.Android.bundleVersionCode})");
}
}
/// <summary> 处理安卓版本的 keystore </summary>
private static void HandleAndroidKeystore()
{
PlayerSettings.Android.useCustomKeystore = false;
if (!File.Exists(KEYSTORE_NAME))
{
Console.WriteLine($":: {KEYSTORE_NAME} not found, skipping setup, using Unity's default keystore");
return;
}
PlayerSettings.Android.keystoreName = KEYSTORE_NAME;
if (TryGetEnv(KEY_ALIAS_NAME, out string keyaliasName))
{
PlayerSettings.Android.keyaliasName = keyaliasName;
Console.WriteLine($":: using ${KEY_ALIAS_NAME} env var on PlayerSettings");
}
else
{
Console.WriteLine($":: ${KEY_ALIAS_NAME} env var not set, using Project's PlayerSettings");
return;
}
if (!TryGetEnv(KEYSTORE_PASS, out string keystorePass))
{
Console.WriteLine($":: ${KEYSTORE_PASS} env var not set, skipping setup, using Unity's default keystore");
return;
}
if (!TryGetEnv(KEY_ALIAS_PASS, out string keystoreAliasPass))
{
Console.WriteLine($":: ${KEY_ALIAS_PASS} env var not set, skipping setup, using Unity's default keystore");
return;
}
PlayerSettings.Android.useCustomKeystore = true;
PlayerSettings.Android.keystorePass = keystorePass;
PlayerSettings.Android.keyaliasPass = keystoreAliasPass;
}
/// <summary> 被命令行调用的方法 </summary>
public static void PerformBuild()
{
Console.WriteLine(":: Performing build");
BuildTarget buildTarget = GetBuildTarget();
SetBundleVersion(buildTarget);
SetScriptingBackend(buildTarget);
if (buildTarget == BuildTarget.Android)
{
HandleGradle();
HandleAndroidAppBundle();
HandleAndroidBundleVersionCode();
HandleAndroidKeystore();
}
string buildPath = GetBuildPath();
string buildName = GetBuildName();
BuildOptions buildOptions = GetBuildOptions();
string fixedBuildPath = GetFixedBuildPath(buildTarget, buildPath, buildName);
BuildReport buildReport = BuildPipeline.BuildPlayer(GetEnabledScenes(), fixedBuildPath, buildTarget, buildOptions);
if (buildReport.summary.result != BuildResult.Succeeded)
throw new Exception($":: Build ended with {buildReport.summary.result} status");
Console.WriteLine(":: Done with build");
}
}Unity 部分设置并不在工程路径, 而是放在操作系统的特定目录下, 与所有 Unity 工程共享.
然而, 使用命令行运行 Unity 时, 无法自动获取这些设置, 因此需要手动写代码来设置.
详见 Unity 官方文档 ScriptReference - EditorPrefs, 也可以参考此示例:
EditorPrefsManager.cs
using System;
using UnityEditor;
using UnityEngine;
// Unity 的部分设置并没有存放在工程文件夹内,而是放在操作系统的特定目录下,给所有 Unity 工程共享.
// 然而,使用命令行运行 unity 时,无法自动获取这些设置,因此需要手动写代码来设置.
// 详见 Unity 官方文档: https://docs.unity3d.com/ScriptReference/EditorPrefs.html
public static class EditorPrefsManager
{
#pragma warning disable IDE1006
/// <summary> 安卓 sdk 所在目录 </summary>
const string ANDROID_SDK_PATH = "ANDROID_SDK_PATH";
const string AndroidSdkRoot = "AndroidSdkRoot";
/// <summary> 安卓 ndk 所在目录(Unity 2020 即更早的版本) </summary>
const string ANDROID_NDK_PATH_19 = "ANDROID_NDK_PATH_19";
const string AndroidNdkRootR19 = "AndroidNdkRootR19";
/// <summary> 安卓 ndk 所在目录(Unity 2021 及更新的版本) </summary>
const string ANDROID_NDK_PATH_21 = "ANDROID_NDK_PATH_21";
const string AndroidNdkRootR21D = "AndroidNdkRootR21D";
/// <summary> Gradle 所在目录 </summary>
const string GRADLE_PATH = "GRADLE_PATH";
const string GradlePath = "GradlePath";
/// <summary> JDK 所在目录 </summary>
const string JDK_PATH = "JDK_PATH";
const string JdkPath = "jdkPath";
#pragma warning restore IDE1006
/// <summary> 被命令行调用的方法 </summary>
static void SetEditorPrefs()
{
if (!TryGetEnv(ANDROID_SDK_PATH, out string sdkPath))
throw new ArgumentNullException($":: cannot find environment variable \"{ANDROID_SDK_PATH}\"");
else
EditorPrefs.SetString(AndroidSdkRoot, sdkPath);
if (!TryGetEnv(ANDROID_NDK_PATH_19, out string ndkPath))
throw new ArgumentNullException($":: cannot find environment variable \"{ANDROID_NDK_PATH_19}\"");
else
EditorPrefs.SetString(AndroidNdkRootR19, ndkPath);
if (!TryGetEnv(ANDROID_NDK_PATH_21, out string ndkPath21))
throw new ArgumentNullException($":: cannot find environment variable \"{ANDROID_NDK_PATH_21}\"");
else
EditorPrefs.SetString(AndroidNdkRootR21D, ndkPath21);
if (!TryGetEnv(GRADLE_PATH, out string gradlePath))
{
Console.WriteLine($":: cannot find environment variable \"{GRADLE_PATH}\"");
gradlePath = EditorPrefs.GetString(GradlePath);
Console.WriteLine($":: using default gradlePath: {gradlePath}");
}
else
EditorPrefs.SetString(GradlePath, gradlePath);
if (!TryGetEnv(JDK_PATH, out string jdkPath))
{
Console.WriteLine($":: cannot find environmen variable \"{JDK_PATH}\"");
jdkPath = EditorPrefs.GetString(JdkPath);
Console.WriteLine($":: using default JDK path: {jdkPath}");
}
else
EditorPrefs.SetString(JdkPath, jdkPath);
}
[MenuItem("Tools/Show EditorPrefs")]
static void ShowEditorPrefs()
{
string sdkPath = EditorPrefs.GetString(AndroidSdkRoot);
Debug.Log($"android sdk path: {sdkPath}");
string ndkPath19 = EditorPrefs.GetString(AndroidNdkRootR19);
Debug.Log($"android ndk path: {ndkPath19}");
string ndkPath21 = EditorPrefs.GetString(AndroidNdkRootR21D);
Debug.Log($"android ndk path (Unity 2021 and above): {ndkPath21}");
string gradlePath = EditorPrefs.GetString(GradlePath);
Debug.Log($"Gradle Path: {gradlePath}");
string jdkPath = EditorPrefs.GetString(JdkPath);
Debug.Log($"JDK path: {jdkPath}");
}
static bool TryGetEnv(string key, out string value)
{
value = Environment.GetEnvironmentVariable(key);
return !string.IsNullOrEmpty(value);
}
}分步构建方案
INFO
参考安卓开发者官方文档 Build your app from the command line.
要想通过 Unity 命令行导出 Gradle 工程, 可以通过以下 C# 代码设置:
UnityEditor.EditorUserBuildSettings.exportAsGoogleAndroidProject = true;通过调用 Gradle 命令来构建:
cd [gradle-project]
# 给工程下载 7.2 版本的 gradle, 并初始化
gradle wrapper --gradle-version 7.2 --distribution-type bin
./gradlew assemble --daemon --parallel --quiet # 构建 apk
./gradlew bundle --daemon --parallel --quiet # 构建 aab证书与密码
CI/CD 环境变量
由于我们需要给应用签名 (通过 keystore), 但为了安全:
不能把证书文件放入 git 工程中, 也不能在 git 工程的任意位置记录的密码.
因此建议 在 GitLab 网页中配置 keystore 相关的环境变量.
| 环境变量 | 含义 | 备注 |
|---|---|---|
MY_KEYSTORE | 密钥库文件 (xxx.keystore) 的路径 | 好好保存, 避免丢失, 避免泄露 |
MY_KEYSTORE_PASSWORD | 密钥库文件的密码 | 记得要 启用 mask |
KEY_ALIAS | 密钥的别称 | 每个安卓应用都需要独特的密钥 |
KEY_PASSWORD | 密钥的密码 | 记得要 启用 mask |
Gradle Template
为了使用 Gradle 给应用签名, 我们需要修改模板文件 launcherTemplate.gradle.
在 Project Settings / Player / Android / Publish Settings 中勾选:

修改 launcherTemplate.gradle
//...
signingConfigs { // [!code ++]
release { // [!code ++]
storeFile file(System.getenv('MY_KEYSTORE')) // [!code ++]
storePassword System.getenv('MY_KEYSTORE_PASSWORD') // [!code ++]
keyAlias System.getenv('KEY_ALIAS') // [!code ++]
keyPassword System.getenv('KEY_PASSWORD') // [!code ++]
} // [!code ++]
} // [!code ++]
buildTypes {
debug {
minifyEnabled **MINIFY_DEBUG**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGNCONFIG**
jniDebuggable true
}
release {
minifyEnabled **MINIFY_RELEASE**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGNCONFIG**
signingConfig signingConfigs.release // [!code ++]
}
}**PACKAGING_OPTIONS****PLAY_ASSET_PACKS****SPLITS**
// ...WARNING
自定义 Gradle 相关的文件模板后会导致使用 Unity Editor 的 UI 界面构建失败.
因此当需要使用 Unity Editor 按钮来构建时, 不勾选 Custom Launcher Gradle Template.
上述操作的效果相当于在 Unity 设置中启用了:

环境搭建
INFO
这里略过 C# 脚本和 .gitlab-ci.yml, 重点说明构建环境的配置.
我们只需要在运行 gitlab-runner 的设备中搭建构建环境.
Git 长路径
由于 gradle 构建时所产生的一些中间文件, 有的文件名特别长.
因此需要配置 git, 使其支持长路径, 避免报错.
git config --global core.longpaths trueJDK 环境
INFO
安卓开发环境配置详见官方文档 Unity Manual - Android environment setup.
建议直接将 Unity 自动安装的 jdk 复制一份找个文件夹存放.
因为用其他方式安装的 jdk 版本不一致, 可能会出问题.
配置系统环境变量:
- 将
JAVA_HOME设置为 jdk 路径, 因为 gradle 需要用到这个环境变量. - 在
Path中添加 jdk 路径, 用于确保名为java的指令能够被找到.
我们可以通过以下命令查看 jdk 是否成功安装并配置:
bash
echo $JAVA_HOME
java --versionWARNING
注意 macOS 默认的 shell 是 zsh, 其配置文件为 ~/.zprofile.
而 gitlab-runner 执行时使用 shell 是 bash, 其配置文件为 ~/.bash_profile.
因此一定要在 bash 中检测环境变量是否配置恰当.
Gradle 环境
INFO
Unity 版本与 Gradle 版本对应关系详见官方文档 Unity Manual - Gradle for Android.
可以考虑在官网下载 Gradle Release, 但更建议使用软件包管理器来安装.
注意这里安装的版本并不等于用于构建的版本, 详见 Gradle Wrapper.
brew install gradle # 安装最新版本
brew install gradle@7 # 安装主版本号为 7 的最新版本scoop install gradle # 安装最新版本
scoop install gradle7 # 安装主版本号为 7 的最新版本需要将 Gradle 的目录添加到系统环境变量 Path 中.
通过以下命令检验 Gradle 是否安装并配置成功:
gradle --version