本篇内容和源码均参考 UE5。

# UE 资产导出和加载

资产(Asset)是 Unreal Engine 中持久化对象的重要文件,通常以 .uasset 结尾,Asset 的官方定义是存储 UObject 被序列化后的二进制文件。而在 Unreal Engine Editor 中预览和修改操作,本质上就是对这些「资产」文件的读写。下面以 BlueprintStruct 为例,分成以下几个步骤,简单梳理整个流程:

  • 资产加载(Load)
  • 反序列化(UnSerialize)
  • 资产导出 (Export)
  • 序列化(Serialize)

# 资产加载

Unreal 对于非必须资产的加载,使用的是「懒加载」模式。编辑器会在第一次打开的时候读取文件内容并解析到内存。

通过 UAssetEditorSubsystem::OpenEditorForAsset -> LoadPackage -> LoadPackageInternal

# BeginLoad

BeginLoad 主要是用来在加载资源之前,处理好所有留存的异步任务。这个处理操作会在主线程(Game Thread)完成,并且会一直阻塞,直到所有异步任务都处理完毕,一般用于加载资源之前提前进行垃圾回收,避免加载过程中触发 GC。

void BeginLoad(FUObjectSerializeContext* LoadContext, const TCHAR* DebugContext)
{
	if (!LoadContext->HasStartedLoading() && !IsInAsyncLoadingThread())
	{
		// Make sure we're finishing up all pending async loads, and trigger texture streaming next tick if necessary.
		FlushAsyncLoading();
	}
}
void FlushAsyncLoading(int32 RequestId /* = INDEX_NONE */)
{
	if (GPackageLoader)
	{
		GPackageLoader->FlushLoading(RequestId);
	}
}
void FAsyncLoadingThread::FlushLoading(int32 PackageID)
{
	// ...
    while (IsAsyncLoadingPackages())
    {
        // 处理异步任务
        EAsyncPackageState::Type Result = TickAsyncLoading(false, false, 0, FlushRequest);
        if (IsMultithreaded())
        {
            // 维持心跳
            FThreadHeartBeat::Get().HeartBeat();
            FPlatformProcess::SleepNoStats(0.0001f);
        }
    }
    // ...
}

# GetPackageLinker

GetPackageLinker 负责初始化 UPackage 关联的 FLinkerLoad 对象。FLinkerLoad 是整个资源加载的核心,负责 FStructuredArchiveFormatterFArchiveFStructuredArchiveFStructuredArchiveRecord 的管理工作。在 Window 平台下,读取二进制文件时,FLinkerLoad 中大致的关系如下图:

image-20221219172720904

  • FArchive:通过重载 << 运算符,实现数据的写入和读取操作。

  • FStructuredArchiveRecord && FStructuredArchiveFormatter && FStructuredArchive:共同定义了读写数据的格式范式。几乎所有 UPackage 数据都可以通过 Slot、Stream、Array、Map、Record 排列组合。

# FArchiveFileReaderGeneric::InternalPrecache

FArchiveFileReaderGeneric 是 FArchive 的派生类,负责读接口的封装和读数据的暂存。

bool FArchiveFileReaderGeneric::InternalPrecache( int64 PrecacheOffset, int64 PrecacheSize )
{
    // ...
    int64 WriteOffset = 0;
    int64 BufferCount = FMath::Min(BufferSize, Size - Pos);
    int64 ReadCount = BufferCount;
    BufferArray.SetNumUninitialized(BufferCount, false /* AllowShrink */);
    
    // 读取内容
	int64 Count = 0;
	ReadLowLevel( BufferArray.GetData() + WriteOffset, ReadCount, Count );
    // ...
	return true;
}

FArchiveFileReaderGeneric 持有读取文件的句柄 ——Handle,通过 Handle 对文件进行读取。

