编辑器里拖一个引用,存下来的是一串字符串(SoftObjectPath);运行时拿到的是 UObject*。中间隔着整个资产加载机制:文件怎么读进内存、对象怎么按依赖顺序建出来、引用怎么从索引翻译成指针。基于 UE5.3 源码(UObjectGlobals.cpp、LinkerLoad.cpp)走一遍链路。
Package:加载的基本单元
UE 的资产文件(.uasset)对应一个 UPackage,加载以 package 为原子单位——要么整个加载,要么不加载,不存在"加载半个 package"。LoadPackage(UObjectGlobals.cpp)的同步路径:
FindPackage:内存里已有同名 package 直接返回(加载缓存的第一道);- 找不到则
CreatePackage建一个空的UPackage壳; CreateLinker创建FLinkerLoad,把文件头读进来——此时文件还关着,只有 Summary 段(包名、依赖列表、import/export 表的大小信息)在内存;SerializePackage:linker 逐对象反序列化,CreateImport/CreateExport把表里每一项实例化成UObject;Linker->Preload收尾,UObject::EndLoad。
为什么要先建壳再填?因为依赖环允许存在——A 引用 B、B 引用 A 是合法的资产结构。空壳先注册进全局包表,CreateImport 递归加载 B 时如果发现 B 引用了 A,FindPackage 能立刻返回 A 的壳,避免无限递归。
FPackageIndex:正数 Export,负数 Import
package 文件内部用 FPackageIndex 互相引用——一个 32 位有符号整数,编码规则(LinkerLoad.cpp):
index > 0 → Export 表的第 index 项(本包导出的对象)
index < 0 → Import 表的第 (-index-1) 项(本包依赖的外部对象)
index = 0 → null
Import 表项(FObjectImport)记录三样东西:对象名、UClass 名、所在包名——全是名字,没有指针。Export 表项(FObjectExport)记录:对象名、class 的 FPackageIndex、outer 的 FPackageIndex、序列化数据在文件里的偏移和长度。
序列化时引用怎么写
对象内部引用另一个对象时,FObjectProperty 的序列化写的是 FPackageIndex 而非指针——FLinkerSave 在建表阶段反向遍历:被引用的对象在本包 → 正数 index;在外部包 → 递归确保外部包在 import 表,写负数 index。这就是"存盘时引用是无指针的"的原因,也是资产文件能被改名工具重定向的基础(名字字符串在 import 表里,改完重存即可)。
加载端反过来:FLinkerLoad::CreateExport 处理某个 export 时,对象属性反序列化读到一个 index,调 FLinkerLoad::Resolve 查表:
- 正数 → 本包 ExportMap,已有实例直接返回,没有则先
CreateExport(递归); - 负数 → ImportMap,拿到包名 + 对象名,走
LoadPackage递归加载那个外部包,再在外部包的 export 表里找同名对象。
这就是为什么加载一个 package 会级联拉一串依赖:加载 SM_Crate.uasset 发现 import 了 M_Metal,M_Metal 又 import 了 T_Albedo……整个依赖闭包都被实例化。
同步与异步的分界
上面是同步路径(LoadPackage)。异步路径(LoadPackageAsync)把第 3、4 步拆细:linker 建表照样立即做(快),CreateExport 的密集反序列化丢进 FAsyncLoadingThread 的任务队列,按依赖拓扑分批跑,回调通过 FStreamableManager 或 AsyncLoading 的完成代理通知。
卡顿的根源通常在第 4 步的"级联同步化":异步加载到一半,某个对象的反序列化里碰到 FObjectProperty 引用了一个还没加载的包,旧代码路径会 FlushAsyncLoading 强制等——异步瞬间变同步,主线程卡住。5.x 里 EAsyncLoadingResult 和 FSoftObjectPath 的 RequestAsyncLoad 配合把"提前声明依赖"做进了资产本身(UPROPERTY(meta=(AsyncLoadPriority)) 之类),但手写的 StaticLoadObject 仍是卡顿高发区,审查代码时见一个改一个。
实操命令
obj list/obj list class=Package:看当前加载了哪些 package,排查"为什么这个包被拉进来了"。log LogStreaming Verbose:看异步加载的级联链。- 查单个对象的加载来源:
GetName()反查 outer chain 到 UPackage,UPackage::GetLoadedBy(开发版本)看是谁触发的加载。
这条链路的精髓可以浓缩成一句话:文件里存名字和索引,内存里才是指针,linker 是两者之间的翻译器。后面几篇(UAsset 格式、DDC、Cook)全都是在这个模型上往外长:格式篇讲文件里那些表的具体二进制布局,Cook 篇讲这些文件怎么被批量生成。