APSchmidt 发表于 2021-4-28 09:18

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("Shutting down PIE online subsystems"));
                // Cleanup online subsystem shortly as we might be in a failed delegate
                // have to do this in batch because timer delegate doesn'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'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'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("OBJ REFS CLASS=WORLD NAME=%s"), *TheWorld->GetPathName()));
                        }
                        else
                        {
                                UE_LOG(LogPlayLevel, Error, TEXT("No PIE world was found when attempting to gather references after GC."));
                        }

                        FReferenceChainSearch RefChainSearch(Object, EReferenceChainSearchMode::Shortest);

                        FFormatNamedArguments Arguments;
                        Arguments.Add(TEXT("Path"), 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("PIEObjectStillReferenced", "Object from PIE level still referenced. Shortest path from root: {Path}"), 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'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.)
                                        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'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("TearDownPlaySession current world: %s"), GWorld ? *GWorld->GetName() : TEXT("No World"));
        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'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'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'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()
-------------------------------"怎么来的" 结束分割线----------------------------------------
"怎么做呢":
让我们来看看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的接口.
这个时候,有人可能会问"既然是模拟Shutdown,是不是可以直接把Shutdown逻辑拷贝一下就行?",当然这也是一个办法。但是每个项目都会继承UGameInstance,那每层继承都要拷贝,修改的时候还要注意修改两处,维护起来是很麻烦。所以使用UGameInstance的Shutdown和Init会更方便维护。
下面是模拟逻辑ShutdownOnlyForReload和InitOnlyForReload,最终都会调用到Shutdown和Init.
void UMGameInstance::ShutdownOnlyForReload()
{
        UE_LOG(LogTemp,Log,TEXT("UMGameInstance::ShutdownOnlyForReload....Start"));

        FWorldContext* WorldContextOld = WorldContext;//记录一下WorldContext,Shutdown中置空后再复制回去即可.

        Shutdown();                              //调用UMGameInstance::Shutdown,在里面还会进行UnLua释放
       
        GameInstance = this;
        WorldContext = WorldContextOld;          //还原WorldContext

        FString OutError;
        CreateInitialPlayer(OutError);                  //恢复LocalPlayer

        UE_LOG(LogTemp,Log,TEXT("UMGameInstance::ShutdownOnlyForReload....End"));
}

void UMGameInstance::InitOnlyForReload()
{
        UE_LOG(LogTemp,Log,TEXT("UMGameInstance::InitOnlyForReload....Start"));
       
        Init();                                  //调用UMGameInstance::Init
       
        FBasicMainEntry::InitLua(GetWorld());      //初始化Lua.
       
        UE_LOG(LogTemp,Log,TEXT("UMGameInstance::InitOnlyForReload....End"));
}

"怎么验证"
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("/Game/")))
                        {
                                if (bNeedClass)
                                        strObjs.Add(Obj->GetFullName());
                                else
                                        strObjs.Add(Obj->GetPathName());
                        }
                }
        }

        strObjs.Sort();

        FString DumpLogFile = FString::Format(TEXT("{0}/DumpAllObj_{1}_{2}_{3}.txt"), { FPaths::ConvertRelativePathToFull(FPaths::ProjectPersistentDownloadDir()),bNeedClass,bNoReflectionObj,bOnlyContent });
        FFileHelper::SaveStringArrayToFile(strObjs, *DumpLogFile, FFileHelper::EEncodingOptions::ForceUTF8);
}

"怎么解决":
验证后,发现问题该怎么解决呢?
非UObject问题: 主要是通过项目规范进行约束。
UObject问题: 可能会打印出来上万个没有正常释放的UObject,如果逐一人肉排查引用关系,是很恐怖的事。这里有个小经验,可以先从引用集合UObject开始排查,像DataTable和DataAsset,因为UObject会带来很多引用,释放掉DataTable,UObject数量能大幅度减少。
查找引用代码:
void PrintAllReferencers(FString ObjName)
{
        TMap<UObject*,FObjReferencers> AllReferencers = GetAllReferencers(ObjName);

        UE_LOG(LogTemp,Log,TEXT("---------Start Print All References of %s"),*ObjName);
       
        for(TMap<UObject*,FObjReferencers>::TIterator Iter = AllReferencers.CreateIterator();Iter;++Iter)
        {
                UObject* ReferenceeObj = Iter->Key;
                UE_LOG(LogTemp,Log,TEXT("Referencers Of %s (%s):"),*(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("%30s (%s)"),*(ReferencerObj->GetFName().ToString()),*(ReferencerObj->GetClass()->GetFName().ToString()));
                }

                UE_LOG(LogTemp,Log,TEXT("--------------------------"));
        }

        UE_LOG(LogTemp,Log,TEXT("---------End Print All References of %s"),*ObjName);
}

--------------------------------------------------------------------------------------------
这个方案已经在项目里使用,为了增加测试的机会,我把游戏账号登出也改成了重启XD。
页: [1]
查看完整版本: UE资源热重载方案