void FArchiveFileReaderGeneric::ReadLowLevel( uint8* Dest, int64 CountToRead, int64& OutBytesRead )
{
	if( Handle->Read( Dest, CountToRead ) )
	{
		OutBytesRead = CountToRead;
	}
	else
	{
		OutBytesRead = 0;
	}
}

Window 版本下通过 ReadFile 调用 SDK 对文件进行读取,读取的二进制内容,会保存在 FArchiveFileReaderGeneric 的 BufferArray 中,加载过程到此结束:

//window 下 IFileHandle 的实现类
bool FFileHandleWindows::Read(uint8* Dest, int64 BytesToRead) override
{
    check(IsValid());
    // Now kick off an async read
    TRACE_PLATFORMFILE_BEGIN_READ(&OverlappedIO, FileHandle, FilePos, BytesToRead);
    int64 TotalNumRead = 0;
    do
    {
        uint32 BytesToRead32 = (uint32)FMath::Min<int64>(BytesToRead, int64(UINT32_MAX));
        uint32 NumRead = 0;
        if (!ReadFile(FileHandle, Destination, BytesToRead32, (::DWORD*)&NumRead, &OverlappedIO))
        {
			//... 异常处理 ...
        }
        BytesToRead -= BytesToRead32;
        Destination += BytesToRead32;
        TotalNumRead += NumRead;
        // Update where we are in the file
        FilePos += NumRead;
        UpdateOverlappedPos();
	    // ...
    } while (BytesToRead > 0);
    return true;
}

# 反序列化

加载获取到二进制文件后,下一步就是解析二进制内的内容,得到对应的 UPackage 对象。

# FLinkerLoad::ProcessPackageSummary

ProcessPackageSummary 会对二进制进行解析,并把结果结构化的存储在 StructuredArchiveRootRecord 中,主要分成以下几个步骤,由于该部分代码比较复杂,有兴趣的可以深入阅读,这里只简单介绍一下:

FLinkerLoad::ELinkerStatus FLinkerLoad::ProcessPackageSummary(TMap<TPair<FName, FPackageIndex>, FPackageIndex>* ObjectNameWithOuterToExportMap)
{
    // 解析文件概要:
    // 「FPackageFileSummary Summary」 
    Status = SerializePackageFileSummary();
    // 根据 Summary 信息解析 PackageTrailer,包含一些查找表信息
    // 「TUniquePtr<UE::FPackageTrailer> PackageTrailer」
    Status = SerializePackageTrailer();
    // 解析名称映射,序列化对象的时候,相同的字段名通常为了压缩会集中存储,然后其他地方通过名称映射的编号访问
    // 「TArray<FNameEntryId> NameMap」
    Status = SerializeNameMap();
    // 读取软链接,包括一些资产引用、子路径等
    // 「TSet<FSoftObjectPath> SoftObjectPathList」
    Status = SerializeSoftObjectPathList();
    // 文本数据读取
    Status = SerializeGatherableTextDataMap();
    // 读取 ImportMap
    // 「TArray<FObjectImport> ImportMap」
    Status = SerializeImportMap();
    // 读取 ExportMap
    // 「TArray<FObjectExport> ExportMap」
    Status = SerializeExportMap();
    // 构造 ExportReaders
    // 「TArray<FStructuredArchiveChildReader*> ExportReaders」
    Status = ConstructExportsReaders();
    // 修复版本的读取 ImportMap(修复读取到的和最新的不一致问题?)
    Status = FixupImportMap();
    Status = PopulateInstancingContext();
    Status = PopulateRelocationContext();
    // 修复版本的读取 ExportMap(修复读取到的和最新的不一致问题?)
    Status = FixupExportMap();
    // 读取 DependsMap
    // 「TArray<TArray<FPackageIndex> > DependsMap」
    Status = SerializeDependsMap();
    // 生成 ExportMap 对象的 Hash 值并关联 Hash 和 Idx
    Status = CreateExportHash();
    // 通过 Export.OuterIndex 查找 Export 中是否已经有导出好的对象了,有的话直接关联上该 FLinkerLoad
    Status = FindExistingExports();
    // 序列化 DependsMap
    // 「TArray<FPackageIndex> PreloadDependencies」
    Status = SerializePreloadDependencies();
    // LinkerLoad 创建完成,添加到 FLinkerManager,并再次验证 ImportMap 合法性。
    Status = FinalizeCreation(ObjectNameWithOuterToExportMap);
	return Status;
}

