全文皆为转载备用
前言
这是关于资源加载的第一篇内容,主要从 StaticLoadObject 出发,讨论 UE 是如何把序列化的数据给加载到内存中的。了解加载过程前必须先了解 UPackage、uasset 文件格式、FLinkerLoad。了解这三个概念之后会介绍 StaticLoadObject 加载过程所经过的四个步骤。
UPackage、uasset、FLinkerLoad
一个资源在文件中对应 uasset,在内存中对应为 UPackage。
1、UPackage
一个资源在内存中表现为一个 UPackage 的实例,比如一个 SoundCue 资源,SoundCue 内部可能有很多个蓝图节点,就有一些节点的数据,比如 Modulator、Mixer 等等,这些数据是实例本身的数据。同时 SoundCue 也引用外部声音文件 SoundWave。SoundWave 也是一个资源,也是对应的一个 UPackage 实例。这样两个 UPackage 之间就存在依赖关系。
UPackage 就好比一个班级,底下的数据 UObject 就好比学生,对于班级(UPackage)底下的同学(UObject)来说,UPackage 是 UObject 的 Outer。要知道资源自身数据 UObject 的内容,必须先知道 UPackage 才行。
2、uasset 文件格式
UPackage 序列化到本地之后就是 uasset 文件。uasset 是本地的资源文件,文件格式如图
-
File Summary 文件头信息
-
Name Table 包中对象的名字表
-
Import Table 存放被该包中对象引用的其它包中的对象信息 (路径名和类型)
-
Export Table 该包中的对象信息 (路径名和类型)
-
Export Objects 所有 Export Table 中对象的实际数据。
前文提过,两个 UPackage 实例是可以存在依赖关系的,序列化到 uasset 文件的时候,这些依赖关系就存储为 ImportTable。可以把 ImportTable 看做是这个资源所依赖的其他资源的列表,ExportTable 就是这个资源本身的列表。Unity 导出资源的时候是导出 AssetBundle + 依赖表。每个资源所依赖的其他资源都记录在依赖表中 。这里的 uasset 可以看做是 AssetBundle + 依赖表中这个资源的依赖文件记录。其中 AssetBundle 就是对应的 ExportTable 以及 ExportObject 的内容,依赖表中这个资源的依赖文件记录就是对应的 ImportTable。
3、FLinkerLoad
FLinkerLoad 是作为 uasset 和内存 UPackage 的中间桥梁。在加载内容生成 UPackage 的时候,UPackage 会根据名字找到 uasset 文件,由 FLinkerLoad 来负责加载。FLinkerLoad 主要内容如下:
-
FArchive* Loader; //Loader 负责读取具体文件
-
TArray ImportMap; //将 uasset 的 ImportTable 加载到 ImportMap 中,FObjectImport 是需要依赖(导入)的 UObject
-
TArray ExportMap; //FObjectExport 是这个 UPackage 所拥有的 UObject(这些 UObject 都能提供给其他 UPackage 作为 Import)
在了解了基本概念后,接下来进入主要部分,也就是 StaticLoadObject 加载,StaticLoadObject 可以分成四个部分。
加载内容的四个步骤:
-
根据文件名字创建一个空的包(没有任何文件相关的数据)
-
建立一个 LinkerLoad 去加载对应的 uasset 文件 序列化。
-
优先加载 ImportMap
-
加载 ExportMap(本身的数据)
对应图中右边的四个步骤
序列化 uasset 阶段中会序列化还原这个资源所需要的信息,例如 ImportMap、ExportMap,但这两个 Map 中存储的信息仅仅是 Import 和 Export 的信息而已,可以理解为是知道了去加载的途径,但是还没有去加载。随后在 VerifyImportInner 才会实际上地把 Import 内容加载进内存,(LoadAllObject + EndLoad)把自身资源的数据加载到内存。
1、建立一个 UPackage
从 StaticLoadObject 方法逐步看即可,略过
2、序列化 uasset
在 FLinkerLoad 的 Tick 函数中,会把 uasset 的信息给加载出来。
FLinkerLoad::ELinkerStatus FLinkerLoad::Tick( float InTimeLimit, bool bInUseTimeLimit, bool bInUseFullTimeLimit )
-
读取文件 CreateLoader
-
序列化 FileSummary,SerializePackageFileSummary
-
FPackageFileSummary 主要存储 比如 FolderName 基本字段以及 uasset 其余信息在文件中的偏移信息,比如 ExportOffset、ExportCount。
-
序列化 uasset 其他信息(除 FileSummary、ExportObject)比如: SerializeImportMap、SerializeExportMap。
-
生成必要信息,这些信息不需要序列化到 uasset,可以通过其余序列化信息恢复生成 CreateExportHash
-
FinalizeCreation 创建 LinkerLoad 的最后步骤,verify 加载外部依赖 UObject
verify 加载外部依赖的时候就进入了第三阶段,加载 ImportMap 的内容。
3、加载 ImportMap
ImportMap 是一个 FObjectImport 的数组,存储依赖的 UObject,对应的 ExportMap 也是 FObjectExport 的数组。
verify 主要是调用到 FLinkerLoad::VerifyImportInner,这个函数主要分为两种情况,加载的 UObject 是 Asset 实际资产和非 Asset(MemoryOnly),这两种情况还要区别是加载 UObject 还是 UPackage。就是说加载 Asset 的时候可能只是加载这个资产底下的一个 UObject 而已,也可能是加载整个 UPackage。加载非 Asset 的时候也有可能是加载 UObject 或者 UPackage。(UClass 和 UPackage 都是继承自 UObject 的)
一、Import 是一个 MemoryOnly。
1、 Import 是 MemoryOnlyPackage
加载代码里主要用到了这两个函数。
-
LoadPackageInternal(主要是加载 AssetPackage,不加载 MemoryOnlyPackage)
-
CreatePackage(优先在内存中找,否则创建)
当在 LoadPackageInternal 加载不到的时候,会继续在 CreatePackage 中查找,这个函数优先在内存中查找,而 MemoryOnly 正是提前存在于内存中的。找到 UPackage 对应的包返回即可,Import.XObject = FindPackage。
(就目前的理解来看,可能有错,加载的 MemoryOnly 一般是 UClass 或者包含 UClass 的 UPackage,像 SoundWave 也是一个 UClass,这些 UClass 包含在 /Script/UnrealEngine 的 UPackage 中,这些 UClass 类以及 UPackage 都是在引擎启动的时候就已经加载到内存中的)
2、 Import 是一个 UObject,那必定是 Class 对象(类似 Java Class 对象),找到 TopLevelPackage,也就是这个 Class 对象所在的 UPackage,在 TopLevelPackage 找到这个 Class 对象并赋值给 Import 的 XObject。
比如加载一个 SoundWave 音频资源文件,在 ExportMap 中只有一个 UObject 就是音频数据。但这个音频数据需要 SoundWave 的类对象,所以在 ImportMap 中有一个 UObject 储存类对象,这个 SoundWave 类对象是属于 /Script/UnrealEngine UPackage 里的,/Script/UnrealEngine 就相当于保存类对象定义的地方,所以 ImportMap 中总共有两个 UObject,一个是 Class 对象,一个是 Package。加载的时候在 UPackage 中找到类对象
Sound.uasset
{
ImportMap[0] SoundWave Class对象
ImportMap[1] /Script/UnrealEngine UPackage包
ExportMap[0] 音频数据
}
-
ClassName:Class 表明这是一个 Class 对象、Package 表明是一个 UPackage
-
ObjectName:SoundWave 表明这个 SoundWave 类对象
-
OuterIndex:-2 表明这个类对象是放在索引为 1 的包内
索引换算方法:
-
PackageIndex > 0,表示在 ExportMap 中的索引 实际索引 Index = PackageIndex -1;
-
PackageIndex < 0,表示在 ImportMap 中的索引 实际索引 Index = - PackageIndex -1;
-
PackageIndex = 0,表示当前 UPackage 对象;
看 ExportMap[0] 的 classIndex 为-1,也就是说这个数据的类的数据保存在 ImportMap[0] 的位置。ImportMap[0] 的 ClassName 为 Class 表明这是一个类,如果为 Package 则表明这个 Import 是一个 UPackage。ImportMap[0] 是 U Package 底下的一个 UObject,通过 ImportMap[0].OuterIndex 可以知道这个 UObject 的 Outer 的位置,OuterIndex 是-2,也就是说 Outer 是 ImportMap[1],ImportMap[1] 是一个 UPackage。
二、Import 是一个 Asset 资源。
-
Import 对应的是一个 UPackage,那么会调用 LoadPackageInternal,在这个函数里又会根据名字去找到对应的具体文件,然后创建一个新的 UPackage。这个步骤有点类似于递归。(先假设已经完成加载 Import 的 AssetPackage,因为 LoadPackage 过程是一样), 接着让 Import.SourceLinker = NewUPackage.LinkerLoad 让 Import 持有 NewUPackage 的 FLinkerLoad。
-
Import 表示一个 UObject,UObject 必定属于另一个 UPackage(必定有 Outer),先去加载对应的 Outer。加载完 Outer 之后,才加载 UObject。一个包对应一个 FLinkerLoad,让包 1 中 Import.SourceLinker = 包 2 的 Outer.SourceLinker。同时可以知道,NewUPackage(当作包 2)的 ExportMap 肯定有一个 UObject 是对应着包 1 的 ImportMap。因为两者存在引用关系。为了加载 UObject,通过 HashName 找到对应的资源的索引,包 1 的 Import.SourceIndex = 包 2 的 ExportMap 中对应资源的索引
附:如何寻找正确的 SourceIndex
一个资源引用另一个资源,那么必然这个资源的 ObjectName(资源名字)、ClassName(类名字,比如 SoundWave)、ClassPackage(类所在的 Package)。这三点必然是相等的。
在读取文件序列化 uasset 的过程中,就有一个 HashName 的过程,即一个 Export 的 ObjectName、ClassName、ClassPackage 通过 HashName 得到一个 0-255 之间的一个索引,记录在一个 0-255 的数组中的对应位置记录上这个 Export 在 ExportMap 中的索引。
Import 中也利用同样的三个值 ObjectName、ClassName、ClassPackage,计算出一个同样的索引,在 0-255 中对应索引的位置上找到这个导出在 ExportMap 中的位置
总结:
以上就对四种情况分别做了介绍。
对于 MemoryOnly 来说,是在 Import.XObject 中直接记录 UPackage 指针 或者 UClass 对象指针,Import 里有一个 SourceLinker 表示依赖的资源所需要的 FArchive,对于 MemoryOnly 来说是不需要依赖 Asset 文件的,所以是这个值是 NULL。
对于 Asset 来说,是在 Import.SourceLinker 中记录资源的 Loader,在 Import.SourceIndex 中记录资源在 ExportMap 中的位置。这样就可以找到 Export.Object
其实方法上来讲是很相似的,加载 UObject(Import 加载 UClass Export 加载 UPackage 下的 UObject)的时候都会先去要求 Outer 已经被加载,再从 Outer 中获取 UObject。
4、加载 ExportMap 自身数据
加载 ExportMap 自身数据的部分可以分成两个主要部分,一是根据 CDO 类默认对象生成一个模板,二修改差异性的数据。
一、塑造模板的过程如下:
-
获得 Export.Object 的 Archetype
-
根据 Class 对象、Outer、Name、Template 构建模板对象
-
设置 Linker
获得 Export.Object 的 Archetype
-
是 UPackage,则取得 CDO (Class Default Object),相当于类默认构造函数所构建的一个对象,一个类会在内存中放置一个 CDO。
-
不是 UPackage,则应该是 UPackage 下的一个 UObject,必须先加载到 Outer,从 Outer 中加载原型。加载 Outer 的时候会一直追溯到 UPackage。最后取得的 UObject 就相当于是 CDO 中对应的部分。
如果是 UPackage 则返回一个 CDO。
如果有 Outer 也就是说不是 UPackage 则从 outer 中找到原型 再从原型中找到对应的 component,因为 outer->getArchetype 最终一定有一个 Top-Level Package,这样必定返回一个类的默认对象。
根据 Class Outer Name Template 构建模板对象
在内存中重新构建出来一个 UObject
LoadClass 这个 Object 对应的类
ThisParent 这个 Object 对应的 Outer
Template 这个 Object 对应的模板
设置 Linker
设置 Export.Object 对应的 Linker,并添加到 ObjLoaded 中,在 EndLoad 中重新拿到 ObjLoaded(需要加载的所有 Export)随后真正的序列化这个物体。
二、EndLoad 调用 PreLoad 方法实现序列化
FAA2 即 FArchive 下的 Loader 对象,与 uasset 文件直接关联。
Export 包含了这个 Object 导出所存储的必要信息,在文件中的起始偏移值,文件大小。将内容加载至内存随后序列化
https://yuedu.163.com/book_reader/abb2cf428b244522b17aa2ec9eeea88c_4/dfd26a58c22643bf95b8a473352d5b4c_4
总结
至此四个步骤就已经结束了,第一部分创建了一个 UPackage。第二部分将读取的 uasset 部分数据加载到 FLinkerLoad 中,此时 FLinkerLoad 就已经知道了这个资源依赖哪些资源,自身又有哪些资源。第三部分加载 ImportMap。第四部分加载 ExportMap,其中加载自身数据的这个过程又分为两步,先是依据类模板对象生成一个模板,随后才序列化差异的数据。类模板对象 UClass 总是在 ImportMap 中,这可能也是为什么要先加载 ImportMap 的原因。
补充
-
当资源 1 依赖于资源 2 的时候,也就是加载包 1 的过程中必须加载包 2,例如一个 SoundCue 依赖于一个 SoundWave,加载资源 2 时是根据名字去 Pak 中搜索对应的 uasset。找到对应的 uasset 之后,包 1ImportMap 与包 2ExportMap 中对应的 UObject 建立关联需要保证三个值不变 ObjectName ClassName ClassPackage。
-
场景 1:要更改一个资源,比如只是简单的音乐替换的话,那么 ClassName ClassPackage 肯定是不变的,只需要保证 ObjectName 不变即可。打包后,将修改后的音频资源的 Pak 直接替换,那么游戏的音乐就修改成功。如果需要改变 ClassName ClassPackage 的话,那么必须同时修改两个包才可以!
-
场景 2:如果给 SoundCue 增加了很多功能,比如蓝图中的 Mixer,Modulator。这样会增加 SoundCue 的依赖的 Class 对象,以及自身 Export 中的数据,与依赖的文件资源时没有关系的,这种情形下直接替换 Pak 即可
参考文章: