UE4.27.2 PAK 打包流程的调试分析
2025-05-20 22:58:09

UAT 调用

从 UI 点击,触发回调,最终调用到 FLauncherUATTask::PerformTask,它内部就是唤起了一个 UAT Command

1
2
3
4
5
6
7
8
9
10
11
12
13
FString UATCommandLine;
FString ProjectPath = *ChainState.Profile->GetProjectPath();
ProjectPath = FPaths::ConvertRelativePathToFull(ProjectPath);
UATCommandLine = FString::Printf(TEXT("-ScriptsForProject=\"%s\" BuildCookRun -project=\"%s\" -noP4 -clientconfig=%s -serverconfig=%s"),
*ProjectPath,
*ProjectPath,
LexToString(ChainState.Profile->GetBuildConfiguration()),
LexToString(ChainState.Profile->GetBuildConfiguration()));

...

// launch UAT and monitor its progress
ProcessHandle = FPlatformProcess::CreateProc(*(ExecutablePath / Executable), *UATCommandLine, false, true, true, NULL, 0, *ExecutablePath, WritePipe);

以安卓为例

打包时,调用的 UAT 进程的参数是

1
L"/c ""<my position>/UnrealEngine/Engine/Build/BatchFiles/RunUAT.bat" -ScriptsForProject="<my position>/<my project name>/<my project name>.uproject" BuildCookRun -nocompileeditor -nop4 -project="<my position>/<my project name>/<my project name>.uproject" -cook -stage -archive -archivedirectory="<my position>/<my project name>AndroidPackage" -package -ue4exe="<my position>\UnrealEngine\Engine\Binaries\Win64\UE4Editor-Win64-DebugGame-Cmd.exe"  -compressed -ddc=DerivedDataBackendGraph -pak -prereqs -nodebuginfo -targetplatform=Android -cookflavor=ETC2 -build -target=<my project name> -clientconfig=Development -utf8output""

不太理解为什么写成 -utf8output"",那个后面的 "" 是啥意思

分析一下我要是想自己调用的话,该怎么做

1
"<my position>/UnrealEngine/Engine/Build/BatchFiles/RunUAT.bat" -ScriptsForProject="<my position>/<my project name>/<my project name>.uproject" BuildCookRun -nocompileeditor -nop4 -project="<my position>/<my project name>/<my project name>.uproject" -cook -stage -archive -archivedirectory="<my position>/<my project name>AndroidPackage" -package -ue4exe="<my position>\UnrealEngine\Engine\Binaries\Win64\UE4Editor-Win64-DebugGame-Cmd.exe"  -compressed -ddc=DerivedDataBackendGraph -pak -prereqs -nodebuginfo -targetplatform=Android -cookflavor=ETC2 -build -target=<my project name> -clientconfig=Development -utf8output

这样是 work 的

具体到 RunUAT.bat 里面看,他只是转发参数给 AutomationToolLauncher.exe

于是在 IDE 中传入调试 UE 时获得的命令行参数。它最终转发参数给 AutomationTool.exe

UAT 打包生成 apk

AutomationTool.exe 最终会分析参数,分析出来 BuildCookRun

它会根据字符串查询类型,最终创建出来 public class BuildCookRun : BuildCommand 这个类

他最终会执行 protected void DoBuildCookRun(ProjectParams Params)

该函数调用构建、烘培、打包程序

自然地,打包函数是虚函数,随平台而不同

具体到安卓平台是 AndroidPlatform 实现了 Package 函数

实际调试发现,Package 函数并不是简单的包揽所有,它只是负责 obb 文件的创建与更新,还有 apk 文件的创建的准备工作

之后还有别的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void DoBuildCookRun(ProjectParams Params)
{
...

Project.CopyBuildToStagingDirectory(Params);
Project.Package(Params, WorkingCL);
Project.Archive(Params);
Project.Deploy(Params);
PrintRunTime();
Project.Run(Params);
Project.GetFile(Params);

...
}

这里 Deploy 似乎是没有执行。它是把 apk 部署到当前平台,但是我这里是 windows 所以他没有部署?

重新跑一遍,是在 Project.Archive(Params); 执行之后,apk 和 obb 文件就都生成了

点进来看,是在 public static void Archive(ProjectParams Params)ApplyArchiveManifest(Params, SC); 这里创建出文件

这仅仅是从文件浏览器的角度来看,我感觉可能之前对于文件的准备工作还是会很多

不过目前还是从这个明显的创建出文件的这个函数作为入口来看

进到 ApplyArchiveManifest 发现他只是拷贝文件而已。拷贝文件还可以只拷贝增量,神奇。

所以是在这之间文件已经在 \Binaries\Android 打包好了,现在只是拷贝到输出目录。

于是重新调试,发现 \Binaries\Android 里面的 apk, obb 确实是由 Project.Package(Params, WorkingCL); 创建

于是还是回到 AndroidPlatform.Package,看到

1
2
string StageDirectoryPath = Path.Combine(SC.StageDirectory.FullName, SC.ShortProjectName);
List<FileReference> FilesForObb = ObbFileFilter.ApplyToDirectory(new DirectoryReference(StageDirectoryPath), true);

