摘要
本文学习了aab的基本概念以及UE4中产生aab的构建原理。
从官网了解基本概念
1、什么是aab?
.aab包形如:
2021年7月,在Google Play应用程序中,已经有数千个应用程序率先跟进了AAB格式。谷歌宣布从2021年8月份开始,所有提交到Google Play商店的新应用必须采用AAB格式。
全称Android App Bundles。谷歌在2018年启用了AAB新格式(AAB全称为Android App Bundles"),谷歌声称这种新格式将使应用程序文件更小,意味着aab分布式应用程序比通用apk平均少占用15% 的空间。更重要的是,它拓展了应用程序捆缚包的定义,只包含运行App时的必要代码。也就是说,下载了一部分之后,App就可以直接运行,无需等待下载完成再安装。
2、什么是PAD?
全称 Play Asset Delivery,它是能够打包出 aab 的组件。
3、三个分发模式是什么?
- install-time 资源包在用户安装应用时分发。这些资源包以拆分 APK(APK 集的一部分)的形式提供。它们也称为"预先"资源包;您可以在应用启动时立即使用这些资源包。这些资源包会增加 Google Play 商店上列出的应用大小。用户无法修改或删除这些资源包。
- fast-follow 资源包会在用户安装应用后立即自动下载;用户无需打开应用即可开始 fast-follow 下载。下载过程中,用户仍然可以进入应用。这些资源包会增加 Google Play 商店上列出的应用大小。
- on-demand 资源包会在应用运行时下载。
Google Play 商店会以归档文件(而非拆分 APK)的形式提供配置为 fast-follow 和 on-demand 的资源包。然后,这些资源包会在应用的内部存储空间中展开。
4、资源更新的方式
更新应用时,install-time Asset Pack 会作为基础应用更新的一部分进行更新(开发者无需执行任何操作)。
对于 fast-follow 和 on-demand Asset Pack 的应用更新,则遵循以下步骤:
1 系统将应用的补丁程序(包括所有资产)下载到设备上的安全位置。
2 更新应用二进制文件;这包括所有 install-time Asset Pack。
3 之前下载的所有 Asset Pack 变为无效。
4 将资源的补丁复制并应用到存储在应用内部存储空间中的资源。
5、什么是apks?apks,即apk split,分散的apk。
现在我们从谷歌商店上下载的应用多半都是apks格式的。
apks格式的安装包,就是使用了Android App Bundle(AAB)技术生成的安装包。
这种技术实际上就是将apk文件拆分成多个小包,再根据当前的设备选择适当的文件下载安装。
通过这种技术,谷歌商店就可以根据我们的设备生成一个适合当前设备的安装包,例如使用大屏幕设备的用户就不需要下载小屏幕的资源,使用arm v8架构设备的用户就不需要下载arm v7架构的资源(当然还存在x86架构的),使用中文的用户就不需要下载其他的语言文件,这样就可以在实现更多功能的同时减小app占用空间的大小。并且为开发者提供了灵活的分发方式和极高的性能。同时apks将不再支持使用obb数据包拓展软件,这也意味着使用obb数据包的xapk已经成为过去式了。
6、互斥性。
打apk就不应该打aab、apks;打aab、apks就不应该打apk。aab、apks应该同时出现。
开关
在设置中找到开关 "Generate bundle (AAB)",打开它表示生成Google的AAB。
位置是:
EngineSource\Engine\Source\Runtime\Android\AndroidRuntimeSettings\Classes\AndroidRuntimeSettings.h
// Enables generating AAB bundle
UPROPERTY(GlobalConfig, EditAnywhere, Category = "App Bundles", Meta = (DisplayName = "Generate bundle (AAB)"))
bool bEnableBundle;
它影响了 EngineSource\Engine\Source\Programs\UnrealBuildTool\Platform\Android\UEDeployAndroid.cs 中的逻辑,大致是:
// 位于 private void MakeApk(...) 方法中
if (!bEnableBundle) // 如果打的是 apk
{
......
RunCommandLineProgramWithExceptionAndFiltering(UE4BuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Making .apk with Gradle...");
......
}
else // 如果打的是aab、apks
{
......
RunCommandLineProgramWithExceptionAndFiltering(UE4BuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Making .aab with Gradle...");
......
}
所谓RunCommandLineProgramWithExceptionAndFiltering,字面上执行的是:
// 【书签1】
2024-10-12 15:14:19:037 : VERBOSE:
2024-10-12 15:14:19:037 : Running: cmd.exe /c "{我的项目}\Intermediate\Android\gradle\rungradle.bat" --stacktrace :app:bundleDebug --init-script init.gradle --profile
从字面意思 rungradle.bat 可以得知,无论是打 .apk 还是打 .aab/.apks,都是用 Gradle 来打包的。Gradle是安卓的构建打包管理工具。关于Gradle,推荐阅读《实战Gradle(中文完整版)》这本书。
rungradle.bat
这个脚本位于 {我的项目}\Intermediate\Android\gradle\.gradle 目录,它是 Engine\Source\Programs\UnrealBuildTool\Platform\Android\UEDeployAndroid.cs 生成出来的。内容如下:
@echo off
setlocal
set GRADLEPATH=%~dp0
set GRADLE_CMD_LINE_ARGS=
:setupArgs
if ""%1""=="""" goto doneStart
set GRADLE_CMD_LINE_ARGS=%GRADLE_CMD_LINE_ARGS% %1
shift
goto setupArgs
:doneStart
subst Z: "%CD%"
pushd Z:
call "%GRADLEPATH%\gradlew.bat" %GRADLE_CMD_LINE_ARGS%
set GRADLEERROR=%ERRORLEVEL%
popd
subst Z: /d
exit /b %GRADLEERROR%
它调用的是gradlew.bat,在日志中会衔接Gradle的日志,如下:
Package
MakeApk函数包括了"生成apk"与"生成.aab、.apks"的功能,MakeApk函数发生在 UE4的package阶段。具体的调用过程示意图:
【书签1】的日志,就是在这个过程中间来的,见 UEDeployAndroid.cs 中。尽管叫做 UEDeployAndroid.cs ,但Package阶段会调用它。
众所周知,Package步骤对应的脚本是:
%EnginePath%\Engine\Build\BatchFiles\RunUAT.bat BuildCookRun -project=%ClientPath%\%ProjectName%.uproject -noP4 -platform=Android -client -clientconfig=%TargetBuildConfig% -cookflavor=%COOK_FLAVOR% %DISTRIBUTION% -skipcook -pak -compressed -stage -NoDebugInfo -package -ignorejunk -nocompile -archive -archivedirectory=%ClientPath%\Saved\Archived -manifests -Verbose
这里参数很多,不是这里的重点。这里说一个技巧,如何让【书签1】所对应的c#脚本,能够输出verbose日志呢?做法是为 RunUAT.bat 脚本传参 -Verbose,如上面命令的末尾处。
而Gradle的日志可以在哪里看呢?
打AAB包时,将会在这个位置 C:\android_build_tools\gradle-4.1-rc-2\daemon\7.2 (gradle所在环境)产生日志文件。
UE4中的Gradle工程
UE4的Gradle工程位于 {我的项目}\Intermediate\Android\gradle ,结构如下:
我在查一些无聊的问题(下面两个问题)时,发现清除UE4的Intermediate、Binaries目录,能够有效果,推测是Gradle工程脏了导致的,因此,删除Gradle工程是一个好的选择,毕竟它在Intermediate中,可以安全地删除,删除后UE4会重新产生Gradle工程。
(问题1:"main.obb.png"出现在.aab的base目录中,而不是在obbassets目录中)
(问题2:.apk、.aab|.apks 都会错误地同时出,而应该只出apk或只出aab|apks)
GooglePAD_APL.xml
在EngineSource下,GooglePAD 是谷歌生成.aab的模块。GooglePAD_APL.xml 是其中一个中间作用文件,它搭建了 UE4 到 Gradle 的桥梁,具体的关系如下:
上图只是简单阐述,下面将会仔细理解GooglePAD_APL.xml。接下来,我描述的方式是:贴一段文件内容,然后接一些解释。注意,并非所有的内容都贴出来了。
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
<init>
<log text="GooglePAD Plugin Init"/>
<setBoolFromProperty result="bEnabled" ini="Engine" section="/Script/GooglePADEditor.GooglePADRuntimeSettings" property="bEnablePlugin" default="false"/>
<setBoolFromProperty result="bOnlyDistribution" ini="Engine" section="/Script/GooglePADEditor.GooglePADRuntimeSettings" property="bOnlyDistribution" default="true"/>
依据 /Script/GooglePADEditor.GooglePADRuntimeSettings 的UE4配置,来设置变量值。
<!-- NDK path -->
<setString result="NDKVersion" value="ndk21.4.7075529"/>
声明一个字符串变量。
<if condition="bEnabled">
<true>
<!-- disable if app bundle disabled -->
<if condition="bEnableBundle">
<false>
<log text="Disabled because not generating AAB bundle"/>
<setBool result="bEnabled" value="false"/>
</false>
</if>
等价于
if bEnabled:
if not bEnableBundle:
bEnabled = False
当if else语句多起来时,这种描述方式会让人抓狂。
<resourceCopies>
<log text="Copying libplaycore.so and proguard files"/>
<!-- note: have to stage this since we linked it -->
<copyFile src="S(AbsPluginDir)/../ThirdParty/play-core-native-sdk/libs/S(Architecture)/$S(NDKVersion)/c++_shared/libplaycore.so"
dst="S(BuildDir)/libs/S(Architecture)/libplaycore.so" />
<isDistribution>
<copyDir src="$S(AbsPluginDir)/../ThirdParty/play-core-native-sdk/proguard"
dst="$S(BuildDir)/gradle/app/proguard" />
</isDistribution>
</resourceCopies>
一段和资源拷贝有关的逻辑。
<settingsGradleAdditions>
<if condition="bEnabled">
<true>
<insert>
<![CDATA[
// generate mainobb assetpack
if (OBB_FILECOUNT.toInteger() > 0) {
File obbfile = new File(OBB_FILE0)
......
]]>
<![CDATA[ 后面的内容,直到 ]]> ,是XML语言中的表示纯粹字符串。这段表示对 settings.gradle 追加字符串,稍后会介绍 settings.gradle。
<gameActivityOverrideAPKOBBPackaging>
<if condition="bEnabled">
<true>
<insert>
<![CDATA[
// for GooglePAD (use upfront for main.obb.png)
assetPackManager = AssetPackManagerFactory.getInstance(this);
表示追加字符串到 gradle\app\src\main\java\com\epicgames\ue4\GameActivity.java 中。GameActivity.java是运行时的逻辑。也许你会好奇,我如何得知它们会拷贝给 GameActivity.java。我的做法是在整个Gradle工程中搜索 <![CDATA[ 后面的字符串。
settings.gradle
经过Package执行后,settings.gradle的内容成为了:
rootProject.name='app'
include ':app'
include ':downloader_library'
include ':GCloud'
include ':GCloudCore'
include ':permission_library'
include ':PluginCrosCurl'
include ':TDM'
include ':TssSDK'
// generate mainobb assetpack
if (OBB_FILECOUNT.toInteger() > 0) {
File obbfile = new File(OBB_FILE0)
if (obbfile.exists()) {
println 'Creating install-time assetpack for GooglePAD: assetpacks/install-time/obbassets'
file("assetpacks/install-time/obbassets/src/main/assets").mkdirs()
def assetBuildGradle = """apply plugin: 'com.android.asset-pack'
assetPack {
packName = "obbassets"
dynamicDelivery {
deliveryType = "install-time"
instantDeliveryType = "install-time"
}
}"""
def assetBuildGradleFile = new File("assetpacks/install-time/obbassets/build.gradle")
assetBuildGradleFile.write(assetBuildGradle)
def destobbfile = new File("assetpacks/install-time/obbassets/src/main/assets/main.obb.png")
if (destobbfile.exists()) {
destobbfile.delete()
}
def srcobbStream = obbfile.newDataInputStream()
def dstobbStream = destobbfile.newDataOutputStream()
dstobbStream << srcobbStream
srcobbStream.close()
dstobbStream.close()
}
}
// add the assetpacks
def assetpacksDir = new File("assetpacks/install-time")
if (assetpacksDir.exists()) assetpacksDir.eachDir {
println ':assetpacks:install-time:' + it.name
include ':assetpacks:install-time:' + it.name
}
assetpacksDir = new File("assetpacks/fast-follow")
if (assetpacksDir.exists()) assetpacksDir.eachDir {
println ':assetpacks:fast-follow:' + it.name
include ':assetpacks:fast-follow:' + it.name
}
assetpacksDir = new File("assetpacks/on-demand")
if (assetpacksDir.exists()) assetpacksDir.eachDir {
println ':assetpacks:on-demand:' + it.name
include ':assetpacks:on-demand:' + it.name
}
在这其中,我们看到特定的文件夹被设置成了开头我们说的"install-time"等拉取方式。
未完。