前面提到了,从文件中读取到的二进制内容会存储在 FArchiveFileReaderGeneric 的 BufferArray 中,在反序列化过程会使用 << 运算符从 BufferArray 把数据解析到 StructuredArchiveRootRecord 内,这里以读取 Summary 数据为例:

// 反序列化 Summary
FLinkerLoad::ELinkerStatus FLinkerLoad::SerializePackageFileSummaryInternal()
{
	StructuredArchiveRootRecord.GetValue() << SA_VALUE(TEXT("Summary"), Summary);
}
template<typename T> FORCEINLINE FStructuredArchiveRecord& operator<<(UE::StructuredArchive::Private::TNamedValue<T> Item)
{
    EnterField(Item.Name) << Item.Value;	// Item.Value == FPackageFileSummary
    return *this;
}
// 通过基类的 << 运算符读取 Summary 内的各项数值,这里以 int32 的 Tag 为例:
void operator<<(FStructuredArchive::FSlot Slot, FPackageFileSummary& Sum)
{
	if (bCanStartSerializing)
	{
		Record << SA_VALUE(TEXT("Tag"), Sum.Tag);	// 这里还会执行一次 EnterField 然后进到 FStructuredArchiveSlot << 逻辑
	}
	// ...
}
void FStructuredArchiveSlot::operator<< (int32& Value)
{
	StructuredArchive.EnterSlot(*this);
	StructuredArchive.Formatter.Serialize(Value); // 「FBinaryArchiveFormatter::Serialize」
	StructuredArchive.LeaveSlot();
}
inline void FBinaryArchiveFormatter::Serialize(int32& Value)
{
	Inner << Value;	// 这里会执行 FLinkerLoad << 逻辑
}
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int32& Value)
{
    // ...
    Ar.ByteOrderSerialize(reinterpret_cast<uint32&>(Value));
    return Ar;
}
template<typename T>
FArchive& ByteOrderSerialize(T& Value)
{
    if (!IsByteSwapping())
    {
        Serialize(&Value, sizeof(T)); // 「FLinkerLoad::Serialize」
        return *this;
    }
    return SerializeByteOrderSwapped(Value);
}
using FLinker::Serialize;
FORCEINLINE virtual void Serialize(void* V, int64 Length) override
{
	// ...
    Loader->Serialize(V, Length);	// 「FArchiveFileReaderGeneric::Serialize」
}
// 这里会从 BufferArray 把对应长度的内容读取进来
void FArchiveFileReaderGeneric::Serialize( void* V, int64 Length )
{
	// ...
	while( Length>0 )
	{
		// ...
		FMemory::Memcpy( V, BufferArray.GetData()+Pos-BufferBase, Copy );
		Pos       += Copy;
		Length    -= Copy;
		V          =( uint8* )V + Copy;
	}
}

# FLinkerLoad::LoadAllObjects

LoadAllObjects 主要是初始化 ExportObject。以 BlueprintStruct 为例,其组成主要分为以下几个对象:

  • UMetaData:定义了 UPackage 对象本身的特殊属性,描述一些基本信息。
  • UUserDefinedStruct:定义了 BlueprintStruct 里的数据结构:

image-20221219111729365

image-20221219111700076

  • UUserDefinedStructEditorData:该结构负责存储 BlueprintStruct 在 Unreal Editor 中的相关信息。

到此,LoadPackageInternal 流程就算是完成了,uasset 文件也被反序列化加载到了内存中,编辑器相关的数据也初始化完成。