于是发现这里似乎是已经有文件可以处理了,这个文件看上去还是 pak 包

于是看到 FilesForObb 的成员,看到 <my ue position>\<my project name>\Saved\StagedBuilds\Android_ETC2\<my project name>\Content\Paks\<my project name>-Android_ETC2.pak

于是发现,在 UAT 打包之前,我的 Saved\StagedBuilds\Android_ETC2 文件夹里面已经有 pak 文件了?

那这个暂且搁置,回来看看 UAT 的打包是否都是复制文件

一路调试到 Deploy.PrepForUATPackageOrDeploy,在这里执行之后生成了 apk

重定向到 UEDeployAndroid.PrepForUATPackageOrDeploy,这里面的大头就是 MakeApk

最终调试到

1
RunCommandLineProgramWithExceptionAndFiltering(UE4BuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Making .apk with Gradle...");

是他调用了 Gradle 去生成 apk

之后就是调用 CopyFile(LocalObbName, ObbName); 去拷贝 obb

UnrealPak 调用流程

再回来单步调试,BuildCookRun.DoBuildCookRun 中的 Project.CopyBuildToStagingDirectory(Params); 负责生成 Saved\StagedBuilds\Android_ETC2 中所有内容

其中,Project.ApplyStagingManifest 生成了内容

其中是各种 Manifest 的类型,假设先不管每个条件是什么,根据调试,进入 Project.CreatePakUsingStagingManifest

其中根据 manifest 有一些 rules 的处理,处理完之后调用 Project.CreatePaks

前面部分是压缩设置,优先级,然后就是遍历 PakParamsList 处理每个要打包的 pak

PakParamsList 中的一个 CreatePakParams 成员对应一个 pak

CreatePakParamsUnrealPakResponseFile 成员列出了需要打包的文件列表

遍历 PakParamsList 时,开头是计算文件路径,然后是判断是否可以复用已经生成的 pak,判断是否生成增量补丁

如果不能复用,那么开始生成 pak

为了生成 pak,又要调用别的程序,所以要准备一下传递的参数

最终调用为记录 command

1
2
3
4
5
6
7
8
9
10
11
12
Commands.Add(GetUnrealPakArguments(
Params.RawProjectPath,
UnrealPakResponseFile,
OutputLocation,
PrimaryOrderFiles,
SC.StageTargetPlatform.GetPlatformPakCommandLine(Params, SC) + AdditionalArgs + BulkOption + CompressionFormats + " " + Params.AdditionalPakOptions,
PakParams.bCompressed,
Params.SkipEncryption ? null : CryptoSettings,
Params.SkipEncryption ? null : CryptoKeysCacheFilename,
PatchSourceContentPath,
Params.SkipEncryption ? "" : PakParams.EncryptionKeyGuid,
SecondaryOrderFiles));

进入 Project.GetUnrealPakArguments 可以看到 Project.WritePakResponseFile 也是写 txt 文件,把要打包什么写入到 txt 文件中,应该是要传递给 UnrealPak

每个 pak 都记录一个 command,收集起来成为一个列表,然后并行调用

1
2
3
4
5
// Actually execute UnrealPak
if (Commands.Count > 0)
{
RunUnrealPakInParallel( Commands, LogNames, AdditionalCompressionOptionsOnCommandLine);
}

最终调用 UnrealPak 的参数就是

1
<my ue position>\<my project name>\<my project name>.uproject <my ue position>\<my project name>\Saved\StagedBuilds\Android_ETC2\<my project name>\Content\Paks\<my project name>-Android_ETC2.pak -create=<my ue location>\UnrealEngine\Engine\Programs\AutomationTool\Saved\Logs\PakList_<my project name>-Android_ETC2.txt -cryptokeys=<my ue position>\<my project name>\Saved\Cooked\Android_ETC2\<my project name>\Metadata\Crypto.json -secondaryOrder=<my ue position>\<my project name>\Build\Android_ETC2\FileOpenOrder\CookerOpenOrder.log -platform=Android -compressionformats=Oodle -compressmethod=Kraken -compresslevel=3  -multiprocess -abslog=<my ue location>\UnrealEngine\Engine\Programs\AutomationTool\Saved\Logs\UnrealPak-<my project name>-Android_ETC2-2025.05.09-19.04.31.txt -compressionblocksize=256KB

之后就是 UnrealPak 内部的逻辑了。直接把这段命令行传给 UnrealPak 项目就可以调试了

UnrealPak 内部流程

UnrealPak 是 cpp 项目,是对引擎内 ExecuteUnrealPak 函数的包装

前面的部分是一些功能,比如生成补丁。除了这些功能,打包的逻辑最终是

先收集打包文件列表 CollectFilesToAdd(FilesToAdd, Entries, OrderMap, CmdLineParameters);

根据文件列表创建 PAK bool bResult = CreatePakFile(*PakFilename, FilesToAdd, CmdLineParameters, KeyChain);

CreatePakFile 里面涉及到挂载点、压缩机制、加密机制、对齐和填充、异步和多线程、索引结构,这个打包的细节还是很多的