这是关于资源加载的内容
路径怎么写:
- 类名’/路径/包名.对象名:子对象名’(类名其实会被忽略掉,所以不写也行)
- 类:/路径/包名.类名_C (包名往往和类名一致)
- 对象:/路径/包名.对象名 (包名往往和对象名一致)
- 路径:/Root/Path1/SubPath2
- 路径Root:/Engine, /Game, /XXXModule
一、概要
- Unreal 主要基于
FStreamableManager、FAsyncLoadingThread(按需也可为 FAsyncLoadingThread2)、FLinkerLoad等进行资源的异步加载。 - 资源加载主要基于Package来进行,一个Package中包含多种类型的数据信息,Package的加载完成前提是这些资源全部加载完毕。加载完毕还涉及到其中各类型资源的类型构造、属性等的反序列化设置以及依赖资源的加载。全部完成后Package才算加载完毕。
二、文件,加载类,内存中资源
UPackage
一个资源在内存中表现为一个 UPackage的实例,比如一个 SoundCue 资源,SoundCue 内部可能有很多个蓝图节点,就有一些节点的数据,比如 Modulator、Mixer 等等,这些数据是实例本身的数据。同时 SoundCue 也引用外部声音文件 SoundWave。SoundWave 也是一个资源,也是对应的一个 UPackage实例。这样两个 UPackage之间就存在依赖关系。
UPackage是 UObject 的 Outer。要知道资源自身数据 UObject 的内容,必须先知道 UPackage才行。
uasset 文件格式
UPackage 序列化到本地之后就是 uasset 文件。uasset 是本地的资源文件,文件格式如图

- File Summary 文件头信息
- Name Table 包中对象的名字表
- Import Table 存放被该包中对象引用的其它包中的对象信息 (路径名和类型)
- Export Table 该包中的对象信息 (路径名和类型)
- Export Objects 所有 Export Table 中对象的实际数据。
前文提过,两个 UPackage 实例是可以存在依赖关系的,序列化到 uasset 文件的时候,这些依赖关系就存储为 ImportTable。可以把 ImportTable 看做是这个资源所依赖的其他资源的列表,ExportTable 就是这个资源本身的列表。
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)
class FLinkerTables
{
public:
/** The list of FObjectImports found in the package */
TArray<FObjectImport> ImportMap;
/** The list of FObjectExports found in the package */
TArray<FObjectExport> ExportMap;
}
class FLinkerLoad
#if !WITH_EDITOR
final
#endif
: public FLinker, public FArchiveUObject
{
private:
/** The archive that actually reads the raw data from disk.*/
FArchive* Loader;
}
三、StaticLoadObject加载
加载内容的几个步骤:
- 根据文件名字创建一个空的包(没有任何文件相关的数据)
- 建立一个 LinkerLoad 去加载对应的 uasset 文件 序列化。
- 优先加载 ImportMap
- 加载 ExportMap(本身的数据)
- EndLoad 调用 PreLoad 方法实现序列化
注:这是一般内容浏览器中资源加载流程,里面分支很多,一些资源和此流程并不一样并且其中还有异步加载流程
1、建立一个 UPackage
UObject* StaticLoadObjectInternal(UClass* ObjectClass, UObject* InOuter, const TCHAR* InName, const TCHAR* Filename, uint32 LoadFlags, UPackageMap* Sandbox, bool bAllowObjectReconciliation, const FLinkerInstancingContext* InstancingContext)
{
//......
// break up the name into packages, returning the innermost name and its outer
ResolveName(InOuter, StrName, true, true, LoadFlags & (LOAD_EditorOnly | LOAD_NoVerify | LOAD_Quiet | LOAD_NoWarn | LOAD_DeferDependencyLoads), InstancingContext);
}
调用ResolveName函数,处理我们传入的Outer对象和Name。比如:将我们传入的Outer对象改成了ThirdPersonCharacter这个蓝图资产对应的Package(通过我们传入的InName进行Find或Load或CreatePackage);将传入的Name中的"ThirdPersonCharacter_C"提取出来。