# 资产导出

编辑器里对于资产的保存是通过注册的控件事件向控制台发送 CMD 实现的,具体指令如下:

OBJ SAVEPACKAGE PACKAGE="/Game/learn_blueprint/BP_ST_SUB_TASK_DATA" FILE="../../../../programe/learn_ue/Content/learn_blueprint/BP_ST_SUB_TASK_DATA.uasset" SILENT=true

image-20221219114203968

先来看看 SaveAsset 的调用堆栈:

image-20221219114557034

static InternalSavePackageResult InternalSavePackage(UPackage* PackageToSave, bool bUseDialog, bool& bOutPackageLocallyWritable, FOutputDevice &SaveOutput)
{	
    // ...
	bWasSuccessful = GEngine->Exec( NULL, *FString::Printf( TEXT("OBJ SAVEPACKAGE PACKAGE=\"%s\" FILE=\"%s\" SILENT=true"), *PackageName, *FinalPackageSavePath ), SaveOutput );
    // ...
}

CMD 的解析流程这里就不再展开了,函数最终会执行到 UEditorEngine::Save

image-20221219115129805

# HarvestPackage

在导出对象之前,Unreal 还需要系统性的收集并整理好所有需要打包的内容,打包所需的依赖,这项工作由 FPackageHarvester 来完成,其管理了若干个 FHarvestedRealm,并通过 << 运算符,把需要导出的对象信息按照 ExportObject 为单位存储在 FHarvestedRealm 中:

image-20221219152624707

# FPackageHarvester::ProcessExport

ProcessExport 导出的内容主要是之前提到的 UMetaDataUUserDefinedStructUUserDefinedStructEditorData 三个 ExportObject:

void FPackageHarvester::ProcessExport(const FExportWithContext& InProcessContext)
{	
	UObject* Export = InProcessContext.Export;
	// In the CDO case the above would serialize most of the references, including transient properties
	// but we still want to serialize the object using the normal path to collect all custom versions it might be using.
	{
		SCOPED_SAVETIMER_TEXT(*WriteToString<128>(GetClassTraceScope(Export), TEXT("_SaveSerialize")));
		Export->Serialize(*this);
	}
}

每个类型都有与之对应的宏定义,这里并非真正意义上的序列化,而是获取需要序列化的数据集:

// UMetaData
IMPLEMENT_FARCHIVE_SERIALIZER(UMetaData);
#define IMPLEMENT_FARCHIVE_SERIALIZER( TClass ) void TClass::Serialize(FArchive& Ar) { TClass::Serialize(FStructuredArchiveFromArchive(Ar).GetSlot().EnterRecord()); }
// UUserDefinedStruct
IMPLEMENT_FARCHIVE_SERIALIZER(UUserDefinedStruct);
// UUserDefinedStructEditorData
IMPLEMENT_FSTRUCTUREDARCHIVE_SERIALIZER(UUserDefinedStructEditorData)
#define IMPLEMENT_FSTRUCTUREDARCHIVE_SERIALIZER( TClass ) void TClass::Serialize(FStructuredArchive::FRecord Record) { FArchiveUObjectFromStructuredArchive Ar(Record.EnterField(TEXT("BaseClassAutoGen"))); TClass::Serialize(Ar.GetArchive()); Ar.Close(); }

主要来看下 UUserDefinedStruct 中的实现,该操作会把 UUserDefinedStruct 内的 Property 存储在 FHarvestedRealm 内:

image-20221219145506673

由于代码过于复杂,这里做了一定程度上的简化:

