Unreal是如何驾驭内存的 第14章 资产系统——UPackage、.uasset与Cook流程
第14章 资产系统——UPackage、.uasset与Cook流程本章目标深入理解UE资产系统的内存模型——UPackage作为资产的内存容器、.uasset文件的二进制结构、Import/Export表的引用管理、Cook流程中的内存转换以及资产注册表的内存开销。14.1 资产系统全景运行时视角编辑器视角Cook打包Content Browser/Game/Maps//Game/BP//Game/Tex/.uasset (源格式) / .umap已加载的 UPackageULevel · UTexture · UStaticMesh.uasset (Cook后) / .pak · .ucas核心映射一个 .uasset文件 ↔ 一个 UPackage ↔ 一组相关UObject14.2 UPackage——资产的内存容器14.2.1 UPackage的本质classUPackage:publicUObject{// UPackage就是一个UObject// 它是所有包内UObject的 OuterFName FileName;// 文件路径FGuid Guid;// 包的唯一标识uint32 PackageFlags;// 标志位FLinkerLoad*LinkerLoad;// 关联的加载器// 包内对象关系// UTexture2D-GetOuter() UPackage// UStaticMesh-GetOuter() UPackage};14.2.2 Outer链与内存组织UPackage /Game/Props/Chair该包内的对象层级UStaticMesh“Chair_Mesh”UBodySetup (SubObject)UMaterialInterface“Chair_Mat”UMaterialExpressionTextureSampleUTexture2D“Chair_Diffuse”FBulkData (纹理像素)每个对象的Outer链Chair_Mesh-GetOuter() UPackage(/Game/Props/Chair) UPackage-GetOuter() nullptr (顶层)Outer链的内存意义GC包被标记不可达 → 包内所有对象一起回收序列化包是序列化的单位资产管理加载/卸载以包为粒度14.3 .uasset文件格式14.3.1 文件结构概览.uasset文件的二进制布局区域内容FPackageFileSummary文件头Magic: 0x9E2A83C1、FileVersion、TotalHeaderSize、PackageName、PackageFlags、NameCount NameOffset、ImportCount ImportOffset、ExportCount ExportOffset 等NameMap名称表“Chair_Mesh”、“StaticMesh”、“Chair_Mat” 等ImportMap导入表Import[0]: /Script/Engine.StaticMesh、Import[1]: /Game/Textures/Wood.Wood 等ExportMap导出表Export[0]: Chair_Mesh (offset, size, class)、Export[1]: Chair_Mat (offset, size, class) 等Export对象数据区Chair_Mesh的序列化数据、Chair_Mat的序列化数据 等BulkData纹理/音频等大数据可选或在.ubulk中14.3.2 NameMap——名称表// 文件中的名称表// 包内所有FName都通过索引引用此表// 避免重复存储相同的名称字符串// 内存影响// 加载时NameMap中的名称注册到全局FNamePool// 一旦注册字符串在FNamePool中永不释放// 累积效应加载过的包越多FNamePool越大// 典型大小每个包 100-1000 个名称// 每个名称在池中占 ~30-60 字节// 1000个名称 ≈ 30-60 KB 池内存14.3.3 ImportMap——导入表structFObjectImport{FName ClassPackage;// 类所在的包名FName ClassName;// 类名int32 OuterIndex;// Outer的索引包内FName ObjectName;// 对象名称// ...};// Import表记录了本包引用的外部对象// 例如一个材质引用了另一个包中的纹理// Import[0]: /Game/Textures/WoodGrain.WoodGrain (UTexture2D)//// 加载时需要解析这些引用 → 可能触发依赖包的加载// 内存影响Import越多 → 依赖包越多 → 内存占用可能雪崩14.3.4 ExportMap——导出表structFObjectExport{int32 ClassIndex;// 对象的类Import索引int32 SuperIndex;// 父类int32 OuterIndex;// OuterFName ObjectName;// 对象名uint32 ObjectFlags;// 标志位int64 SerialSize;// 序列化数据大小int64 SerialOffset;// 数据在文件中的偏移// ...};// Export表描述了本包导出提供的所有对象// SerialSize告诉加载器需要分配多少内存// SerialOffset用于seek到正确位置读取数据14.4 引用与依赖14.4.1 硬引用链——“引用雪崩”BP_CharacterSkeletalMesh/Game/Mesh/HeroMaterial/Game/Mat/SkinTexture SkinDiffuseTexture SkinNormalTexture SkinRoughnessAnimBlueprint/Game/Anim/HeroABPAnimSequence × 50CompressedAnimData × 50SoundCue/Game/Audio/FootStepSoundWave × 4加载 BP_Character 时必须同时加载所有硬引用资产可能达到50-200MB内存这就是引用雪崩问题。14.4.2 引用审计// 使用引用查看器检查依赖// 编辑器中右键资产 → Reference Viewer// 控制台命令// obj refs nameBP_Character // 查看引用关系// Size Map工具// 编辑器中右键 → Size Map// 可视化显示资产及其依赖的内存占用// Asset Audit工具// 分析哪些资产被过度引用14.5 Cook流程与内存14.5.1 Cook的目的编辑器资产 (.uasset源格式)Cook处理1. 转换为目标平台格式纹理: PNG/TGA → BC7/ASTC/ETC2Mesh: 可能Nanite处理着色器: HLSL → SPIRV/Metal/DXIL2. 剥离编辑器专有数据缩略图、编辑器元数据等3. 优化序列化格式Unversioned Property序列化零标签开销4. 生成目标平台文件.uasset/.uexp/.ubulkCooked资产 (运行时格式)14.5.2 Cook后的序列化优化Cook前Tagged序列化每个属性需要存储名称、类型、大小和值约24字节/属性。例如一个Health属性Health | Float | 4 | 100.0。Cook后Unversioned序列化只需存储值本身仅4字节/属性100.0。原理Cook时已知目标类的精确属性布局不需要标签按固定偏移量直接读写序列化体积减少50-70%加载速度提升更少的IO和解析。代价是丧失版本兼容性仅限同一版本的Cook。14.5.3 .uasset / .uexp / .ubulk分离Cook后一个资产可能拆分为三个文件 .uasset — 包头 名称/导入/导出表 较小 (~10-100KB)总是加载 .uexp — 导出对象的序列化数据 中等大小对象加载时读取 .ubulk — BulkData (纹理Mip、音频、大数据) 可能很大可延迟/流式加载 分离的内存优势 - 可以只加载.uasset获取元信息不加载实际数据 - .ubulk可以独立流式加载特定Mip级别 - 减少不必要的IO和内存占用14.6 DDC——派生数据缓存14.6.1 DDC的角色转换命中 → 直接读取未命中 → 重新转换源资产PNG纹理 1024×1024DDC KeyHash(源数据转换参数平台引擎版本)目标格式BC7纹理 DDS格式转换处理14.6.2 DDC的内存影响DDC在编辑器中的内存使用DDC组件内存影响本地DDC磁盘缓存无运行时内存内存DDC缓存 (Boot)可配置大小共享DDC (网络)网络IO缓冲DDC键哈希表~10-50MBDDC仅在编辑器/Cook时使用运行时Shipping build不涉及DDC。14.7 资产注册表——FAssetRegistryState14.7.1 资产注册表的作用资产注册表是所有资产元信息的内存索引无需加载资产即可查询。classFAssetRegistryState{// 核心数据结构TMapFName,FAssetData*CachedAssetsByObjectPath;TMapFName,TArrayFAssetData*CachedAssetsByPackageName;TMapFName,TArrayFAssetData*CachedAssetsByClass;TMapFName,TArrayFAssetData*CachedAssetsByTag;// 依赖关系TMapFAssetIdentifier,FDependsNode*CachedDependsNodes;};14.7.2 FAssetData——每资产内存开销structFAssetData{FName PackageName;// 8字节FName PackagePath;// 8字节FName AssetName;// 8字节FName AssetClass;// 8字节// 注意UE 5.1 已将 AssetClass (FName) 替换为// FTopLevelAssetPath AssetClassPath以支持完整的类路径标识。FAssetDataTagMap TagsAndValues;// 变长MapTArrayint32ChunkIDs;uint32 PackageFlags;// ...};// 每个FAssetData ≈ 100-500 字节取决于Tag数量// 大型项目// 50,000 个资产 × ~300 字节 ≈ 15 MB// 加上依赖图FDependsNode≈ 额外 10-20 MB// 资产注册表总计 ≈ 25-40 MB14.7.3 运行时的资产注册表编辑器中 - 扫描Content目录构建完整注册表 - 监听文件变化动态更新 - 内存较大完整元数据 运行时 - 从Cook生成的 AssetRegistry.bin 加载 - 只包含运行时需要的数据裁剪后 - 可通过 bSerializeAssetRegistry 控制 - 通常 5-15 MB14.8 资产与内存的关键规则14.8.1 一包一主资产推荐做法 /Game/Textures/Wood.uasset → 一个UTexture2D 不推荐 /Game/AllTextures.uasset → 100个UTexture2D 理由加载一个纹理会加载整个包 → 99个不需要的纹理也进内存14.8.2 打破引用链// 问题BP_Character直接引用200MB的资产UPROPERTY()USkeletalMesh*Mesh;// 硬引用 → 蓝图加载时立即加载Mesh// 解决使用软引用UPROPERTY()TSoftObjectPtrUSkeletalMeshMesh;// 软引用 → 不自动加载// 需要时手动加载USkeletalMesh*LoadedMeshMesh.LoadSynchronous();// 或异步加载14.9 小结UPackage是资产的内存容器——一个.uasset对应一个UPackage包内所有UObject的Outer指向该包。.uasset文件包含NameMap/ImportMap/ExportMap三大表加载器通过这些表在内存中重建对象和引用关系。Import表决定了包的依赖——Import越多加载时需要解析的依赖越多。硬引用链的雪崩效应是内存问题的常见根源。Cook流程将编辑器资产转换为运行时格式Unversioned序列化可减少50-70%的序列化体积.uexp/.ubulk分离支持按需加载。资产注册表是所有资产的内存索引大型项目中占25-40MB运行时裁剪后通常5-15MB。下一章第15章 资产加载——同步、异步加载与StreamableManager