找回密码
 立即注册
查看: 565|回复: 0

UE资源热重载方案

[复制链接]
发表于 2021-4-28 09:18 | 显示全部楼层 |阅读模式
问题说明:

手游项目中资源热更是非常频繁的,很多项目会在运行过程中支持资源热更。
但是资源热更后,要不就强迫用户重启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,篇幅有限,省略部分部分代码,下面以中文注释进行解释:
  1. void UEditorEngine::EndPlayMap()
  2. {
  3.         FlushAsyncLoading();  //阻塞完成所有异步加载请求------[必要]
  4.         //VR,AR,MR的重置操作,这只是PIE的Stop需要--------------[不需要]
  5.         if (GEngine->XRSystem.IsValid() && !bIsSimulatingInEditor)
  6.         {
  7.                 GEngine->XRSystem->OnEndPlay(*GEngine->GetWorldContextFromWorld(PlayWorld));
  8.         }
  9.         
  10.         //清理Viewport,这只是PIE的Stop需要,热重启并不希望-----[不需要]
  11.         // clean up any previous Play From Here sessions
  12.         if ( GameViewport != NULL && GameViewport->Viewport != NULL )
  13.         {
  14.                 // Remove debugger commands handler binding
  15.                 GameViewport->OnGameViewportInputKey().Unbind();
  16.                 // Remove close handler binding
  17.                 GameViewport->OnCloseRequested().Remove(ViewportCloseRequestedDelegateHandle);
  18.                 GameViewport->CloseRequested(GameViewport->Viewport);
  19.         }
  20.         CleanupGameViewport();
  21.         //清理所有World,热重启并不希望清理当前World-----[不需要]
  22.         // Clean up each world individually
  23.         TArray<FName> OnlineIdentifiers;
  24.         TArray<UWorld*> WorldsBeingCleanedUp;
  25.         bool bSeamlessTravelActive = false;
  26.         for (int32 WorldIdx = WorldList.Num()-1; WorldIdx >= 0; --WorldIdx)
  27.         {
  28.                 FWorldContext &ThisContext = WorldList[WorldIdx];
  29.                 if (ThisContext.WorldType == EWorldType::PIE)
  30.                 {
  31.                         if (ThisContext.World())
  32.                         {
  33.                                 WorldsBeingCleanedUp.Add(ThisContext.World());
  34.                         }
  35.                         if (ThisContext.SeamlessTravelHandler.IsInTransition())
  36.                         {
  37.                                 bSeamlessTravelActive = true;
  38.                         }
  39.                         if (ThisContext.World())
  40.                         {
  41.                                 //但是这里有个例外,这里使用WorldContext进行TeardownPlaySession,
  42.                                 //WorldContext在UE里主要是代表一个LocalPlayer,
  43.                                 //做的事情可能并不止于World内,
  44.                                 //所以需要留意,后面会展开解释 --------------------------- [必要]
  45.                                 TeardownPlaySession(ThisContext);
  46.                                 ShutdownWorldNetDriver(ThisContext.World());
  47.                         }
  48.                         // Cleanup online subsystems instantiated during PIE
  49.                         FName OnlineIdentifier = UOnlineEngineInterface::Get()->GetOnlineIdentifier(ThisContext);
  50.                         if (UOnlineEngineInterface::Get()->DoesInstanceExist(OnlineIdentifier))
  51.                         {
  52.                                 // Stop ticking and clean up, but do not destroy as we may be in a failed online delegate
  53.                                 UOnlineEngineInterface::Get()->ShutdownOnlineSubsystem(OnlineIdentifier);
  54.                                 OnlineIdentifiers.Add(OnlineIdentifier);
  55.                         }
  56.                
  57.                         // Remove world list after online has shutdown in case any async actions require the world context
  58.                         WorldList.RemoveAt(WorldIdx);
  59.                 }
  60.         }
  61.         // SeamlessTravel相关,我们热重启的时候会切换到一个稳定的空场景,
  62.         //所以不需要考虑SeamlessTravel ----- [不需要]
  63.         // If seamless travel is happening then there is likely additional PIE worlds that need tearing down so seek them out
  64.         if (bSeamlessTravelActive)
  65.         {
  66.                 for (TObjectIterator<UWorld> WorldIt; WorldIt; ++WorldIt)
  67.                 {
  68.                         if (WorldIt->IsPlayInEditor())
  69.                         {
  70.                                 WorldsBeingCleanedUp.AddUnique(*WorldIt);
  71.                         }
  72.                 }
  73.         }
  74.         
  75.         //清理World的时候产生的OnlineIdentifiers,所以不考虑  ---- [不需要]
  76.         if (OnlineIdentifiers.Num())
  77.         {
  78.                 UE_LOG(LogPlayLevel, Display, TEXT("Shutting down PIE online subsystems"));
  79.                 // Cleanup online subsystem shortly as we might be in a failed delegate
  80.                 // have to do this in batch because timer delegate doesn't recognize bound data
  81.                 // as a different delegate
  82.                 FTimerDelegate DestroyTimer;
  83.                 DestroyTimer.BindUObject(this, &UEditorEngine::CleanupPIEOnlineSessions, OnlineIdentifiers);
  84.                 GetTimerManager()->SetTimer(CleanupPIEOnlineSessionsTimerHandle, DestroyTimer, 0.1f, false);
  85.         }
  86.        
  87.         //广播EndPlayMap委托事件,因为我们是模拟Stop,并不想在PIE的时候真的触发了EndPlay ----- [不需要]
  88.         FGameDelegates::Get().GetEndPlayMapDelegate().Broadcast();
  89.        
  90.          //清理outer带PKG_PlayInEditor标签的standalone UObject  --- [不需要]
  91.         // find objects like Textures in the playworld levels that won't get garbage collected as they are marked RF_Standalone
  92.         for (FObjectIterator It; It; ++It)
  93.         {
  94.                 UObject* Object = *It;
  95.                 if (Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
  96.                 {
  97.                         if (Object->HasAnyFlags(RF_Standalone))
  98.                         {
  99.                                 // Clear RF_Standalone flag from objects in the levels used for PIE so they get cleaned up.
  100.                                 Object->ClearFlags(RF_Standalone);
  101.                         }
  102.                         // Close any asset editors that are currently editing this object
  103.                         GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->CloseAllEditorsForAsset(Object);
  104.                 }
  105.         }
  106.         //不是释放逻辑,我们不关心    ---- [不需要]
  107.         EditorWorld->bAllowAudioPlayback = true;
  108.         EditorWorld = nullptr;
  109.         //清理World带来的逻辑   --------------- [不需要]
  110.         // mark everything contained in the PIE worlds to be deleted
  111.         for (UWorld* World : WorldsBeingCleanedUp)
  112.         {
  113.                 // Occasionally during seamless travel the Levels array won't yet be populated so mark this world first
  114.                 // then pick up the sub-levels via the level iterator
  115.                 World->MarkObjectsPendingKill();
  116.                
  117.                 // Because of the seamless travel the world might still be in the root set too, so also clear that
  118.                 World->RemoveFromRoot();
  119.                 for (auto LevelIt(World->GetLevelIterator()); LevelIt; ++LevelIt)
  120.                 {
  121.                         if (const ULevel* Level = *LevelIt)
  122.                         {
  123.                                 // We already picked up the persistent level with the top level mark objects
  124.                                 if (Level->GetOuter() != World)
  125.                                 {
  126.                                         CastChecked<UWorld>(Level->GetOuter())->MarkObjectsPendingKill();
  127.                                 }
  128.                         }
  129.                 }
  130.                 for (ULevelStreaming* LevelStreaming : World->GetStreamingLevels())
  131.                 {
  132.                         // If an unloaded levelstreaming still has a loaded level we need to mark its objects to be deleted as well
  133.                         if (LevelStreaming->GetLoadedLevel() && (!LevelStreaming->ShouldBeLoaded() || !LevelStreaming->ShouldBeVisible()))
  134.                         {
  135.                                 CastChecked<UWorld>(LevelStreaming->GetLoadedLevel()->GetOuter())->MarkObjectsPendingKill();
  136.                         }
  137.                 }
  138.         }
  139.         //强制清理GameInstance中的所有UObject,我们只是考虑模拟Shutdown,并不需要强制清理 --- [不需要]
  140.         // mark all objects contained within the PIE game instances to be deleted
  141.         for (TObjectIterator<UGameInstance> It; It; ++It)
  142.         {
  143.                 auto MarkObjectPendingKill = [](UObject* Object)
  144.                 {
  145.                         Object->MarkPendingKill();
  146.                 };
  147.                 ForEachObjectWithOuter(*It, MarkObjectPendingKill, true, RF_NoFlags, EInternalObjectFlags::PendingKill);
  148.         }
  149.         // 清理Slate的所有渲染状态,由于是在稳定的空场景里面,展示的UI也是不需要释放的,所以 --- [不需要]
  150.         // Flush any render commands and released accessed UTextures and materials to give them a chance to be collected.
  151.         if ( FSlateApplication::IsInitialized() )
  152.         {
  153.                 FSlateApplication::Get().FlushRenderState();
  154.         }
  155.          //只是做一下检查,检查结果打印一下而已,不涉及到释放  ---- [不需要]
  156.         // Make sure that all objects in the temp levels were entirely garbage collected.
  157.         for( FObjectIterator ObjectIt; ObjectIt; ++ObjectIt )
  158.         {
  159.                 UObject* Object = *ObjectIt;
  160.                 if( Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
  161.                 {
  162.                         UWorld* TheWorld = UWorld::FindWorldInPackage(Object->GetOutermost());
  163.                         if ( TheWorld )
  164.                         {
  165.                                 StaticExec(nullptr, *FString::Printf(TEXT("OBJ REFS CLASS=WORLD NAME=%s"), *TheWorld->GetPathName()));
  166.                         }
  167.                         else
  168.                         {
  169.                                 UE_LOG(LogPlayLevel, Error, TEXT("No PIE world was found when attempting to gather references after GC."));
  170.                         }
  171.                         FReferenceChainSearch RefChainSearch(Object, EReferenceChainSearchMode::Shortest);
  172.                         FFormatNamedArguments Arguments;
  173.                         Arguments.Add(TEXT("Path"), FText::FromString(RefChainSearch.GetRootPath()));
  174.                                
  175.                         // We cannot safely recover from this.
  176.                         FMessageLog(NAME_CategoryPIE).CriticalError()
  177.                                 ->AddToken(FUObjectToken::Create(Object, FText::FromString(Object->GetFullName())))
  178.                                 ->AddToken(FTextToken::Create(FText::Format(LOCTEXT("PIEObjectStillReferenced", "Object from PIE level still referenced. Shortest path from root: {Path}"), Arguments)));
  179.                 }
  180.         }
  181.         // Final cleanup/reseting
  182.         FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
  183.         UPackage* Package = EditorWorldContext.World()->GetOutermost();
  184.         // 重置Stereo状态,不涉及到释放  --- [不需要]
  185.         //ensure stereo rendering is disabled in case we need to re-enable next PIE run (except when the editor is running in VR)
  186.         bool bInVRMode = IVREditorModule::Get().IsVREditorModeActive();
  187.         if (GEngine->StereoRenderingDevice && !bInVRMode)
  188.         {
  189.                 GEngine->StereoRenderingDevice->EnableStereo(false);
  190.         }
  191.       
  192.         //热重启不应该涉及到Viewport的清理,所以不需要   ---- [不需要]
  193.         // Restores realtime viewports that have been disabled for PIE.
  194.         RemoveViewportsRealtimeOverride();
  195.       
  196.         //重新注册音频组件,不涉及到释放  --- [不需要]
  197.         for(TObjectIterator<UAudioComponent> It; It; ++It)
  198.         {
  199.                 UAudioComponent* AudioComp = *It;
  200.                 if (AudioComp->GetWorld() == EditorWorldContext.World())
  201.                 {
  202.                         AudioComp->ReregisterComponent();
  203.                 }
  204.         }
  205.         //清理所有的Play回话,热重启不需要  --- [不需要]
  206.         // no longer queued
  207.         CancelRequestPlaySession();
  208.         bRequestEndPlayMapQueued = false;
  209. }
复制代码
最后总结需要在热重启里面做只剩下两个:
     1.FlushAsyncLoading()
     2.TeardownPlaySession(ThisContext);


其中FlushAsyncLoading()就不多说了,继续研究一下TeardownPlaySession:
  1. void UEditorEngine::TeardownPlaySession(FWorldContext& PieWorldContext)
  2. {
  3.         //销毁World,因为热重启会在一个稳定的空场景里,所以不需要销毁World   ---[不需要]
  4.         check(PieWorldContext.WorldType == EWorldType::PIE);
  5.         PlayWorld = PieWorldContext.World();
  6.         PlayWorld->BeginTearingDown();
  7.         if (!PieWorldContext.RunAsDedicated)
  8.         {
  9.                 // Slate data for this pie world
  10.                 FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
  11.                 //销毁ViewPort,热重启不希望销毁Viewport,所以不需要   ----[不需要]
  12.                 // Destroy Viewport
  13.                 if ( PieWorldContext.GameViewport != NULL && PieWorldContext.GameViewport->Viewport != NULL )
  14.                 {
  15.                         PieWorldContext.GameViewport->CloseRequested(PieWorldContext.GameViewport->Viewport);
  16.                 }
  17.                 CleanupGameViewport();
  18.        
  19.                 //清理PIEviewport   -------------------------------[不需要]
  20.                 // Clean up the slate PIE viewport if we have one
  21.                 if (SlatePlayInEditorSession)
  22.                 {
  23.                         if (SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
  24.                         {
  25.                                 TSharedPtr<IAssetViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
  26.                                 if(PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::PlayInEditor)
  27.                                 {
  28.                                         // Set the editor viewport location to match that of Play in Viewport if we aren't simulating in the editor, we have a valid player to get the location from (unless we're going back to VR Editor, in which case we won't teleport the user.)
  29.                                         if (bLastViewAndLocationValid == true && !GEngine->IsStereoscopic3D( Viewport->GetActiveViewport() ) )
  30.                                         {
  31.                                                 bLastViewAndLocationValid = false;
  32.                                                 Viewport->GetAssetViewportClient().SetViewLocation( LastViewLocation );
  33.                                                 if( Viewport->GetAssetViewportClient().IsPerspective() )
  34.                                                 {
  35.                                                         // Rotation only matters for perspective viewports not orthographic
  36.                                                         Viewport->GetAssetViewportClient().SetViewRotation( LastViewRotation );
  37.                                                 }
  38.                                         }
  39.                                 }
  40.                                 // No longer simulating in the viewport
  41.                                 Viewport->GetAssetViewportClient().SetIsSimulateInEditorViewport( false );
  42.                                
  43.                                 FEditorModeRegistry::Get().UnregisterMode(FBuiltinEditorModes::EM_Physics);
  44.                                
  45.                                 // Clear out the hit proxies before GC'ing
  46.                                 Viewport->GetAssetViewportClient().Viewport->InvalidateHitProxy();
  47.                         }
  48.                         else if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
  49.                         {
  50.                                 // Unregister the game viewport from slate.  This sends a final message to the viewport
  51.                                 // so it can have a chance to release mouse capture, mouse lock, etc.               
  52.                                 FSlateApplication::Get().UnregisterGameViewport();
  53.                                 // Viewport client is cleaned up.  Make sure its not being accessed
  54.                                 SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->SetViewportClient(NULL);
  55.                                 // The window may have already been destroyed in the case that the PIE window close box was pressed
  56.                                 if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
  57.                                 {
  58.                                         // Destroy the SWindow
  59.                                         FSlateApplication::Get().DestroyWindowImmediately(SlatePlayInEditorSession->SlatePlayInEditorWindow.Pin().ToSharedRef());
  60.                                 }
  61.                         }
  62.                 }
  63.        
  64.                 //LocalPlayer是玩家实例,代表一个本地玩家 ---- [不需要]
  65.                 // Disassociate the players from their PlayerControllers.
  66.                 // This is done in the GameEngine path in UEngine::LoadMap.
  67.                 // But since PIE is just shutting down, and not loading a
  68.                 // new map, we need to do it manually here for now.
  69.                 //for (auto It = GEngine->GetLocalPlayerIterator(PlayWorld); It; ++It)
  70.                 for (FLocalPlayerIterator It(GEngine, PlayWorld); It; ++It)
  71.                 {
  72.                         if(It->PlayerController)
  73.                         {
  74.                                 if(It->PlayerController->GetPawn())
  75.                                 {
  76.                                         PlayWorld->DestroyActor(It->PlayerController->GetPawn(), true);
  77.                                 }
  78.                                 PlayWorld->DestroyActor(It->PlayerController, true);
  79.                                 It->PlayerController = NULL;
  80.                         }
  81.                 }
  82.         }
  83.         // Change GWorld to be the play in editor world during cleanup.
  84.         ensureMsgf( EditorWorld == GWorld, TEXT("TearDownPlaySession current world: %s"), GWorld ? *GWorld->GetName() : TEXT("No World"));
  85.         GWorld = PlayWorld;
  86.         GIsPlayInEditorWorld = true;
  87.        
  88.         // Remember Simulating flag so that we know if OnSimulateSessionFinished is required after everything has been cleaned up.
  89.         bool bWasSimulatingInEditor = PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor;
  90.        
  91.         //重置音频   --------------------------------------------------------- [不需要]
  92.         // Stop all audio and remove references to temp level.
  93.         if (FAudioDevice* AudioDevice = PlayWorld->GetAudioDeviceRaw())
  94.         {
  95.                 AudioDevice->Flush(PlayWorld);
  96.                 AudioDevice->ResetInterpolation();
  97.                 AudioDevice->OnEndPIE(false); // TODO: Should this have been bWasSimulatingInEditor?
  98.                 AudioDevice->SetTransientMasterVolume(1.0f);
  99.         }
  100.         //LevelStreaming相关,热重启所在的场景是空场景,不会有LevelStreaming   --- [不需要]
  101.         // Clean up all streaming levels
  102.         PlayWorld->bIsLevelStreamingFrozen = false;
  103.         PlayWorld->SetShouldForceUnloadStreamingLevels(true);
  104.         PlayWorld->FlushLevelStreaming();
  105.         //LevelStreaming相关,热重启所在的场景是空场景,不会有LevelStreaming   --- [不需要]
  106.         // cleanup refs to any duplicated streaming levels
  107.         for ( int32 LevelIndex=0; LevelIndex<PlayWorld->GetStreamingLevels().Num(); LevelIndex++ )
  108.         {
  109.                 ULevelStreaming* StreamingLevel = PlayWorld->GetStreamingLevels()[LevelIndex];
  110.                 if( StreamingLevel != NULL )
  111.                 {
  112.                         const ULevel* PlayWorldLevel = StreamingLevel->GetLoadedLevel();
  113.                         if ( PlayWorldLevel != NULL )
  114.                         {
  115.                                 UWorld* World = Cast<UWorld>( PlayWorldLevel->GetOuter() );
  116.                                 if( World != NULL )
  117.                                 {
  118.                                         // Attempt to move blueprint debugging references back to the editor world
  119.                                         if( EditorWorld != NULL && EditorWorld->GetStreamingLevels().IsValidIndex(LevelIndex) )
  120.                                         {
  121.                                                 const ULevel* EditorWorldLevel = EditorWorld->GetStreamingLevels()[LevelIndex]->GetLoadedLevel();
  122.                                                 if ( EditorWorldLevel != NULL )
  123.                                                 {
  124.                                                         UWorld* SublevelEditorWorld  = Cast<UWorld>(EditorWorldLevel->GetOuter());
  125.                                                         if( SublevelEditorWorld != NULL )
  126.                                                         {
  127.                                                                 World->TransferBlueprintDebugReferences(SublevelEditorWorld);
  128.                                                         }       
  129.                                                 }
  130.                                         }
  131.                                 }
  132.                         }
  133.                 }
  134.         }
  135.         //Editor相关   -------------------------------- [不需要]
  136.         // 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
  137.         TArray< IBlueprintEditor* > Editors;
  138.         UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
  139.         const UWorld::FBlueprintToDebuggedObjectMap& EditDebugObjectsPre = PlayWorld->GetBlueprintObjectsBeingDebugged();
  140.         for (UWorld::FBlueprintToDebuggedObjectMap::TConstIterator EditIt(EditDebugObjectsPre); EditIt; ++EditIt)
  141.         {
  142.                 if (UBlueprint* TargetBP = EditIt.Key().Get())
  143.                 {
  144.                         if(IBlueprintEditor* EachEditor = static_cast<IBlueprintEditor*>(AssetEditorSubsystem->FindEditorForAsset(TargetBP, false)))
  145.                         {
  146.                                 Editors.AddUnique( EachEditor );
  147.                         }
  148.                 }
  149.         }
  150.         //通知Actor即将销毁,热重启场景是一个稳定的空场景,不存在需要销毁的Actor  ----- [不需要]
  151.         // Go through and let all the PlayWorld Actor's know they are being destroyed
  152.         for (FActorIterator ActorIt(PlayWorld); ActorIt; ++ActorIt)
  153.         {
  154.                 ActorIt->RouteEndPlay(EEndPlayReason::EndPlayInEditor);
  155.         }
  156.    
  157.         //GameInstance::Shutdown     ---------------- [需要]
  158.         PieWorldContext.OwningGameInstance->Shutdown();
  159.         //Editor相关     --------- [不需要]
  160.         // Move blueprint debugging pointers back to the objects in the editor world
  161.         PlayWorld->TransferBlueprintDebugReferences(EditorWorld);
  162.         //物理系统的虚拟场景清理,由于我们不会清理当前场景   ----  [不需要]
  163.         FPhysScene* PhysScene = PlayWorld->GetPhysicsScene();
  164.         if (PhysScene)
  165.         {
  166.                 PhysScene->WaitPhysScenes();
  167.                 PhysScene->KillVisualDebugger();
  168.         }
  169.         //清理场景,热重启的空场景不需要清理    ------  [不需要]
  170.         // Clean up the temporary play level.
  171.         PlayWorld->CleanupWorld();
  172.         // Remove from root (Seamless travel may have done this)
  173.         PlayWorld->RemoveFromRoot();
  174.         PlayWorld = NULL;
  175.    
  176.          //Editor相关 ---------------------------------------------------------------[不需要]
  177.         // Refresh any editors we had open in case they referenced objects that no longer exist.
  178.         for (int32 iEditors = 0; iEditors <  Editors.Num(); iEditors++)
  179.         {
  180.                 Editors[ iEditors ]->RefreshEditors();
  181.         }
  182.        
  183.         // Restore GWorld.
  184.         GWorld = EditorWorld;
  185.         GIsPlayInEditorWorld = false;
  186.         FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
  187.         //依然是清理Viewport,不需要清理Viewport      -------- [不需要]
  188.         // Let the viewport know about leaving PIE/Simulate session. Do it after everything's been cleaned up
  189.         // as the viewport will play exit sound here and this has to be done after GetAudioDevice()->Flush
  190.         // otherwise all sounds will be immediately stopped.
  191.         if (!PieWorldContext.RunAsDedicated)
  192.         {
  193.                 // Slate data for this pie world
  194.                 FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
  195.                 if (SlatePlayInEditorSession && SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
  196.                 {
  197.                         TSharedPtr<IAssetViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
  198.                         if( Viewport->HasPlayInEditorViewport() )
  199.                         {
  200.                                 Viewport->EndPlayInEditorSession();
  201.                         }
  202.                         // Let the Slate viewport know that we're leaving Simulate mode
  203.                         if( bWasSimulatingInEditor )
  204.                         {
  205.                                 Viewport->OnSimulateSessionFinished();
  206.                         }
  207.                         StaticCast<FLevelEditorViewportClient&>(Viewport->GetAssetViewportClient()).SetReferenceToWorldContext(EditorWorldContext);
  208.                 }
  209.                 // 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)
  210.                 SlatePlayInEditorMap.Remove(PieWorldContext.ContextHandle);
  211.         }
  212. }
复制代码
看来TeardownPlaySession中只有OwningGameInstance->Shutdown();是我们关心的。


最终总结一下,模拟PIE的Stop流程中需要注意的只有两个调用:
1.FlushAsyncLoading().
2.OwningGameInstance->Shutdown();
而FlushAsyncLoading()是阻塞加载所有异步请求没什么好说的,我们就主要看看UGameInstance::Shutdown()
-------------------------------"怎么来的" 结束分割线----------------------------------------
"怎么做呢":
让我们来看看UGameInstance::Shutdown():
注意看中文注释(忽略误选中的选中框)
再对比一下UGameInstance::Init()
注意看中文注释(忽略误选中的选中框)
对比:
Shutdown:Init:
1.通知蓝图Shutdown1.通知蓝图Init
2.清理网络回话对象2.创建网络回话对象
3.清理所有LocalPlayer3.XXXXXXXXXXXXX
4.清理所有GameInstanceSubsystem4.初始化所有GameInstanceSubsystem
5.清理网路错误委托监听5.绑定网络错误委托监听
6.清理WorldContext6.XXXXXXXXXXXX
可见,Shutdown相对Init多了:清理所有LocalPlayer,清理WorldContext。什么概念呢? 就是Shutdown做了些事情,而且是在再次Init后都没办法恢复。
1.清理LocalPlayer: LocalPlayer代表一个用户端,一般手游都是只有一个LocalPlayer,主机游戏可能会通过分屏方式出现两个或者四个LocalPlayer。清理LocalPlayer后如果不进行恢复,用户端的输入将无效,包括Console Command.
2.清理WorldContext: WorldContext即是World上下文,与UWorld有关系,并且会影响到一些需要区别多个LocalPlayer的接口.
这个时候,有人可能会问"既然是模拟Shutdown,是不是可以直接把Shutdown逻辑拷贝一下就行?",当然这也是一个办法。但是每个项目都会继承UGameInstance,那每层继承都要拷贝,修改的时候还要注意修改两处,维护起来是很麻烦。所以使用UGameInstance的Shutdown和Init会更方便维护。
下面是模拟逻辑ShutdownOnlyForReload和InitOnlyForReload,最终都会调用到Shutdown和Init.
  1. void UMGameInstance::ShutdownOnlyForReload()
  2. {
  3.         UE_LOG(LogTemp,Log,TEXT("UMGameInstance::ShutdownOnlyForReload....Start"));
  4.         FWorldContext* WorldContextOld = WorldContext;  //记录一下WorldContext,Shutdown中置空后再复制回去即可.
  5.         Shutdown();                              //调用UMGameInstance::Shutdown,在里面还会进行UnLua释放
  6.        
  7.         GameInstance = this;
  8.         WorldContext = WorldContextOld;          //还原WorldContext
  9.         FString OutError;
  10.         CreateInitialPlayer(OutError);                  //恢复LocalPlayer
  11.         UE_LOG(LogTemp,Log,TEXT("UMGameInstance::ShutdownOnlyForReload....End"));
  12. }
  13. void UMGameInstance::InitOnlyForReload()
  14. {
  15.         UE_LOG(LogTemp,Log,TEXT("UMGameInstance::InitOnlyForReload....Start"));
  16.        
  17.         Init();                                  //调用UMGameInstance::Init
  18.        
  19.         FBasicMainEntry::InitLua(GetWorld());      //初始化Lua.
  20.        
  21.         UE_LOG(LogTemp,Log,TEXT("UMGameInstance::InitOnlyForReload....End"));
  22. }
复制代码

"怎么验证"
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的释放情况。遍历打印代码如下:
  1. //bNeedClass 是否显示Class并参与排序
  2. //bNoReflectionObj 是否不收集UFunction/UClass/UStruct这些UObject
  3. //bOnlyContent 是否只打印工程资源.
  4. void DumpAllObj(bool bNeedClass,bool bNoReflectionObj,bool bOnlyContent)
  5. {
  6.         TArray<FString> strObjs;
  7.         for (TObjectIterator<UObject> It; It; ++It)
  8.         {
  9.                 UObject* Obj = *It;
  10.                
  11.                 if (!bNoReflectionObj || (Cast<UField>(Obj) == nullptr
  12. #if ENGINE_MINOR_VERSION < 25
  13.                         && Cast<UProperty>(Obj) == nullptr
  14. #endif
  15.                         && Cast<UFunction>(Obj) == nullptr
  16.                         && Cast<UClass>(Obj) == nullptr
  17.                         && Cast<UStruct>(Obj) == nullptr))
  18.                 {
  19.                         if (!bOnlyContent || (Obj->GetPathName().StartsWith("/Game/")))
  20.                         {
  21.                                 if (bNeedClass)
  22.                                         strObjs.Add(Obj->GetFullName());
  23.                                 else
  24.                                         strObjs.Add(Obj->GetPathName());
  25.                         }
  26.                 }
  27.         }
  28.         strObjs.Sort();
  29.         FString DumpLogFile = FString::Format(TEXT("{0}/DumpAllObj_{1}_{2}_{3}.txt"), { FPaths::ConvertRelativePathToFull(FPaths::ProjectPersistentDownloadDir()),bNeedClass,bNoReflectionObj,bOnlyContent });
  30.         FFileHelper::SaveStringArrayToFile(strObjs, *DumpLogFile, FFileHelper::EEncodingOptions::ForceUTF8);
  31. }
复制代码

"怎么解决":
验证后,发现问题该怎么解决呢?
非UObject问题: 主要是通过项目规范进行约束。
UObject问题: 可能会打印出来上万个没有正常释放的UObject,如果逐一人肉排查引用关系,是很恐怖的事。这里有个小经验,可以先从引用集合UObject开始排查,像DataTable和DataAsset,因为UObject会带来很多引用,释放掉DataTable,UObject数量能大幅度减少。
查找引用代码:
  1. void PrintAllReferencers(FString ObjName)
  2. {
  3.         TMap<UObject*,FObjReferencers> AllReferencers = GetAllReferencers(ObjName);
  4.         UE_LOG(LogTemp,Log,TEXT("---------Start Print All References of %s"),*ObjName);
  5.        
  6.         for(TMap<UObject*,FObjReferencers>::TIterator Iter = AllReferencers.CreateIterator();Iter;++Iter)
  7.         {
  8.                 UObject* ReferenceeObj = Iter->Key;
  9.                 UE_LOG(LogTemp,Log,TEXT("Referencers Of %s (%s):"),*(ReferenceeObj->GetFName().ToString()),*(ReferenceeObj->GetClass()->GetFName().ToString()));
  10.                 FObjReferencers ReferencersInfo = Iter->Value;
  11.                 for(TArray<UObject*>::TIterator Iter2 = ReferencersInfo.Referencers.CreateIterator();Iter2;++Iter2)
  12.                 {
  13.                         UObject* ReferencerObj =  *Iter2;
  14.                         UE_LOG(LogTemp,Log,TEXT("%30s (%s)"),*(ReferencerObj->GetFName().ToString()),*(ReferencerObj->GetClass()->GetFName().ToString()));
  15.                 }
  16.                 UE_LOG(LogTemp,Log,TEXT("--------------------------"));
  17.         }
  18.         UE_LOG(LogTemp,Log,TEXT("---------End Print All References of %s"),*ObjName);
  19. }
复制代码

--------------------------------------------------------------------------------------------
这个方案已经在项目里使用,为了增加测试的机会,我把游戏账号登出也改成了重启XD。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 04:40 , Processed in 0.092558 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表