// 直接执行父类序列化接口
void UUserDefinedStruct::SerializeTaggedProperties(FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct, uint8* Defaults, const UObject* BreakRecursionIfFullyLoad) const 
{
	// ...
	Super::SerializeTaggedProperties(Slot, Data, DefaultsStruct, Defaults, BreakRecursionIfFullyLoad);
}
// 执行带版本控制的属性序列化 SerializeVersionedTaggedProperties
void UStruct::SerializeTaggedProperties(FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct, uint8* Defaults, const UObject* BreakRecursionIfFullyLoad) const
{
	if (Slot.GetArchiveState().UseUnversionedPropertySerialization())
	{
		SerializeUnversionedProperties(this, Slot, Data, DefaultsStruct, Defaults);
	}
	else
	{
		SerializeVersionedTaggedProperties(Slot, Data, DefaultsStruct, Defaults, BreakRecursionIfFullyLoad);
	}
}
// 由于属性本身就是单链表结构,因此这里其实就是一个个的进行序列化,但是加了很多复杂校验因此看着代码非常庞大
void UStruct::SerializeVersionedTaggedProperties(FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct, uint8* Defaults, const UObject* BreakRecursionIfFullyLoad) const
{	
    // Iterate over properties in the order they were linked and serialize them.
    for (FProperty* Property = UnderlyingArchive.ArUseCustomPropertyList ? (CustomPropertyNode ? CustomPropertyNode->Property : nullptr) : PropertyLink; Property; Property = UnderlyingArchive.ArUseCustomPropertyList ? FCustomPropertyListNode::GetNextPropertyAndAdvance(CustomPropertyNode) : Property->PropertyLinkNext)
    {
        FPropertyTag Tag( UnderlyingArchive, Property, Idx, DataPtr, DefaultValue );
        PropertySlot << Tag;
        Tag.SerializeTaggedProperty(PropertySlot, Property, DataPtr, DefaultValue);
    }
}
// 通过 << 运算符存储
void operator<<(FStructuredArchive::FSlot Slot, FPropertyTag& Tag)
{
	Slot << SA_ATTRIBUTE(TEXT("Name"), Tag.Name);
	Slot << SA_ATTRIBUTE(TEXT("Type"), Tag.Type);
	// ...
}

这里以 task_id 为例,说明整个存储过程:

image-20221219212010069

template <typename T>
FORCEINLINE void operator<<(UE::StructuredArchive::Private::TNamedAttribute<T> Item)
{
    EnterAttribute(Item.Name) << Item.Value;
}
void FStructuredArchiveSlot::operator<< (FName& Value)
{
	StructuredArchive.EnterSlot(*this);
	StructuredArchive.Formatter.Serialize(Value);
	StructuredArchive.LeaveSlot();
}
inline void FBinaryArchiveFormatter::Serialize(FName& Value)
{
	Inner << Value;
}
FArchive& FArchiveFromStructuredArchiveImpl::operator<<(class FName& Value)
{
	InnerArchive << Value;
	return *this;
}
// 最终会调用到 FPackageHarvester 的 << 函数
FArchive& FPackageHarvester::operator<<(FName& Name)
{
	HarvestExportDataName(Name);
	return *this;
}
// 数据最终会存储在 HarvestedRealm 的 NamesReferencedFromExportData 中以备导出时使用
// 实际上 property 里面的字段还有很多,不同字段可能存放在不同位置,这里仅仅是用变量的别名存储作为样例
void FPackageHarvester::HarvestExportDataName(FName Name)
{
	SaveContext.GetHarvestedRealm(CurrentExportHarvestingRealm).GetNamesReferencedFromExportData().Add(Name.GetDisplayIndex());
}

image-20221219212427302

值得一提的是,这里的三个 HarvestedRealms 分别定义了三个存储域,目前用的是 Editor 版本,因此存储在第三个 HarvestedRealms

/** 
 * Available save realm during save package harvesting 
 * A realm is the set of objects gathered and referenced for a particular domain/context
 */
enum class ESaveRealm : uint32
{
	Game		= 0,
	Optional,
	Editor,
	RealmCount,
	None		= RealmCount
};

# CreateLinker && BuildLinker

