|
问题说明:
手游项目中资源热更是非常频繁的,很多项目会在运行过程中支持资源热更。
但是资源热更后,要不就强迫用户重启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[WorldIdx];
- 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()[LevelIndex];
- 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()[LevelIndex]->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.通知蓝图Shutdown | 1.通知蓝图Init | 2.清理网络回话对象 | 2.创建网络回话对象 | 3.清理所有LocalPlayer | 3.XXXXXXXXXXXXX | 4.清理所有GameInstanceSubsystem | 4.初始化所有GameInstanceSubsystem | 5.清理网路错误委托监听 | 5.绑定网络错误委托监听 | 6.清理WorldContext | 6.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。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|