UE 代码分析:GConfig 的加载

UE4 中提供了一套非常成熟的 INI 文件配置机制,引擎中也使用了 ini 作为引擎和项目的配置文件。本篇文章来简单分析一下引擎中 GConfig 的加载。

UE 中定义了一堆的全局 ini:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Runtime/Core/Private/Misc/CoreGlobals.cpp
FString GEngineIni; /* Engine ini filename */

/** Editor ini file locations - stored per engine version (shared across all projects). Migrated between versions on first run. */
FString GEditorIni; /* Editor ini filename */
FString GEditorKeyBindingsIni; /* Editor Key Bindings ini file */
FString GEditorLayoutIni; /* Editor UI Layout ini filename */
FString GEditorSettingsIni; /* Editor Settings ini filename */

/** Editor per-project ini files - stored per project. */
FString GEditorPerProjectIni; /* Editor User Settings ini filename */

FString GCompatIni;
FString GLightmassIni; /* Lightmass settings ini filename */
FString GScalabilityIni; /* Scalability settings ini filename */
FString GHardwareIni; /* Hardware ini filename */
FString GInputIni; /* Input ini filename */
FString GGameIni; /* Game ini filename */
FString GGameUserSettingsIni; /* User Game Settings ini filename */

但是并没有直接硬编码指定 ini 是在哪里的,其加载的过程为:在 FEngineLoop::AppInit(LaunchEngineLoop.cpp) 中通过调用 FConfigCacheIni::InitializeConfigSystem 来执行加载 ini 文件,其定义在ConfigCacheIni.cpp

FConfigCacheIni::InitializeConfigSystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// --------------------------------
// # FConfigCacheIni::InitializeConfigSystem declaration
/**
* Creates GConfig, loads the standard global ini files (Engine, Editor, etc),
* fills out GEngineIni, etc. and marks GConfig as ready for use
*/
static void InitializeConfigSystem();
// --------------------------------
void FConfigCacheIni::InitializeConfigSystem()
{
// Perform any upgrade we need before we load any configuration files
FConfigManifest::UpgradeFromPreviousVersions();

// create GConfig
GConfig = new FConfigCacheIni(EConfigCacheType::DiskBacked);

// load the main .ini files (unless we're running a program or a gameless UE4Editor.exe, DefaultEngine.ini is required).
const bool bIsGamelessExe = !FApp::HasProjectName();
const bool bDefaultEngineIniRequired = !bIsGamelessExe && (GIsGameAgnosticExe || FApp::IsProjectNameEmpty());
bool bEngineConfigCreated = FConfigCacheIni::LoadGlobalIniFile(GEngineIni, TEXT("Engine"), nullptr, bDefaultEngineIniRequired);

if (!bIsGamelessExe)
{
// Now check and see if our game is correct if this is a game agnostic binary
if (GIsGameAgnosticExe && !bEngineConfigCreated)
{
const FText AbsolutePath = FText::FromString(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(GEngineIni)) );
//@todo this is too early to localize
const FText Message = FText::Format(NSLOCTEXT("Core", "FirstCmdArgMustBeGameName", "'{0}' must exist and contain a DefaultEngine.ini."), AbsolutePath );
if (!GIsBuildMachine)
{
FMessageDialog::Open(EAppMsgType::Ok, Message);
}
FApp::SetProjectName(TEXT("")); // this disables part of the crash reporter to avoid writing log files to a bogus directory
if (!GIsBuildMachine)
{
exit(1);
}
UE_LOG(LogInit, Fatal,TEXT("%s"), *Message.ToString());
}
}

FConfigCacheIni::LoadGlobalIniFile(GGameIni, TEXT("Game"));
FConfigCacheIni::LoadGlobalIniFile(GInputIni, TEXT("Input"));
#if WITH_EDITOR
// load some editor specific .ini files

FConfigCacheIni::LoadGlobalIniFile(GEditorIni, TEXT("Editor"));

// Upgrade editor user settings before loading the editor per project user settings
FConfigManifest::MigrateEditorUserSettings();
FConfigCacheIni::LoadGlobalIniFile(GEditorPerProjectIni, TEXT("EditorPerProjectUserSettings"));

// Project agnostic editor ini files
static const FString EditorSettingsDir = FPaths::Combine(*FPaths::GameAgnosticSavedDir(), TEXT("Config")) + TEXT("/");
FConfigCacheIni::LoadGlobalIniFile(GEditorSettingsIni, TEXT("EditorSettings"), nullptr, false, false, true, *EditorSettingsDir);
FConfigCacheIni::LoadGlobalIniFile(GEditorLayoutIni, TEXT("EditorLayout"), nullptr, false, false, true, *EditorSettingsDir);
FConfigCacheIni::LoadGlobalIniFile(GEditorKeyBindingsIni, TEXT("EditorKeyBindings"), nullptr, false, false, true, *EditorSettingsDir);

