刚好遇到一个ini加载的优化。趁此机会记录学习一下UE引擎的config文件层级结构和读取流程

两个问题

在看项目目录结构的时候,有没有这样的疑问:
为什么saved同级目录下面有一个config,saved里面还有一个config:saved/config?它们有什么区别吗?
【UE4】引擎配置文件原理学习笔记-LMLPHP

看虚幻读配置文件的源码:发现它读取配置文件用的是一个LoadExternalIniFile的函数。但这个函数执行了读操作以外,还进行了写操作。为什么虚幻加载配置文件的以后还要写文件?
【UE4】引擎配置文件原理学习笔记-LMLPHP

层级结构

实际上,UE采用的config配置文件读取策略是以层级的形式,可以分为以下四个层级:

  1. Engine/Config
  2. Engine/Saved/Config
  3. [ProjectName]/Config
  4. [ProjectName]/Saved/Config

加载一个名为"ConfigFileName"的config文件的时候,UE会依次遍历这些目录。后读取的覆盖前面的,即:其优先级依次递增。

这个层级的概念很有意思。颇有点子类复写的味道:如果项目内的目录就有ConofigFileName这个ini,就使用项目内的ini。否则使用其父层级引擎目录的ConofigFileName.ini。

保证了修改一个项目的ini不会影响到别的项目,没修改的部分依然沿用通用的设置。

UE会读到的配置文件目录记录于ConfigCacheIni.cpp的GConfigLayers中:

// ConfigCacheIni.cpp
GConfigLayers[] =
{
	/**************************************************
	**** CRITICAL NOTES
	**** If you change this array, you need to also change EnumerateConfigFileLocations() in ConfigHierarchy.cs!!!
	**** And maybe UObject::GetDefaultConfigFilename(), UObject::GetGlobalUserConfigFilename()
	**************************************************/

	// Engine/Base.ini
	{ TEXT("AbsoluteBase"),				TEXT("{ENGINE}/Config/Base.ini"), EConfigLayerFlags::Required | EConfigLayerFlags::NoExpand},
	
	// Engine/Base*.ini
	{ TEXT("Base"),						TEXT("{ENGINE}/Config/Base{TYPE}.ini") },
	// Engine/Platform/BasePlatform*.ini
	{ TEXT("BasePlatform"),				TEXT("{ENGINE}/Config/{PLATFORM}/Base{PLATFORM}{TYPE}.ini")  },
	// Project/Default*.ini
	{ TEXT("ProjectDefault"),			TEXT("{PROJECT}/Config/Default{TYPE}.ini"), EConfigLayerFlags::AllowCommandLineOverride | EConfigLayerFlags::GenerateCacheKey },
	// Project/Generated*.ini Reserved for files generated by build process and should never be checked in 
	{ TEXT("ProjectGenerated"),			TEXT("{PROJECT}/Config/Generated{TYPE}.ini"), EConfigLayerFlags::GenerateCacheKey },
	// Engine/Platform/Platform*.ini
	{ TEXT("EnginePlatform"),			TEXT("{ENGINE}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
	// Project/Platform/Platform*.ini
	{ TEXT("ProjectPlatform"),			TEXT("{PROJECT}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
	// Project/Generated*.ini Reserved for files generated by build process and should never be checked in 
	{ TEXT("ProjectPlatformGenerated"),	TEXT("{PROJECT}/Config/{PLATFORM}/Generated{PLATFORM}{TYPE}.ini") },
	// UserSettings/.../User*.ini
	{ TEXT("UserSettingsDir"),			TEXT("{USERSETTINGS}Unreal Engine/Engine/Config/User{TYPE}.ini"), EConfigLayerFlags::NoExpand },
	// UserDir/.../User*.ini
	{ TEXT("UserDir"),					TEXT("{USER}Unreal Engine/Engine/Config/User{TYPE}.ini"), EConfigLayerFlags::NoExpand },
	// Project/User*.ini
	{ TEXT("GameDirUser"),				TEXT("{PROJECT}/Config/User{TYPE}.ini"), EConfigLayerFlags::GenerateCacheKey | EConfigLayerFlags::NoExpand },
};

文件读取流程

初始化流程

读进来的ini配置文件都被缓存在全局变量GConfig里。
【UE4】引擎配置文件原理学习笔记-LMLPHP

文件读取流程

【UE4】引擎配置文件原理学习笔记-LMLPHP
其中主要的耗时在读配置和写配置这两个位置。

文件的写入

UE在进行了上述的层级遍历以后,会得到一份最后生效的配置文件。它将这个文件的内容写入Saved目录中。

只从Engine/Config中读取,子层级没有复写的文件,写入Engine/Saved/Config中。否则写入[ProjectName]/Saved/Config中。注意这边的写入只写有差异的部分。

对于某个property的写入,如果这个property的值与其class的CDO相同,则没有存储多份的必要,不会写入配置文件。

为了遍历以确保不存储多份相同信息,写操作内涵多个嵌套的for遍历。如果ini文件非常大,ConfigFile.Write这个操作可能会花费非常多的时间。

所以我们的第一个问题就解决了:带Saved的路径下文件是运行时写入的,不带Save的是原有配置,用于读取的。

  1. Engine/Config【原有配置】
  2. Engine/Saved/Config【引擎运行后写入】
  3. [ProjectName]/Config【原有配置】
  4. [ProjectName]/Saved/Config【项目运行后写入】

这里有一个疑问:为什么在初始化加载ini文件的时候还需要写入到saved?虽然UE在写入的操作内部进行了比较,如果跟自己父层级的内容没有不一样的地方,就不写入saved目录下。但初始化加载的时候理论上刚刚加载到GConfig的内容是不会被修改的

配置文件内容和GConfig结构

打开一个配置文件可以看到其结构:
【UE4】引擎配置文件原理学习笔记-LMLPHP
抽象出来一个ConfigFile的文件结构如下:
【UE4】引擎配置文件原理学习笔记-LMLPHP
GConfig也是完全按照这个结构来构造的:
【UE4】引擎配置文件原理学习笔记-LMLPHP
GConfig是一个以文件名作为key值的TMap<FString,FConfigFile>;FConfigFile是一个以Section名作为Key的TMap<FString,FConfigSection>,而FConfigSection则是每个Section下面的键值对。

12-09 14:41