谷歌新安装包文件形式 .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"等拉取方式。

未完。

相关推荐
小江村儿的文杰1 天前
UE4 iOS Package的过程与XCode工程中没有游戏Content的原因
macos·ios·ue4·xcode
小江村儿的文杰3 天前
XCode Build时遇到 .entitlements could not be opened 的问题
ide·macos·ue4·xcode
程序员小范3 天前
孙玲:从流水线工人到谷歌程序员
人工智能·程序员·谷歌·远程工作
Deveuper10 天前
UE5 UE4 播放视频没有声音解决
ue5·ue4·音视频
小江村儿的文杰21 天前
UE4安卓Gradle工程中的libUE4.so的生成原理
ue4
北冥没有鱼啊1 个月前
UE5 射线折射
游戏·ue5·游戏引擎·ue4
Growthofnotes1 个月前
UE4_Niagara基础实例—9、使用条带渲染器来制作闪电
ue4
Growthofnotes1 个月前
UE4_Niagara基础实例—10、位置事件
ue4
directx3d_beginner1 个月前
ue4 .usf抄写记录
ue4
 M͏⁠͏r.D1 个月前
UE4 材质学习笔记13(格斯特纳波)
学习·ue4·材质