#endif
#if PLATFORM_DESKTOP
// load some desktop only .ini files
FConfigCacheIni::LoadGlobalIniFile(GCompatIni, TEXT("Compat"));
FConfigCacheIni::LoadGlobalIniFile(GLightmassIni, TEXT("Lightmass"));
#endif

// Load scalability settings.
FConfigCacheIni::LoadGlobalIniFile(GScalabilityIni, TEXT("Scalability"));
// Load driver blacklist
FConfigCacheIni::LoadGlobalIniFile(GHardwareIni, TEXT("Hardware"));

// Load user game settings .ini, allowing merging. This also updates the user .ini if necessary.
FConfigCacheIni::LoadGlobalIniFile(GGameUserSettingsIni, TEXT("GameUserSettings"));

// now we can make use of GConfig
GConfig->bIsReadyForUse = true;
FCoreDelegates::ConfigReadyForUse.Broadcast();
}

FConfigCacheIni::LoadGlobalIniFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// --------------------------------
// # FConfigCacheIni::LoadGlobalIniFile declaration
/**
* Loads and generates a destination ini file and adds it to GConfig:
* - Looking on commandline for override source/dest .ini filenames
* - Generating the name for the engine to refer to the ini
* - Loading a source .ini file hierarchy
* - Filling out an FConfigFile
* - Save the generated ini
* - Adds the FConfigFile to GConfig
*
* @param FinalIniFilename The output name of the generated .ini file (in Game\Saved\Config)
* @param BaseIniName The "base" ini name, with no extension (ie, Engine, Game, etc)
* @param Platform The platform to load the .ini for (if NULL, uses current)
* @param bForceReload If true, the destination .in will be regenerated from the source, otherwise this will only process if the dest isn't in GConfig
* @param bRequireDefaultIni If true, the Default*.ini file is required to exist when generating the final ini file.
* @param bAllowGeneratedIniWhenCooked If true, the engine will attempt to load the generated/user INI file when loading cooked games
* @param GeneratedConfigDir The location where generated config files are made.
* @return true if the final ini was created successfully.
*/
static bool LoadGlobalIniFile(FString& FinalIniFilename, const TCHAR* BaseIniName, const TCHAR* Platform=NULL, bool bForceReload=false, bool bRequireDefaultIni=false, bool bAllowGeneratedIniWhenCooked=true, const TCHAR* GeneratedConfigDir = *FPaths::GeneratedConfigDir());
// --------------------------------

bool FConfigCacheIni::LoadGlobalIniFile(FString& FinalIniFilename, const TCHAR* BaseIniName, const TCHAR* Platform, bool bForceReload, bool bRequireDefaultIni, bool bAllowGeneratedIniWhenCooked, const TCHAR* GeneratedConfigDir)
{
// figure out where the end ini file is
FinalIniFilename = GetDestIniFilename(BaseIniName, Platform, GeneratedConfigDir);

// Start the loading process for the remote config file when appropriate
if (FRemoteConfig::Get()->ShouldReadRemoteFile(*FinalIniFilename))
{
FRemoteConfig::Get()->Read(*FinalIniFilename, BaseIniName);
}

FRemoteConfigAsyncIOInfo* RemoteInfo = FRemoteConfig::Get()->FindConfig(*FinalIniFilename);
if (RemoteInfo && (!RemoteInfo->bWasProcessed || !FRemoteConfig::Get()->IsFinished(*FinalIniFilename)))
{
// Defer processing this remote config file to until it has finish its IO operation
return false;
}

// need to check to see if the file already exists in the GConfigManager's cache
// if it does exist then we are done, nothing else to do
if (!bForceReload && GConfig->FindConfigFile(*FinalIniFilename) != nullptr)
{
//UE_LOG(LogConfig, Log, TEXT( "Request to load a config file that was already loaded: %s"), GeneratedIniFile );
return true;
}

// make a new entry in GConfig (overwriting what's already there)
FConfigFile& NewConfigFile = GConfig->Add(FinalIniFilename, FConfigFile());

return LoadExternalIniFile(NewConfigFile, BaseIniName, *FPaths::EngineConfigDir(), *FPaths::SourceConfigDir(), true, Platform, bForceReload, true, bAllowGeneratedIniWhenCooked, GeneratedConfigDir);
}