从上面 SaveContext 的数据中可以发现,所有 HarvestedRealms 里的 Linker 对象都是 NULL,根据资产加载的经验,每个文件的导出必须依赖一个 Linker。因此还需要创建对应的 Linker 对象来关联导出文件,此外还需要关联上要导出的三个对象:UMetaDataUUserDefinedStructUUserDefinedStructEditorData

image-20221219171233982

// 创建 Linker 关联
ESavePackageResult CreateLinker(FSaveContext& SaveContext)
{
    //...
    // Allocate the linker with a tempfile, forcing byte swapping if wanted.
    SaveContext.SetTempFilename(FPaths::CreateTempFilename(*FPaths::ProjectSavedDir(), *BaseFilename.Left(32)));
    SaveContext.SetLinker(MakePimpl<FLinkerSave>(SaveContext.GetPackage(), *SaveContext.GetTempFilename().GetValue(), SaveContext.IsForceByteSwapping(), SaveContext.IsSaveUnversionedNative()));
    //...
	// set formatter
	SaveContext.SetFormatter(MakeUnique<FBinaryArchiveFormatter>(*(FArchive*)SaveContext.GetLinker()));
	SaveContext.SetStructuredArchive(MakeUnique<FStructuredArchive>(*SaveContext.GetFormatter()));
	return ReturnSuccessOrCancel();
}
// 主要是把 HarvestedRealms 里的数据解析存储到 Linker 内
ESavePackageResult BuildLinker(FSaveContext& SaveContext)
{
    // Setup Linker 
	{
		// ...
	}
	// Build Name Map
	{
        // ...
	}
	// Build SoftObjectPathList
	{
		// ...
	}
	// Build GatherableText
	{
		// ...
	}
	// Build ImportMap
	{
		// ...
	}
	// Build ExportMap & Package Netplay data
	{
		// ...
	}
	// Build Linker Reverse Mapping
	{
		// ...
	}
	// Build DependsMap
	{
		// ...
	}
	// Build SoftPackageReference & Searchable Name Map
	{
		// ...
	}
	// Map Export Indices
	{
		// ...
	}
	return ReturnSuccessOrCancel();
}

# 序列化

序列化流程就是把对象数据打包成二进制文件,Window 环境下序列化 UUserDefinedStruct 涉及到的类图:

image-20221219222221548

  • FHarvestedRealm 内部的对象非常丰富,承担了序列化数据的临时载体。
  • FSaveContext 是整个流程的核心。负责管理 FHarvestedRealm 和各种上下文标记。
  • FLinkerSave 则是用来关联导出文件和 FSaveContext,等于是所有功能载体的集合。

# WritePackageHeader && WriteExports

写入其实分为两个部分,一个是头文件(WritePackageHeader),包括 Summary 、NameMap 等等,可以参考之前的反序列化;另一个是 ExportObject 内容(WriteExports),整体流程比较接近,这里以 WriteExports 为例进行介绍:

image-20221219213849222

整体流程和 FPackageHarvester::ProcessExport 几乎一致,唯一的区别在于 FArchiveFromStructuredArchiveImpl 中的 InnerArchive 对象:

FArchive& FArchiveFromStructuredArchiveImpl::operator<<(class FName& Value)
{
    // ProcessExport 中由于还没有初始化 Linker 对象,因此 InnerArchive 是 FPackageHarvester
    // WriteExports 中 Linker 对象已经初始化完毕,因此 InnerArchive 是 FLinkerSave
	InnerArchive << Value;
	return *this;
}
// ProcessExport
FArchive& FPackageHarvester::operator<<(FName& Name)
{
	HarvestExportDataName(Name);
	return *this;
}
// WriteExports
FArchive& FLinkerSave::operator<<( FName& InName )
{
    //...
    return Ar << Save << Number;
}

FLinkerSave 最终会调用 FArchiveFileWriterGenericSerializeBufferArray 内临时存储的二进制内容,写入到 Handler 句柄所绑定的文件内。

