谷歌新安装包文件形式 .aab 在UE4中的打包原理

摘要

本文学习了aab的基本概念以及UE4中产生aab的构建原理。

从官网了解基本概念

官网:Android Developers

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"等拉取方式。

未完。

相关推荐
北冥没有鱼啊9 天前
UE 材质 条纹循环发光
游戏·ue5·游戏引擎·ue4·材质
北冥没有鱼啊15 天前
UE 滚动提示条材质制作
游戏·ue5·游戏引擎·ue4·虚幻·材质
一点.点15 天前
如何让自己的博客可以在百度、谷歌、360上搜索到(让自己写的CSDN博客可以有更多的人看到)
百度·谷歌·csdn·360·搜狗
北冥没有鱼啊1 个月前
UE 使用事件分发器设计程序
游戏·ue5·ue4·游戏开发·虚幻
GR901 个月前
UE4 踩坑记录
ue4
程序猿熊跃晖1 个月前
解决 Unreal Engine 5.2 中服务器目标构建问题:从源码编译到项目配置优化
ue4
Ⅰㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 个月前
开篇 - Unlua+VsCode的智能提示、调试
vscode·ue4·智能提示·unlua
吴梓穆1 个月前
UE5学习笔记 FPS游戏制作33 换子弹 动画事件
笔记·学习·ue4
吴梓穆1 个月前
UE5学习笔记 FPS游戏制作31 显示计分板
笔记·学习·ue4
吴梓穆1 个月前
UE5学习笔记 FPS游戏制作28 显式玩家子弹数
笔记·学习·ue4