FConfigCacheIni::LoadLocalIniFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// --------------------------------
// # FConfigCacheIni::LoadLocalIniFile declaration
/**
* Load an ini file directly into an FConfigFile, and nothing is written to GConfig or disk.
* The passed in .ini name can be a "base" (Engine, Game) which will be modified by platform and/or commandline override,
* or it can be a full ini filenname (ie WrangleContent) loaded from the Source config directory
*
* @param ConfigFile The output object to fill
* @param IniName Either a Base ini name (Engine) or a full ini name (WrangleContent). NO PATH OR EXTENSION SHOULD BE USED!
* @param bIsBaseIniName true if IniName is a Base name, which can be overridden on commandline, etc.
* @param Platform The platform to use for Base ini names, NULL means to use the current platform
* @param bForceReload force reload the ini file from disk this is required if you make changes to the ini file not using the config system as the hierarchy cache will not be updated in this case
* @return true if the ini file was loaded successfully
*/
static bool LoadLocalIniFile(FConfigFile& ConfigFile, const TCHAR* IniName, bool bIsBaseIniName, const TCHAR* Platform=NULL, bool bForceReload=false);
// --------------------------------
bool FConfigCacheIni::LoadLocalIniFile(FConfigFile& ConfigFile, const TCHAR* IniName, bool bIsBaseIniName, const TCHAR* Platform, bool bForceReload )
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FConfigCacheIni::LoadLocalIniFile" ), STAT_FConfigCacheIni_LoadLocalIniFile, STATGROUP_LoadTime );

FString EngineConfigDir = FPaths::EngineConfigDir();
FString SourceConfigDir = FPaths::SourceConfigDir();

if (bIsBaseIniName)
{
FConfigFile* BaseConfig = GConfig->FindConfigFileWithBaseName(IniName);
// If base ini, try to use an existing GConfig file to set the config directories instead of assuming defaults

if (BaseConfig)
{
FIniFilename* EngineFilename = BaseConfig->SourceIniHierarchy.Find(EConfigFileHierarchy::EngineDirBase);
if (EngineFilename)
{
EngineConfigDir = FPaths::GetPath(EngineFilename->Filename) + TEXT("/");
}

FIniFilename* GameFilename = BaseConfig->SourceIniHierarchy.Find(EConfigFileHierarchy::GameDirDefault);
if (GameFilename)
{
SourceConfigDir = FPaths::GetPath(GameFilename->Filename) + TEXT("/");
}
}

}

return LoadExternalIniFile(ConfigFile, IniName, *EngineConfigDir, *SourceConfigDir, bIsBaseIniName, Platform, bForceReload, false);
}

FConfigCacheIni::LoadExternalIniFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// --------------------------------
// # FConfigCacheIni::LoadExternalIniFile declaration
/**
* Load an ini file directly into an FConfigFile from the specified config folders, optionally writing to disk.
* The passed in .ini name can be a "base" (Engine, Game) which will be modified by platform and/or commandline override,
* or it can be a full ini filenname (ie WrangleContent) loaded from the Source config directory
*
* @param ConfigFile The output object to fill
* @param IniName Either a Base ini name (Engine) or a full ini name (WrangleContent). NO PATH OR EXTENSION SHOULD BE USED!
* @param EngineConfigDir Engine config directory.
* @param SourceConfigDir Game config directory.
* @param bIsBaseIniName true if IniName is a Base name, which can be overridden on commandline, etc.
* @param Platform The platform to use for Base ini names
* @param bForceReload force reload the ini file from disk this is required if you make changes to the ini file not using the config system as the hierarchy cache will not be updated in this case
* @param bWriteDestIni write out a destination ini file to the Saved folder, only valid if bIsBaseIniName is true
* @param bAllowGeneratedIniWhenCooked If true, the engine will attempt to load the generated/user INI file when loading cooked games
* @param GeneratedConfigDir The location where generated config files are made.
* @return true if the ini file was loaded successfully
*/
static bool LoadExternalIniFile(FConfigFile& ConfigFile, const TCHAR* IniName, const TCHAR* EngineConfigDir, const TCHAR* SourceConfigDir, bool bIsBaseIniName, const TCHAR* Platform=NULL, bool bForceReload=false, bool bWriteDestIni=false, bool bAllowGeneratedIniWhenCooked = true, const TCHAR* GeneratedConfigDir = *FPaths::GeneratedConfigDir());
// --------------------------------
bool FConfigCacheIni::LoadExternalIniFile(FConfigFile& ConfigFile, const TCHAR* IniName, const TCHAR* EngineConfigDir, const TCHAR* SourceConfigDir, bool bIsBaseIniName, const TCHAR* Platform, bool bForceReload, bool bWriteDestIni, bool bAllowGeneratedIniWhenCooked, const TCHAR* GeneratedConfigDir)
{
// if bIsBaseIniName is false, that means the .ini is a ready-to-go .ini file, and just needs to be loaded into the FConfigFile
if (!bIsBaseIniName)
{
// generate path to the .ini file (not a Default ini, IniName is the complete name of the file, without path)
FString SourceIniFilename = FString::Printf(TEXT("%s/%s.ini"), SourceConfigDir, IniName);

// load the .ini file straight up
LoadAnIniFile(*SourceIniFilename, ConfigFile);

ConfigFile.Name = IniName;
}
else
{
FString DestIniFilename = GetDestIniFilename(IniName, Platform, GeneratedConfigDir);

GetSourceIniHierarchyFilenames(IniName, Platform, EngineConfigDir, SourceConfigDir, ConfigFile.SourceIniHierarchy, false );

if (bForceReload)
{
ClearHierarchyCache(IniName);
}

// Keep a record of the original settings
ConfigFile.SourceConfigFile = new FConfigFile();

// now generate and make sure it's up to date (using IniName as a Base for an ini filename)
const bool bAllowGeneratedINIs = true;
bool bNeedsWrite = GenerateDestIniFile(ConfigFile, DestIniFilename, ConfigFile.SourceIniHierarchy, bAllowGeneratedIniWhenCooked, true);

ConfigFile.Name = IniName;

// don't write anything to disk in cooked builds - we will always use re-generated INI files anyway.
if (bWriteDestIni && (!FPlatformProperties::RequiresCookedData() || bAllowGeneratedIniWhenCooked)
// We shouldn't save config files when in multiprocess mode,
// otherwise we get file contention in XGE shader builds.
&& !FParse::Param(FCommandLine::Get(), TEXT("Multiprocess")))
{
// Check the config system for any changes made to defaults and propagate through to the saved.
ConfigFile.ProcessSourceAndCheckAgainstBackup();

if (bNeedsWrite)
{
// if it was dirtied during the above function, save it out now
ConfigFile.Write(DestIniFilename);
}
}
}

// GenerateDestIniFile returns true if nothing is loaded, so check if we actually loaded something
return ConfigFile.Num() > 0;
}

