你的浏览器不支持canvas

让我们携手共创奇迹!

资源加载

Date: Author: LBD

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自WindCrazy

这是关于资源加载的内容

路径怎么写:

  • 类名’/路径/包名.对象名:子对象名’(类名其实会被忽略掉,所以不写也行)
  • 类:/路径/包名.类名_C (包名往往和类名一致)
  • 对象:/路径/包名.对象名 (包名往往和对象名一致)
  • 路径:/Root/Path1/SubPath2
  • 路径Root:/Engine, /Game, /XXXModule

一、概要

  • Unreal 主要基于 FStreamableManagerFAsyncLoadingThread(按需也可为 FAsyncLoadingThread2)、 FLinkerLoad 等进行资源的异步加载。
  • 资源加载主要基于Package来进行,一个Package中包含多种类型的数据信息,Package的加载完成前提是这些资源全部加载完毕。加载完毕还涉及到其中各类型资源的类型构造、属性等的反序列化设置以及依赖资源的加载。全部完成后Package才算加载完毕。

二、文件,加载类,内存中资源

UPackage

一个资源在内存中表现为一个 UPackage的实例,比如一个 SoundCue 资源,SoundCue 内部可能有很多个蓝图节点,就有一些节点的数据,比如 Modulator、Mixer 等等,这些数据是实例本身的数据。同时 SoundCue 也引用外部声音文件 SoundWave。SoundWave 也是一个资源,也是对应的一个 UPackage实例。这样两个 UPackage之间就存在依赖关系。

UPackageUObjectOuter。要知道资源自身数据 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加载

加载内容的几个步骤:

  1. 根据文件名字创建一个空的包(没有任何文件相关的数据)
  2. 建立一个 LinkerLoad 去加载对应的 uasset 文件 序列化。
  3. 优先加载 ImportMap
  4. 加载 ExportMap(本身的数据)
  5. 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进行FindLoadCreatePackage);将传入的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

  1. 是 UPackage,则取得 CDO (Class Default Object),相当于类默认构造函数所构建的一个对象,一个类会在内存中放置一个 CDO。
  2. 不是 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());等
}

四、异步加载

UE4.27图

UE4.27图

过程说明

  • 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

一、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 中看到的资产面板中的属性。

每一个条目都是一个 TagValue

// 搜索 /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);//例子

参考链接

1,UE4资源加载(转载)

2,,《调教UE5:编辑器拓展指南》常用模块

3,UE4 Package浅析

4,UE中查“资源包因何变脏”的方式

5, Unreal 资源加载和卸载原理机制源码剖析

6,[[中文直播]第33期 UE4资产管理基础1 Epic 大钊](https://www.bilibili.com/video/BV1Mr4y1A7nZ/?spm_id_from=333.337.search-card.all.click&vd_source=0bbcc4f97fa0a9ef4522e975882db7eb)

对于本文内容有问题或建议的小伙伴,欢迎提出问题