而在ResolveName函数中一般来说会来到这里
// Try to find the package in memory first, should be faster than attempting to load or create
InPackage = InPackage ? nullptr : StaticFindObjectFast(UPackage::StaticClass(), InPackage, *PartialName);
if (!bIsScriptPackage && !InPackage)
{
InPackage = LoadPackage(Cast<UPackage>(InPackage), *PartialName, LoadFlags, nullptr, InstancingContext);
}
我们在内存中没有此文件对应的package而且一般不是ScriptPackage(以/Script/开头的包名),我们会进入LoadPackage函数,在检查包名等操作后进入LoadPackageInternal函数,如果ShouldAlwaysLoadPackageAsync函数为true,那么就走异步加载拿到包后就返回,否则就继续第二步。
2、GetPackageLinker
他会先找已经加载的Linker并返回,否则会在处理包名后将FLinkerLoad创建出来,在FlinkerLoad中的Tick函数内我们会将Loader创建出来
// Create loader, aka FArchive used for serialization and also precache the package file summary.创建加载器,又名 FArchive,用于序列化并预缓存包文件摘要。
// false is returned until any precaching is complete.创建加载器,又名 FArchive,用于序列化并预缓存包文件摘要。
Status = CreateLoader(TFunction<void()>([]() {}));
bCanSerializePackageFileSummary = (Status == LINKER_Loaded);
}
// Serialize the package file summary and presize the various arrays (name, import & export map)
if (bCanSerializePackageFileSummary)
{
Status = ProcessPackageSummary(ObjectNameWithOuterToExportMap);
}
在ProcessPackageSummary函数中进行:
- 序列化 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()中调用到VerifyImport
// Validate all imports and map them to their remote linkers.
for (int32 ImportIndex = 0; ImportIndex < Summary.ImportCount; ImportIndex++)
{
FObjectImport& Import = ImportMap[ImportIndex];
#if WITH_EDITOR
if (SlowTask)
{
UE_SERIALIZE_ACCCESS_SCOPE_SUSPEND();
static const FText LoadingImportText = NSLOCTEXT("Core", "LinkerLoad_LoadingImportName", "Loading Import '{0}'");
SlowTask->EnterProgressFrame(1, FText::Format(LoadingImportText, FText::FromString(Import.ObjectName.ToString())));
}
#endif
VerifyImport( ImportIndex );
}
而VerifyImport主要是调用到 FLinkerLoad::VerifyImportInner,这个函数主要分为两种情况,加载的 UObject 是 Asset 实际资产和非 Asset(MemoryOnly),这两种情况还要区别是加载 UObject 还是 UPackage。就是说加载 Asset 的时候可能只是加载这个资产底下的一个 UObject 而已,也可能是加载整个 UPackage。加载非 Asset 的时候也有可能是加载 UObject 或者 UPackage。(UClass 和 UPackage 都是继承自 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,这样必定返回一个类的默认对象。
UObject* FLinkerLoad::CreateExport( int32 Index )
{
//...
// Find the Archetype object for the one we are loading.找到我们正在加载的原型对象。
UObject* Template = UObject::GetArchetypeFromRequiredInfo(LoadClass, ThisParent, Export.ObjectName, Export.ObjectFlags);//line: 5300
//...
}
UObject* GetArchetypeFromRequiredInfoImpl(const UClass* Class, const UObject* Outer, FName Name, EObjectFlags ObjectFlags, bool bUseUpToDateClass)
{
UObject* Result = NULL;
const bool bIsCDO = !!(ObjectFlags & RF_ClassDefaultObject);
if (bIsCDO)
{
Result = bUseUpToDateClass ? Class->GetAuthoritativeClass()->GetArchetypeForCDO() : Class->GetArchetypeForCDO();
}
else
{
if (Outer
&& Outer->GetClass() != UPackage::StaticClass()) // packages cannot have subobjects
{
// Get a lock on the UObject hash tables for the duration of the GetArchetype operation
FScopedUObjectHashTablesLock HashTablesLock;
UObject* ArchetypeToSearch = nullptr;
#if UE_CACHE_ARCHETYPE
ArchetypeToSearch = Outer->GetArchetype();
#else
ArchetypeToSearch = GetArchetypeFromRequiredInfoImpl(Outer->GetClass(), Outer->GetOuter(), Outer->GetFName(), Outer->GetFlags(), bUseUpToDateClass);//递归,这样必有一个CDO返回
#endif // UE_CACHE_ARCHETYPE
UObject* MyArchetype = static_cast<UObject*>(FindObjectWithOuter(ArchetypeToSearch, Class, Name));
if (MyArchetype)
{
Result = MyArchetype; // found that my outers archetype had a matching component, that must be my archetype
}
else if (!!(ObjectFlags & RF_InheritableComponentTemplate) && Outer->IsA<UClass>()){}
else{}
}
if (!Result)
{
// nothing found, I am not a CDO, so this is just the class CDO
Result = bUseUpToDateClass ? Class->GetAuthoritativeClass()->GetDefaultObject() : Class->GetDefaultObject();
}
}
return Result;
}
根据 Class Outer Name Template 构建模板对象
如果通过StaticFindObjectFastInternal找到内存中的Object就设置Linker和Flags后返回
UObject* FLinkerLoad::CreateExport( int32 Index )
{
//...
// Try to find existing object first in case we're a forced export to be able to reconcile. Also do it for the
// case of async loading as we cannot in-place replace objects.
//首先尝试找到现有对象,以防我们强制导出以便能够协调。对于异步加载的情况也这样做,因为我们无法就地替换对象。
UObject* ActualObjectWithTheName = StaticFindObjectFastInternal(nullptr, ThisParent, Export.ObjectName, true);//line: 5311
check(Export.Object == nullptr);
if (ActualObjectWithTheName && (ActualObjectWithTheName->GetClass() == LoadClass))
{
Export.Object = ActualObjectWithTheName;
}
// Object is found in memory.
if (Export.Object)
{
//...
return Export.Object;
}
//...
}
没有找到就根据 Class Outer Name Template 等,构建模板对象
UObject* FLinkerLoad::CreateExport( int32 Index )
{
//...
Export.Object = StaticConstructObject_Internal(Params);//line: 5479
//...
}
struct FStaticConstructObjectParameters
{
/** The class of the object to create */
const UClass* Class;
/** The object to create this object within (the Outer property for the new object will be set to the value specified here). */
UObject* Outer;
/** The name to give the new object.If no value(NAME_None) is specified, the object will be given a unique name in the form of ClassName_#. */
FName Name;
/** The ObjectFlags to assign to the new object. some flags can affect the behavior of constructing the object. */
EObjectFlags SetFlags = RF_NoFlags;
/** The InternalObjectFlags to assign to the new object. some flags can affect the behavior of constructing the object. */
EInternalObjectFlags InternalSetFlags = EInternalObjectFlags::None;
/**
* If specified, the property values from this object will be copied to the new object, and the new object's ObjectArchetype value will be set to this object.
* If nullptr, the class default object is used instead.
*/
UObject* Template = nullptr;
//...
};
设置 Linker
UObject* FLinkerLoad::CreateExport( int32 Index )
{
//...
// Check to see if LoadClass is a blueprint, which potentialLy needs
// to be refreshed and regenerated. If so, regenerate and patch it
// back into the export table
//检查LoadClass是否是一个蓝图,它可能需要刷新和重新生成。如果是这样,重新生成后将其修补回导出表中
if (bIsBlueprintCDO && (LoadClass->GetOutermost() != GetTransientPackage()))//line: 5537
{
{
// For classes that are about to be regenerated, make sure we register them with the linker, so future references to this linker index will be valid
//对于即将重新生成的类,请确保我们使用链接器注册它们,以便将来对此链接器索引的引用将有效
const EObjectFlags OldFlags = Export.Object->GetFlags();
Export.Object->ClearFlags(RF_NeedLoad|RF_NeedPostLoad|RF_NeedPostLoadSubobjects);
Export.Object->SetLinker( this, Index, false );
Export.Object->SetFlags(OldFlags);
}
if ( RegenerateBlueprintClass(LoadClass, Export.Object) )
{
return Export.Object;
}
}
else
{
// we created the object, but the data stored on disk for this object has not yet been loaded,
// so add the object to the list of objects that need to be loaded, which will be processed
// in EndLoad()
//我们创建了该对象,但该对象存储在磁盘上的数据尚未加载,因此将该对象添加到需要加载的对象列表中,该列表将在 EndLoad() 中处理
Export.Object->SetLinker( this, Index );
check(CurrentLoadContext);
CurrentLoadContext->AddLoadedObject(Export.Object);
}
//...
}
5、EndLoad 调用 PreLoad 方法实现序列化
while (LoadContext->DecrementBeginLoadCount() == 0 && (LoadContext->HasLoadedObjects() || LoadContext->HasPendingImportsOrForcedExports()))
{
// Temporary list of loaded objects as GObjLoaded might expand during iteration.
TArray<UObject*> ObjLoaded;
TSet<FLinkerLoad*> LoadedLinkers;
while (LoadContext->HasLoadedObjects())
{
// Accumulate till GObjLoaded no longer increases.
LoadContext->AppendLoadedObjectsAndEmpty(ObjLoaded);
// Finish loading everything.
{
for (int32 i = 0; i < ObjLoaded.Num(); i++)
{
// Preload.
UObject* Obj = ObjLoaded[i];//在这拿到之前添加进去的需要加载的所有Export等
if (Obj->HasAnyFlags(RF_NeedLoad))
{
FLinkerLoad* Linker = Obj->GetLinker();
check(Linker);
UPackage* Package = Linker->LinkerRoot;
check(Package);
Linker->Preload(Obj);
}
}
}
//再之后做一些操作后开始进行PostLoad等之类逻辑并刷新Linker删除对给定包的链接器的引用等
AsyncLoader是FArchive 下的 Loader 对象,与 uasset 文件直接关联。
Export 包含了这个 Object 导出所存储的必要信息,在文件中的起始偏移值,文件大小。将内容加载至内存随后序列化
void FLinkerLoad::Preload( UObject* Object )
{
//...
FAsyncArchive* AsyncLoader = GetAsyncLoader();
{
SCOPE_CYCLE_COUNTER(STAT_LinkerPrecache);
// tell the file reader to read the raw data from disk
if (AsyncLoader)
{
bool bReady = AsyncLoader->PrecacheWithTimeLimit(Export.SerialOffset, Export.SerialSize, bUseTimeLimit, bUseFullTimeLimit, TickStartTime, TimeLimit);
UE_CLOG(!(bReady || !bUseTimeLimit || !FPlatformProperties::RequiresCookedData()), LogLinker, Warning, TEXT("Hitch on async loading of %s; this export was not properly precached."), *Object->GetFullName());
}
else
{
Loader->Precache(Export.SerialOffset, Export.SerialSize);
}
}
//...//Object->SerializeScriptProperties(Adapter.GetArchive());等
}
四、异步加载


过程说明
- UE中的加载接口有多个,本文基于 UAsyncActionLoadPrimaryAsset::AsyncLoadPrimaryAsset 来触发资源的加载,底层基于 FStreamableManager::RequestAsyncLoad 。此为主流异步加载方式。
- 加载时会先从内存中找对应的资源是否已加载(FStreamableManager::FindInMemory),如果找到则返回,没有才需要加载。
- 加载前先初始化,把加载通过 AddPendingRequest 加入 PendingRequests 队列。基于加载路径等信息创建 FAsyncPackageDesc2 ,加入 QueuedPackages 。
- 加载时,通过 ProcessAsyncLoading 执行加载。其会调用 GPackageLoader 的 ProcessLoading (在 AsyncPackageLoader.cpp 的 InitAsyncThread 的时候,根据 WITH_ASYNCLOADING2 来决定 GPackageLoader 是 FAsyncLoadingThread 还是 FAsyncLoadingThread2)。
- 加载涉及的关键方法有:FAsyncLoadingThread::ProcessAsyncLoading 、FAsyncPackage::TickAsyncPackage 以及 FLinkerLoad::Tick 等。 其中 FAsyncPackage::TickAsyncPackage 主要负责Package的加载流程,FLinkerLoad::Tick 则负责资源的类型获取及反序列化等。
- 加载tick开始是,先把已经加载完毕的处理了,执行回调(FAsyncLoadingThread::ProcessLoadedPackages)。
- 从 QueuedPackages 中获取和准备要加载的资源包,对他们执行 FAsyncPackage::TickAsyncPackage 。
- 资源加载主要基于 FLinkerLoad ,其在加载开始时会基于目标资源路径进行创建(FLinkerLoad::CreateLoader)。
- FLinkerLoad 创建后,执行反序列化,尤为重要的是 ImportMap 和 ExportMap 等的反序列化。
- 在 FLinkerLoad::Tick 中,基于 Linker 的 ExportMap 循环创建 FObjectExport。
- 通过反射等机制调用 StaticConstructObject_Internal 等,以及借助 FLinkerLoad::IndexToObject 等从反射出的数据中拿到资源对应的基础类型。
- 对基础资源类型进行 FLinkerLoad::Preload ,主要做反序列化,给属性赋值等。
- 资源加载完毕后,加到已加载队列(FUObjectSerializeContext::AddLoadedObject)
- Package 加载完毕后,加到已加载Package队列(PackageObjLoaded.Add(Object)),注意:一个Package可能包括很多子资源。
- 全部Package都加载完毕后,执行加载完毕的回调(FAsyncPackage::CallCompletionCallbacks)
使用类型说明
-
UAssetManager
-
- 负责加载和卸载主要资产并维护游戏特定资产引用的单例对象,可覆盖以做项目自定义。
-
FStreamableManager
-
- 用于管理流式资产并将其保存在内存中的本机类。 AssetManager 是本类的具有蓝图访问权限的全局单例版本
-
FStreamable
-
- 内部对象,存储有Object的引用、加载中及激活中的句柄。
-
FStreamableHandle
-
- 同步或异步加载的句柄。 只要句柄处于活动状态,加载的资产就会保留在内存中。内有优先级、请求的资源列表、各种回调等。
-
FAsyncPackage
-
- 包含异步加载 FLinkerLoad 的所有导入和导出所需的中间数据的结构。TickAsyncPackage 是 Package 的异步加载的关键函数。
-
FAsyncLoadingThread
-
- 异步加载线程。 在异步加载线程上预加载/序列化包。 在游戏线程上 Postloads 对象。存储和更新加载中的以及完毕的资源和包列表等。
-
FLinkerLoad
-
- 处理加载虚幻包文件,包括从磁盘读取和反序列化 UObject 数据。祖父类 FLinkerTables 中存储了从文件中反序列化而来的 ImportMap 、 ExportMap 、 DependsMap 等数据,这些是获取和创建类的关键。
-
FArchive
-
- 序列化和反序列化的关键类,详见“Unreal 序列化和反序列化原理机制源码解析”,此处主要关注加载卸载,不对此进行赘述。
一、AssetRegistry
AssetRegistry 是资产信息的管理器。它将有关资产的 FAssetData 缓存在 UE 的后台,可用于在不加载资产的情况下查询此 FAssetData。因此,AssetRegistry 与 FAssetData 具有紧密的联系。
FAssetData FAssetData是一个用来保存资产信息的结构体。
//资产的对象路径,格式为PackageName.AssetName。只有包中最顶层的对象才能有资产数据 FName ObjectPath; //完整的的包名,如/Game/Path/Package FName PackageName; //资产所在包的路径,即/Game/Path/ FName PackagePath; //资产名称 FName AssetName; //资产的类名 FName AssetClass;
编辑器使用 AssetRegistry 来收集引用、数据验证、ContentBrowser 的高级搜索等。
我们可以通过 AssetRegistry 来执行类似的任务,例如根据 FARFilter 来搜索 FAssetData 以找到符合要求的 Asset。
在下面的代码中,我们通过 AssetData.GetTagValue() 来搜索名为“Triangles”的 TagValue,并找到 Triangles 值小于 300 的 StaticMesh。
TagValue 就是我们经常在 ContentBrowser 中看到的资产面板中的属性。

// 搜索 /Game/ 路径下所有 Triangles 小于 300 的StaticMesh
int32 MaxTriangles = 300;
IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry();
FARFilter ARFilter;
ARFilter.ClassNames.Add(UStaticMesh::StaticClass()->GetFName());
ARFilter.PackagePaths.Add(TEXT("/Game/"));
ARFilter.bRecursivePaths = true;
TArray<FSoftObjectPath> AssetList;
TArray<FAssetData> Assets;
AssetRegistry.GetAssets(ARFilter, Assets);
//AssetList.Reserve(Assets.Num());
for(int32 i = 0; i < Assets.Num(); i++)
{
const FAssetData& AssetData = Assets[i];
int32 TriangleCount;
if(AssetData.GetTagValue<int32>("Triangles", TriangleCount)
&& TriangleCount < MaxTriangles)
{
AssetList.Add(AssetData.ToSoftObjectPath());
}
}
AssetList.Shrink();
for(const FSoftObjectPath& Asset : AssetList)
{
FString Message = Asset.ToString();
GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, Message);
}
二、资源变Dirty
“脏”(Dirty)的意思
当用户对一个资源包(关卡,或材质等美术资源)做出了修改,变得与磁盘上存储的内容有差异时,UE会对其“标脏”(Mark Dirty),显示为 * 符号:
标记为Dirty
我们一般对资产做修改后都会去调用MarkPackageDirty函数,使用户知道有修改,用户需要决定是否需要保存。
有两种方法一个是MarkPackageDirty();一个是SetDirtyFlag(true);
/**
* Marks/Unmarks the package's bDirty flag, save the package to the transaction buffer if a transaction is ongoing
*/
COREUOBJECT_API void SetDirtyFlag( bool bIsDirty );
//D:\UE_5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\Package.cpp
/**
* Marks/Unmarks the package's bDirty flag
*/
void UPackage::SetDirtyFlag( bool bInIsDirty ){
//...
// Package is changing dirty state, let the editor know so we may prompt for source control checkout
PackageDirtyStateChangedEvent.Broadcast(this);
//...
}
/** D:\UE_5.4\Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectBaseUtility.h
* Finds the outermost package and marks it dirty.
* The editor suppresses this behavior during load as it is against policy to dirty packages simply by loading them.
*
* @return false if the request to mark the package dirty was suppressed by the editor and true otherwise.
*/
COREUOBJECT_API bool MarkPackageDirty() const;
/** D:\UE_5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBaseUtility.cpp
* Finds the outermost package and marks it dirty
*/
bool UObjectBaseUtility::MarkPackageDirty() const{
//...
// We prevent needless re-dirtying as this can be an expensive operation.
if( !bIsDirty )
{
Package->SetDirtyFlag(true);
}
// Always call PackageMarkedDirtyEvent, even when the package is already dirty
Package->PackageMarkedDirtyEvent.Broadcast(Package, bIsDirty);
//...
}
两个委托都是在Package.cpp中
/** Delegate to notify subscribers when the dirty state of a package is changed.
* Allows the editor to register the modified package as one that should be prompted for source control checkout.
* Use Package->IsDirty() to get the updated dirty state of the package */
UPackage::FOnPackageDirtyStateChanged UPackage::PackageDirtyStateChangedEvent;//这个是SetDirtyFlag会调用的
/**
* Delegate to notify subscribers when a package is marked as dirty via UObjectBaseUtilty::MarkPackageDirty
* Note: Unlike FOnPackageDirtyStateChanged, this is always called, even when the package is already dirty
* Use bWasDirty to check the previous dirty state of the package
* Use Package->IsDirty() to get the updated dirty state of the package
*/
UPackage::FOnPackageMarkedDirty UPackage::PackageMarkedDirtyEvent;//这个是MarkPackageDirty调用的
//用法
UPackage::PackageMarkedDirtyEvent.AddRaw(this, &FAssetManagerEditorModule::OnMarkPackageDirty);//例子
参考链接
6,[[中文直播]第33期 UE4资产管理基础1 Epic 大钊](https://www.bilibili.com/video/BV1Mr4y1A7nZ/?spm_id_from=333.337.search-card.all.click&vd_source=0bbcc4f97fa0a9ef4522e975882db7eb)