void FLinkerSave::Serialize( void* V, int64 Length )
{
	Saver->Serialize( V, Length );
}
void FArchiveFileWriterGeneric::Serialize( void* V, int64 Length )
{
	Pos += Length;
	if ( Length >= BufferSize )
	{
		FlushBuffer();
		if( !WriteLowLevel( (uint8*)V, Length ) )
		{
			SetError();
			LogWriteError(TEXT("Error writing to file"));
		}
	}
	else
	{
		int64 Copy;
		while( Length >( Copy=BufferSize-BufferArray.Num() ) )
		{
			BufferArray.Append((uint8*)V, Copy);
			Length      -= Copy;
			V            =( uint8* )V + Copy;
			FlushBuffer();	// 回写
		}
		if( Length )
		{
			BufferArray.Append((uint8*)V, Length);
		}
	}
}
bool FArchiveFileWriterGeneric::FlushBuffer()
{
	bool bDidWriteData = false;
	if (int64 BufferNum = BufferArray.Num())
	{
		bDidWriteData = WriteLowLevel(BufferArray.GetData(), BufferNum);
		if (!bDidWriteData)
		{
			SetError();
			LogWriteError(TEXT("Error flushing file"));
		}
		BufferArray.Reset();
	}
	return bDidWriteData;
}
bool FArchiveFileWriterGeneric::WriteLowLevel( const uint8* Src, int64 CountToWrite )
{
	return Handle->Write( Src, CountToWrite );
}

# FinalizeTempOutputFiles

另外导出文件实际上是个临时文件,因为考虑到写入过程中可能出现的各种异常情况,如果写坏了原文件并且写入异常情况下,可能导致新文件不可用

image-20221219161435715

并且,在文件写入成功即将替换掉原文件之前,Unreal 还会对原文件进行备份,避免异常情况下替换失败导致原文件丢失:

ESavePackageResult FinalizeTempOutputFiles(const FPackagePath& PackagePath, const FSavePackageOutputFileArray& OutputFiles, const FDateTime& FinalTimeStamp)
{
	for (int32 Index = 0; Index < OutputFiles.Num(); ++Index)
	{
		if (CanFileBeMoved[Index]) 
		{
            // 【step.1】备份文件 .uasset -> old.temp
			if (FileSystem.Move(*TempFilePath, *File.TargetPath))
			{
				OriginalPackageState.RecordMovedFile(File.TargetPath, TempFilePath);
			}
			else
			{
				OriginalPackageState.RestorePackage();
				return ESavePackageResult::Error;
			}
		}
	}
	for (const FSavePackageOutputFile& File : OutputFiles)
	{
		if (!File.TempFilePath.IsEmpty()) 
		{
			// 【step.2】新文件覆盖原文件 new.temp -> .uasset
			if (FileSystem.Move(*File.TargetPath, *File.TempFilePath))
			{
				OriginalPackageState.RecordNewFile(File.TargetPath);
			}
			else
			{
				OriginalPackageState.RestorePackage();
				return ESavePackageResult::Error;
			}
			if (FinalTimeStamp != FDateTime::MinValue())
			{
				FileSystem.SetTimeStamp(*File.TargetPath, FinalTimeStamp);
			}
		}
	}
	// 【step.3】清理原文件的备份 remove old.temp
	OriginalPackageState.DiscardBackupFiles();
	return ESavePackageResult::Success;
}

# 总结

至此,整个加载导出流程就基本上梳理完成,其中的某些对象由于依赖关系和复杂的数据结构等问题可能涉及到更为复杂的序列化 && 反序列化规则,但整理流程应该大同小异,更多细节的内容还需要读者自行摸索。

更新于 阅读次数

请我[恰饭]~( ̄▽ ̄)~*

鑫酱(●'◡'●) 微信支付

微信支付

鑫酱(●'◡'●) 支付宝

支付宝