UE资源热重载方案
问题说明:手游项目中资源热更是非常频繁的,很多项目会在运行过程中支持资源热更。
但是资源热更后,要不就强迫用户重启app,要不就app内重新启动。
方案说明:
接触Unreal后,我也一直在思考app内热重启的方案,这里给大家分享一下我的做法:
首先,热重启最重要问题就是:该如何释放呢?
释放主要方面:
1.释放UnLua
清理luastate,清理UnLua组件
2.UGameInstance释放,参考PIE的Play&Stop
主要是调用其Shutdown()接口进行清理,并不会释放UGameInstance对象
3.空场景释放资源,Unmount所有pak
4.最后通过GC进行UObject清理(分帧做两次强制GC,第二次GC是为了阻塞完成回收第一次GC收集的不可达对象)
为了减少资源的引用,然后通过GC尽可能多释放UObject
方案思路解释:
方案的核心主要是UGameInstance::Shutdown(),而为什么是UGameInstance呢?为什么不是更高级的UEngine或者是更低级的AGameMode呢?
首先,选择UGameInstance是因为参考的PIE的Play&Stop。从Unity转到Unreal的同学应该发现一个问题:
同样是PIE(Play in Editor),Unity得益于.Net平台在每次Play都可以新建一个完全独立的虚拟机进行代码托管,而Unreal使用C++每次Play都是同一个执行环境。使用Unity开发基本不需要考虑Stop后的清理释放流程,而Unreal则需要谨慎处理Stop时数据缓存和委托监听等清理释放问题。
万事都有双面性,虽然这这点上Unreal没有Unity那样方便,但也得益于Unreal这个问题,促使我们在开发中要保证Stop时进行充分的清理,否则开发阶段就会出现问题。
由此,也可以得出,参考PIE的Stop&Play过程是不是也可以实现我们App中的Stop&Play了呢。
"怎么来的"(分析PIE的Stop流程,比较啰嗦,可以直接跳到后面怎么来的分割线):
调试PIE的Stop,就发现最终就会执行到UGameInstance::Shutdown()。
那我们就仔细看看PIE的Stop做了什么.
Stop后,实际是调用了RequestEndPlayMap
查看RequestEndPlayMap的代码,发现主要是设置了bRequestEndPlayMapQueued=true
至于这个bRequestEndPlayMapQueued会触发什么逻辑呢?
1.暂停播放音频,只是暂停,不是释放,所以我们不关心
2.调用EndPlayMap
好了,重点看一下EndPlayMap,篇幅有限,省略部分部分代码,下面以中文注释进行解释:
void UEditorEngine::EndPlayMap()
{
FlushAsyncLoading();//阻塞完成所有异步加载请求------[必要]
//VR,AR,MR的重置操作,这只是PIE的Stop需要--------------[不需要]
if (GEngine->XRSystem.IsValid() && !bIsSimulatingInEditor)
{
GEngine->XRSystem->OnEndPlay(*GEngine->GetWorldContextFromWorld(PlayWorld));
}
//清理Viewport,这只是PIE的Stop需要,热重启并不希望-----[不需要]
// clean up any previous Play From Here sessions
if ( GameViewport != NULL && GameViewport->Viewport != NULL )
{
// Remove debugger commands handler binding
GameViewport->OnGameViewportInputKey().Unbind();
// Remove close handler binding
GameViewport->OnCloseRequested().Remove(ViewportCloseRequestedDelegateHandle);
GameViewport->CloseRequested(GameViewport->Viewport);
}
CleanupGameViewport();
//清理所有World,热重启并不希望清理当前World-----[不需要]
// Clean up each world individually
TArray<FName> OnlineIdentifiers;
TArray<UWorld*> WorldsBeingCleanedUp;
bool bSeamlessTravelActive = false;
for (int32 WorldIdx = WorldList.Num()-1; WorldIdx >= 0; --WorldIdx)
{
FWorldContext &ThisContext = WorldList;
if (ThisContext.WorldType == EWorldType::PIE)
{
if (ThisContext.World())
{
WorldsBeingCleanedUp.Add(ThisContext.World());
}
if (ThisContext.SeamlessTravelHandler.IsInTransition())
{
bSeamlessTravelActive = true;
}
if (ThisContext.World())
{
//但是这里有个例外,这里使用WorldContext进行TeardownPlaySession,
//WorldContext在UE里主要是代表一个LocalPlayer,
//做的事情可能并不止于World内,
//所以需要留意,后面会展开解释 --------------------------- [必要]
TeardownPlaySession(ThisContext);
ShutdownWorldNetDriver(ThisContext.World());
}
// Cleanup online subsystems instantiated during PIE
FName OnlineIdentifier = UOnlineEngineInterface::Get()->GetOnlineIdentifier(ThisContext);
if (UOnlineEngineInterface::Get()->DoesInstanceExist(OnlineIdentifier))
{
// Stop ticking and clean up, but do not destroy as we may be in a failed online delegate
UOnlineEngineInterface::Get()->ShutdownOnlineSubsystem(OnlineIdentifier);
OnlineIdentifiers.Add(OnlineIdentifier);
}
// Remove world list after online has shutdown in case any async actions require the world context
WorldList.RemoveAt(WorldIdx);
}
}
// SeamlessTravel相关,我们热重启的时候会切换到一个稳定的空场景,
//所以不需要考虑SeamlessTravel ----- [不需要]
// If seamless travel is happening then there is likely additional PIE worlds that need tearing down so seek them out
if (bSeamlessTravelActive)
{
for (TObjectIterator<UWorld> WorldIt; WorldIt; ++WorldIt)
{
if (WorldIt->IsPlayInEditor())
{
WorldsBeingCleanedUp.AddUnique(*WorldIt);
}
}
}
//清理World的时候产生的OnlineIdentifiers,所以不考虑---- [不需要]
if (OnlineIdentifiers.Num())
{
UE_LOG(LogPlayLevel, Display, TEXT(&#34;Shutting down PIE online subsystems&#34;));
// Cleanup online subsystem shortly as we might be in a failed delegate
// have to do this in batch because timer delegate doesn&#39;t recognize bound data
// as a different delegate
FTimerDelegate DestroyTimer;
DestroyTimer.BindUObject(this, &UEditorEngine::CleanupPIEOnlineSessions, OnlineIdentifiers);
GetTimerManager()->SetTimer(CleanupPIEOnlineSessionsTimerHandle, DestroyTimer, 0.1f, false);
}
//广播EndPlayMap委托事件,因为我们是模拟Stop,并不想在PIE的时候真的触发了EndPlay ----- [不需要]
FGameDelegates::Get().GetEndPlayMapDelegate().Broadcast();
//清理outer带PKG_PlayInEditor标签的standalone UObject--- [不需要]
// find objects like Textures in the playworld levels that won&#39;t get garbage collected as they are marked RF_Standalone
for (FObjectIterator It; It; ++It)
{
UObject* Object = *It;
if (Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
{
if (Object->HasAnyFlags(RF_Standalone))
{
// Clear RF_Standalone flag from objects in the levels used for PIE so they get cleaned up.
Object->ClearFlags(RF_Standalone);
}
// Close any asset editors that are currently editing this object
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->CloseAllEditorsForAsset(Object);
}
}
//不是释放逻辑,我们不关心 ---- [不需要]
EditorWorld->bAllowAudioPlayback = true;
EditorWorld = nullptr;
//清理World带来的逻辑 --------------- [不需要]
// mark everything contained in the PIE worlds to be deleted
for (UWorld* World : WorldsBeingCleanedUp)
{
// Occasionally during seamless travel the Levels array won&#39;t yet be populated so mark this world first
// then pick up the sub-levels via the level iterator
World->MarkObjectsPendingKill();
// Because of the seamless travel the world might still be in the root set too, so also clear that
World->RemoveFromRoot();
for (auto LevelIt(World->GetLevelIterator()); LevelIt; ++LevelIt)
{
if (const ULevel* Level = *LevelIt)
{
// We already picked up the persistent level with the top level mark objects
if (Level->GetOuter() != World)
{
CastChecked<UWorld>(Level->GetOuter())->MarkObjectsPendingKill();
}
}
}
for (ULevelStreaming* LevelStreaming : World->GetStreamingLevels())
{
// If an unloaded levelstreaming still has a loaded level we need to mark its objects to be deleted as well
if (LevelStreaming->GetLoadedLevel() && (!LevelStreaming->ShouldBeLoaded() || !LevelStreaming->ShouldBeVisible()))
{
CastChecked<UWorld>(LevelStreaming->GetLoadedLevel()->GetOuter())->MarkObjectsPendingKill();
}
}
}
//强制清理GameInstance中的所有UObject,我们只是考虑模拟Shutdown,并不需要强制清理 --- [不需要]
// mark all objects contained within the PIE game instances to be deleted
for (TObjectIterator<UGameInstance> It; It; ++It)
{
auto MarkObjectPendingKill = [](UObject* Object)
{
Object->MarkPendingKill();
};
ForEachObjectWithOuter(*It, MarkObjectPendingKill, true, RF_NoFlags, EInternalObjectFlags::PendingKill);
}
// 清理Slate的所有渲染状态,由于是在稳定的空场景里面,展示的UI也是不需要释放的,所以 --- [不需要]
// Flush any render commands and released accessed UTextures and materials to give them a chance to be collected.
if ( FSlateApplication::IsInitialized() )
{
FSlateApplication::Get().FlushRenderState();
}
//只是做一下检查,检查结果打印一下而已,不涉及到释放---- [不需要]
// Make sure that all objects in the temp levels were entirely garbage collected.
for( FObjectIterator ObjectIt; ObjectIt; ++ObjectIt )
{
UObject* Object = *ObjectIt;
if( Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
{
UWorld* TheWorld = UWorld::FindWorldInPackage(Object->GetOutermost());
if ( TheWorld )
{
StaticExec(nullptr, *FString::Printf(TEXT(&#34;OBJ REFS CLASS=WORLD NAME=%s&#34;), *TheWorld->GetPathName()));
}
else
{
UE_LOG(LogPlayLevel, Error, TEXT(&#34;No PIE world was found when attempting to gather references after GC.&#34;));
}
FReferenceChainSearch RefChainSearch(Object, EReferenceChainSearchMode::Shortest);
FFormatNamedArguments Arguments;
Arguments.Add(TEXT(&#34;Path&#34;), FText::FromString(RefChainSearch.GetRootPath()));
// We cannot safely recover from this.
FMessageLog(NAME_CategoryPIE).CriticalError()
->AddToken(FUObjectToken::Create(Object, FText::FromString(Object->GetFullName())))
->AddToken(FTextToken::Create(FText::Format(LOCTEXT(&#34;PIEObjectStillReferenced&#34;, &#34;Object from PIE level still referenced. Shortest path from root: {Path}&#34;), Arguments)));
}
}
// Final cleanup/reseting
FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
UPackage* Package = EditorWorldContext.World()->GetOutermost();
// 重置Stereo状态,不涉及到释放--- [不需要]
//ensure stereo rendering is disabled in case we need to re-enable next PIE run (except when the editor is running in VR)
bool bInVRMode = IVREditorModule::Get().IsVREditorModeActive();
if (GEngine->StereoRenderingDevice && !bInVRMode)
{
GEngine->StereoRenderingDevice->EnableStereo(false);
}
//热重启不应该涉及到Viewport的清理,所以不需要 ---- [不需要]
// Restores realtime viewports that have been disabled for PIE.
RemoveViewportsRealtimeOverride();
//重新注册音频组件,不涉及到释放--- [不需要]
for(TObjectIterator<UAudioComponent> It; It; ++It)
{
UAudioComponent* AudioComp = *It;
if (AudioComp->GetWorld() == EditorWorldContext.World())
{
AudioComp->ReregisterComponent();
}
}
//清理所有的Play回话,热重启不需要--- [不需要]
// no longer queued
CancelRequestPlaySession();
bRequestEndPlayMapQueued = false;
}最后总结需要在热重启里面做只剩下两个:
1.FlushAsyncLoading()
2.TeardownPlaySession(ThisContext);
其中FlushAsyncLoading()就不多说了,继续研究一下TeardownPlaySession:
void UEditorEngine::TeardownPlaySession(FWorldContext& PieWorldContext)
{
//销毁World,因为热重启会在一个稳定的空场景里,所以不需要销毁World ---[不需要]
check(PieWorldContext.WorldType == EWorldType::PIE);
PlayWorld = PieWorldContext.World();
PlayWorld->BeginTearingDown();
if (!PieWorldContext.RunAsDedicated)
{
// Slate data for this pie world
FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
//销毁ViewPort,热重启不希望销毁Viewport,所以不需要 ----[不需要]
// Destroy Viewport
if ( PieWorldContext.GameViewport != NULL && PieWorldContext.GameViewport->Viewport != NULL )
{
PieWorldContext.GameViewport->CloseRequested(PieWorldContext.GameViewport->Viewport);
}
CleanupGameViewport();
//清理PIEviewport -------------------------------[不需要]
// Clean up the slate PIE viewport if we have one
if (SlatePlayInEditorSession)
{
if (SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
{
TSharedPtr<IAssetViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
if(PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::PlayInEditor)
{
// Set the editor viewport location to match that of Play in Viewport if we aren&#39;t simulating in the editor, we have a valid player to get the location from (unless we&#39;re going back to VR Editor, in which case we won&#39;t teleport the user.)
if (bLastViewAndLocationValid == true && !GEngine->IsStereoscopic3D( Viewport->GetActiveViewport() ) )
{
bLastViewAndLocationValid = false;
Viewport->GetAssetViewportClient().SetViewLocation( LastViewLocation );
if( Viewport->GetAssetViewportClient().IsPerspective() )
{
// Rotation only matters for perspective viewports not orthographic
Viewport->GetAssetViewportClient().SetViewRotation( LastViewRotation );
}
}
}
// No longer simulating in the viewport
Viewport->GetAssetViewportClient().SetIsSimulateInEditorViewport( false );
FEditorModeRegistry::Get().UnregisterMode(FBuiltinEditorModes::EM_Physics);
// Clear out the hit proxies before GC&#39;ing
Viewport->GetAssetViewportClient().Viewport->InvalidateHitProxy();
}
else if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
{
// Unregister the game viewport from slate.This sends a final message to the viewport
// so it can have a chance to release mouse capture, mouse lock, etc.
FSlateApplication::Get().UnregisterGameViewport();
// Viewport client is cleaned up.Make sure its not being accessed
SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->SetViewportClient(NULL);
// The window may have already been destroyed in the case that the PIE window close box was pressed
if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
{
// Destroy the SWindow
FSlateApplication::Get().DestroyWindowImmediately(SlatePlayInEditorSession->SlatePlayInEditorWindow.Pin().ToSharedRef());
}
}
}
//LocalPlayer是玩家实例,代表一个本地玩家 ---- [不需要]
// Disassociate the players from their PlayerControllers.
// This is done in the GameEngine path in UEngine::LoadMap.
// But since PIE is just shutting down, and not loading a
// new map, we need to do it manually here for now.
//for (auto It = GEngine->GetLocalPlayerIterator(PlayWorld); It; ++It)
for (FLocalPlayerIterator It(GEngine, PlayWorld); It; ++It)
{
if(It->PlayerController)
{
if(It->PlayerController->GetPawn())
{
PlayWorld->DestroyActor(It->PlayerController->GetPawn(), true);
}
PlayWorld->DestroyActor(It->PlayerController, true);
It->PlayerController = NULL;
}
}
}
// Change GWorld to be the play in editor world during cleanup.
ensureMsgf( EditorWorld == GWorld, TEXT(&#34;TearDownPlaySession current world: %s&#34;), GWorld ? *GWorld->GetName() : TEXT(&#34;No World&#34;));
GWorld = PlayWorld;
GIsPlayInEditorWorld = true;
// Remember Simulating flag so that we know if OnSimulateSessionFinished is required after everything has been cleaned up.
bool bWasSimulatingInEditor = PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor;
//重置音频 --------------------------------------------------------- [不需要]
// Stop all audio and remove references to temp level.
if (FAudioDevice* AudioDevice = PlayWorld->GetAudioDeviceRaw())
{
AudioDevice->Flush(PlayWorld);
AudioDevice->ResetInterpolation();
AudioDevice->OnEndPIE(false); // TODO: Should this have been bWasSimulatingInEditor?
AudioDevice->SetTransientMasterVolume(1.0f);
}
//LevelStreaming相关,热重启所在的场景是空场景,不会有LevelStreaming --- [不需要]
// Clean up all streaming levels
PlayWorld->bIsLevelStreamingFrozen = false;
PlayWorld->SetShouldForceUnloadStreamingLevels(true);
PlayWorld->FlushLevelStreaming();
//LevelStreaming相关,热重启所在的场景是空场景,不会有LevelStreaming --- [不需要]
// cleanup refs to any duplicated streaming levels
for ( int32 LevelIndex=0; LevelIndex<PlayWorld->GetStreamingLevels().Num(); LevelIndex++ )
{
ULevelStreaming* StreamingLevel = PlayWorld->GetStreamingLevels();
if( StreamingLevel != NULL )
{
const ULevel* PlayWorldLevel = StreamingLevel->GetLoadedLevel();
if ( PlayWorldLevel != NULL )
{
UWorld* World = Cast<UWorld>( PlayWorldLevel->GetOuter() );
if( World != NULL )
{
// Attempt to move blueprint debugging references back to the editor world
if( EditorWorld != NULL && EditorWorld->GetStreamingLevels().IsValidIndex(LevelIndex) )
{
const ULevel* EditorWorldLevel = EditorWorld->GetStreamingLevels()->GetLoadedLevel();
if ( EditorWorldLevel != NULL )
{
UWorld* SublevelEditorWorld= Cast<UWorld>(EditorWorldLevel->GetOuter());
if( SublevelEditorWorld != NULL )
{
World->TransferBlueprintDebugReferences(SublevelEditorWorld);
}
}
}
}
}
}
}
//Editor相关 -------------------------------- [不需要]
// Construct a list of editors that are active for objects being debugged. We will refresh these when we have cleaned up to ensure no invalid objects exist in them
TArray< IBlueprintEditor* > Editors;
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
const UWorld::FBlueprintToDebuggedObjectMap& EditDebugObjectsPre = PlayWorld->GetBlueprintObjectsBeingDebugged();
for (UWorld::FBlueprintToDebuggedObjectMap::TConstIterator EditIt(EditDebugObjectsPre); EditIt; ++EditIt)
{
if (UBlueprint* TargetBP = EditIt.Key().Get())
{
if(IBlueprintEditor* EachEditor = static_cast<IBlueprintEditor*>(AssetEditorSubsystem->FindEditorForAsset(TargetBP, false)))
{
Editors.AddUnique( EachEditor );
}
}
}
//通知Actor即将销毁,热重启场景是一个稳定的空场景,不存在需要销毁的Actor----- [不需要]
// Go through and let all the PlayWorld Actor&#39;s know they are being destroyed
for (FActorIterator ActorIt(PlayWorld); ActorIt; ++ActorIt)
{
ActorIt->RouteEndPlay(EEndPlayReason::EndPlayInEditor);
}
//GameInstance::Shutdown ---------------- [需要]
PieWorldContext.OwningGameInstance->Shutdown();
//Editor相关 --------- [不需要]
// Move blueprint debugging pointers back to the objects in the editor world
PlayWorld->TransferBlueprintDebugReferences(EditorWorld);
//物理系统的虚拟场景清理,由于我们不会清理当前场景 ----[不需要]
FPhysScene* PhysScene = PlayWorld->GetPhysicsScene();
if (PhysScene)
{
PhysScene->WaitPhysScenes();
PhysScene->KillVisualDebugger();
}
//清理场景,热重启的空场景不需要清理 ------[不需要]
// Clean up the temporary play level.
PlayWorld->CleanupWorld();
// Remove from root (Seamless travel may have done this)
PlayWorld->RemoveFromRoot();
PlayWorld = NULL;
//Editor相关 ---------------------------------------------------------------[不需要]
// Refresh any editors we had open in case they referenced objects that no longer exist.
for (int32 iEditors = 0; iEditors <Editors.Num(); iEditors++)
{
Editors[ iEditors ]->RefreshEditors();
}
// Restore GWorld.
GWorld = EditorWorld;
GIsPlayInEditorWorld = false;
FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
//依然是清理Viewport,不需要清理Viewport -------- [不需要]
// Let the viewport know about leaving PIE/Simulate session. Do it after everything&#39;s been cleaned up
// as the viewport will play exit sound here and this has to be done after GetAudioDevice()->Flush
// otherwise all sounds will be immediately stopped.
if (!PieWorldContext.RunAsDedicated)
{
// Slate data for this pie world
FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
if (SlatePlayInEditorSession && SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
{
TSharedPtr<IAssetViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
if( Viewport->HasPlayInEditorViewport() )
{
Viewport->EndPlayInEditorSession();
}
// Let the Slate viewport know that we&#39;re leaving Simulate mode
if( bWasSimulatingInEditor )
{
Viewport->OnSimulateSessionFinished();
}
StaticCast<FLevelEditorViewportClient&>(Viewport->GetAssetViewportClient()).SetReferenceToWorldContext(EditorWorldContext);
}
// Remove the slate info from the map (note that the UWorld* is long gone at this point, but the WorldContext still exists. It will be removed outside of this function)
SlatePlayInEditorMap.Remove(PieWorldContext.ContextHandle);
}
}看来TeardownPlaySession中只有OwningGameInstance->Shutdown();是我们关心的。
最终总结一下,模拟PIE的Stop流程中需要注意的只有两个调用:
1.FlushAsyncLoading().
2.OwningGameInstance->Shutdown();
而FlushAsyncLoading()是阻塞加载所有异步请求没什么好说的,我们就主要看看UGameInstance::Shutdown()
-------------------------------&#34;怎么来的&#34; 结束分割线----------------------------------------
&#34;怎么做呢&#34;:
让我们来看看UGameInstance::Shutdown():
注意看中文注释(忽略误选中的选中框)
再对比一下UGameInstance::Init()
注意看中文注释(忽略误选中的选中框)
对比:
Shutdown:Init:1.通知蓝图Shutdown1.通知蓝图Init2.清理网络回话对象2.创建网络回话对象3.清理所有LocalPlayer3.XXXXXXXXXXXXX4.清理所有GameInstanceSubsystem4.初始化所有GameInstanceSubsystem5.清理网路错误委托监听5.绑定网络错误委托监听6.清理WorldContext6.XXXXXXXXXXXX可见,Shutdown相对Init多了:清理所有LocalPlayer,清理WorldContext。什么概念呢? 就是Shutdown做了些事情,而且是在再次Init后都没办法恢复。
1.清理LocalPlayer: LocalPlayer代表一个用户端,一般手游都是只有一个LocalPlayer,主机游戏可能会通过分屏方式出现两个或者四个LocalPlayer。清理LocalPlayer后如果不进行恢复,用户端的输入将无效,包括Console Command.
2.清理WorldContext: WorldContext即是World上下文,与UWorld有关系,并且会影响到一些需要区别多个LocalPlayer的接口.
这个时候,有人可能会问&#34;既然是模拟Shutdown,是不是可以直接把Shutdown逻辑拷贝一下就行?&#34;,当然这也是一个办法。但是每个项目都会继承UGameInstance,那每层继承都要拷贝,修改的时候还要注意修改两处,维护起来是很麻烦。所以使用UGameInstance的Shutdown和Init会更方便维护。
下面是模拟逻辑ShutdownOnlyForReload和InitOnlyForReload,最终都会调用到Shutdown和Init.
void UMGameInstance::ShutdownOnlyForReload()
{
UE_LOG(LogTemp,Log,TEXT(&#34;UMGameInstance::ShutdownOnlyForReload....Start&#34;));
FWorldContext* WorldContextOld = WorldContext;//记录一下WorldContext,Shutdown中置空后再复制回去即可.
Shutdown(); //调用UMGameInstance::Shutdown,在里面还会进行UnLua释放
GameInstance = this;
WorldContext = WorldContextOld; //还原WorldContext
FString OutError;
CreateInitialPlayer(OutError); //恢复LocalPlayer
UE_LOG(LogTemp,Log,TEXT(&#34;UMGameInstance::ShutdownOnlyForReload....End&#34;));
}
void UMGameInstance::InitOnlyForReload()
{
UE_LOG(LogTemp,Log,TEXT(&#34;UMGameInstance::InitOnlyForReload....Start&#34;));
Init(); //调用UMGameInstance::Init
FBasicMainEntry::InitLua(GetWorld()); //初始化Lua.
UE_LOG(LogTemp,Log,TEXT(&#34;UMGameInstance::InitOnlyForReload....End&#34;));
}
&#34;怎么验证&#34;
1.Lua: Shutdown过程会把Lua_State释放,所以Lua里的数据Cache和UObject引用都会释放掉了.
2.非UObject实例/静态实例: 这里并没有验证方法,但是文章开头就说了,模拟PIE的Stop&Play流程,开发阶段就要保证重复Stop&Play能正常运作的,这是很正常的项目规范要求。加上非UObject实例/静态对象都不会影响UObject的GC。而回归到需求,只是进行资源重新加载,所以不需要保证这些对象都被清理干净,只需要保证重复Stop&Play正常工作即可。
3.UObject实例: 在Shutdown和UnMount所有资源后,中止重启流程,遍历打印所有的UObject,并人肉检查UObject的释放情况。遍历打印代码如下:
//bNeedClass 是否显示Class并参与排序
//bNoReflectionObj 是否不收集UFunction/UClass/UStruct这些UObject
//bOnlyContent 是否只打印工程资源.
void DumpAllObj(bool bNeedClass,bool bNoReflectionObj,bool bOnlyContent)
{
TArray<FString> strObjs;
for (TObjectIterator<UObject> It; It; ++It)
{
UObject* Obj = *It;
if (!bNoReflectionObj || (Cast<UField>(Obj) == nullptr
#if ENGINE_MINOR_VERSION < 25
&& Cast<UProperty>(Obj) == nullptr
#endif
&& Cast<UFunction>(Obj) == nullptr
&& Cast<UClass>(Obj) == nullptr
&& Cast<UStruct>(Obj) == nullptr))
{
if (!bOnlyContent || (Obj->GetPathName().StartsWith(&#34;/Game/&#34;)))
{
if (bNeedClass)
strObjs.Add(Obj->GetFullName());
else
strObjs.Add(Obj->GetPathName());
}
}
}
strObjs.Sort();
FString DumpLogFile = FString::Format(TEXT(&#34;{0}/DumpAllObj_{1}_{2}_{3}.txt&#34;), { FPaths::ConvertRelativePathToFull(FPaths::ProjectPersistentDownloadDir()),bNeedClass,bNoReflectionObj,bOnlyContent });
FFileHelper::SaveStringArrayToFile(strObjs, *DumpLogFile, FFileHelper::EEncodingOptions::ForceUTF8);
}
&#34;怎么解决&#34;:
验证后,发现问题该怎么解决呢?
非UObject问题: 主要是通过项目规范进行约束。
UObject问题: 可能会打印出来上万个没有正常释放的UObject,如果逐一人肉排查引用关系,是很恐怖的事。这里有个小经验,可以先从引用集合UObject开始排查,像DataTable和DataAsset,因为UObject会带来很多引用,释放掉DataTable,UObject数量能大幅度减少。
查找引用代码:
void PrintAllReferencers(FString ObjName)
{
TMap<UObject*,FObjReferencers> AllReferencers = GetAllReferencers(ObjName);
UE_LOG(LogTemp,Log,TEXT(&#34;---------Start Print All References of %s&#34;),*ObjName);
for(TMap<UObject*,FObjReferencers>::TIterator Iter = AllReferencers.CreateIterator();Iter;++Iter)
{
UObject* ReferenceeObj = Iter->Key;
UE_LOG(LogTemp,Log,TEXT(&#34;Referencers Of %s (%s):&#34;),*(ReferenceeObj->GetFName().ToString()),*(ReferenceeObj->GetClass()->GetFName().ToString()));
FObjReferencers ReferencersInfo = Iter->Value;
for(TArray<UObject*>::TIterator Iter2 = ReferencersInfo.Referencers.CreateIterator();Iter2;++Iter2)
{
UObject* ReferencerObj =*Iter2;
UE_LOG(LogTemp,Log,TEXT(&#34;%30s (%s)&#34;),*(ReferencerObj->GetFName().ToString()),*(ReferencerObj->GetClass()->GetFName().ToString()));
}
UE_LOG(LogTemp,Log,TEXT(&#34;--------------------------&#34;));
}
UE_LOG(LogTemp,Log,TEXT(&#34;---------End Print All References of %s&#34;),*ObjName);
}
--------------------------------------------------------------------------------------------
这个方案已经在项目里使用,为了增加测试的机会,我把游戏账号登出也改成了重启XD。
页:
[1]