这个函数中最重要的部分就是调用了GetSourceIniHierarchyFilenames,它把当前传入的 baseName 的 ini 在 Engine 和项目下的所有 ini 文件都收集了起来。

这就是为什么我们看到其实 GConfig 中很多都是 Saved/Config 下的 ini,里面内容是空的,但是我们怎么查到项目里的设置的呢?这是因为 UE 把这么多个 ini 合并了:

这些 ini 的的内容都会加载进来。

GetDestIniFilename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Runtime/Source/Core/Private/Misc/ConfigCacheIni.cpp

/**
* Calculates the name of a dest (generated) .ini file for a given base (ie Engine, Game, etc)
*
* @param IniBaseName Base name of the .ini (Engine, Game)
* @param PlatformName Name of the platform to get the .ini path for (nullptr means to use the current platform)
* @param GeneratedConfigDir The base folder that will contain the generated config files.
*
* @return Standardized .ini filename
*/
static FString GetDestIniFilename(const TCHAR* BaseIniName, const TCHAR* PlatformName, const TCHAR* GeneratedConfigDir)
{
// figure out what to look for on the commandline for an override
FString CommandLineSwitch = FString::Printf(TEXT("%sINI="), BaseIniName);

// if it's not found on the commandline, then generate it
FString IniFilename;
if (FParse::Value(FCommandLine::Get(), *CommandLineSwitch, IniFilename) == false)
{
FString Name(PlatformName ? PlatformName : ANSI_TO_TCHAR(FPlatformProperties::PlatformName()));

FString BaseIniNameString = BaseIniName;
if (BaseIniNameString.Contains(GeneratedConfigDir))
{
IniFilename = BaseIniNameString;
}
else
{
// put it all together
IniFilename = FString::Printf(TEXT("%s%s/%s.ini"), GeneratedConfigDir, *Name, BaseIniName);
}
}

// standardize it!
FPaths::MakeStandardFilename(IniFilename);
return IniFilename;
}

FPaths::GeneratedConfigDir 获取的路径是当前项目的 Saved 下的 Config:

1
2
3
4
5
6
7
8
9
// Paths.cpp
FString FPaths::GeneratedConfigDir()
{
#if PLATFORM_MAC
return FPlatformProcess::UserPreferencesDir();
#else
return FPaths::ProjectSavedDir() + TEXT("Config/");
#endif
}

即在 Windows 平台上项目启动时创建的全局 G*Ini 文件读取的都是 Saved/Config/Windows 下的 .ini
而且在 GetDestIniFilename 的实现中也可以看到,G*Ini的配置也是可以从 CommandLine 传入的,可以替换掉默认的 Saved/Config/Platform 下的 ini:

1
2
// 使用指定的 Engine.ini
UE4Editor.exe uprojectPath -EngineINI="D:\\CustomEngine.ini"


扩展阅读