刚好遇到一个ini加载的优化。趁此机会记录学习一下UE引擎的config文件层级结构和读取流程
两个问题
在看项目目录结构的时候,有没有这样的疑问:
为什么saved同级目录下面有一个config,saved里面还有一个config:saved/config?它们有什么区别吗?
看虚幻读配置文件的源码:发现它读取配置文件用的是一个LoadExternalIniFile
的函数。但这个函数执行了读操作以外,还进行了写操作。为什么虚幻加载配置文件的以后还要写文件?
层级结构
实际上,UE采用的config配置文件读取策略是以层级的形式,可以分为以下四个层级:
- Engine/Config
- Engine/Saved/Config
- [ProjectName]/Config
- [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里。
文件读取流程
其中主要的耗时在读配置和写配置这两个位置。
文件的写入
UE在进行了上述的层级遍历以后,会得到一份最后生效的配置文件。它将这个文件的内容写入Saved目录中。
只从Engine/Config中读取,子层级没有复写的文件,写入Engine/Saved/Config中。否则写入[ProjectName]/Saved/Config中。注意这边的写入只写有差异的部分。
对于某个property的写入,如果这个property的值与其class的CDO相同,则没有存储多份的必要,不会写入配置文件。
为了遍历以确保不存储多份相同信息,写操作内涵多个嵌套的for
遍历。如果ini文件非常大,ConfigFile.Write
这个操作可能会花费非常多的时间。
所以我们的第一个问题就解决了:带Saved的路径下文件是运行时写入的,不带Save的是原有配置,用于读取的。
- Engine/Config【原有配置】
- Engine/Saved/Config【引擎运行后写入】
- [ProjectName]/Config【原有配置】
- [ProjectName]/Saved/Config【项目运行后写入】
这里有一个疑问:为什么在初始化加载ini文件的时候还需要写入到saved?虽然UE在写入的操作内部进行了比较,如果跟自己父层级的内容没有不一样的地方,就不写入saved目录下。但初始化加载的时候理论上刚刚加载到GConfig的内容是不会被修改的
配置文件内容和GConfig结构
打开一个配置文件可以看到其结构:
抽象出来一个ConfigFile的文件结构如下:
GConfig也是完全按照这个结构来构造的:
GConfig是一个以文件名作为key值的TMap<FString,FConfigFile>
;FConfigFile是一个以Section名作为Key的TMap<FString,FConfigSection>
,而FConfigSection则是每个Section下面的键值对。