[UE4]資源非同步載入(Assets Asynchronous Loading)與內存釋放(Free Memory)

為什麼需要非同步載入資源,因為當一次性載入的資源較多或者單個資源較大時,普通的LoadObject()方式會阻塞引擎的主線程。

假設測試工程叫TestTD4,自定義Character叫ATestTD4Character(頭文件為TestTD4Character.h)

假設在Content/Assets/目錄下放了三個動畫文件(AnimSequence)。

非同步載入

通過DefaultGame.ini配置文件生成FSoftObjectPath

DefaultGame.ini

[/Script/TestTD4.TestTD4Character]+TestAssets=/Game/Assets/ThirdPerson_Jump.ThirdPerson_Jump+TestAssets=/Game/Assets/ThirdPersonRun.ThirdPersonRun+TestAssets=/Game/Assets/ThirdPersonWalk.ThirdPersonWalk

TestTD4Character.h

UPROPERTY(Config) TArray<FSoftObjectPath> TestAssets;

這個屬性的意思是:加Config標籤表示從DefaultGame.ini讀取,TestAssets就是DefaultGame.ini中配置的屬性名,當遊戲啟動時,這個數組會被自動填充3個元素,即資源的路徑。

註:4.18版本中FStringAssetReference、TAssetPtr兩個變數被重命名為:FSoftObjectPath、TSoftObjectPtr.

TestTD4Character.cpp

void ATestTD4Character::BeginPlay(){ Super::BeginPlay(); for (FSoftObjectPath& Asset : TestAssets) { GEngine->AddOnScreenDebugMessage(-1, 3, FColor::Cyan, Asset.ToString()); } FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager(); AssetLoader.RequestAsyncLoad(TestAssets, FStreamableDelegate::CreateUObject(this, &ATestTD4Character::AnimAssetsDeferred));}void ATestTD4Character::AnimAssetsDeferred(){ for (FSoftObjectPath SoftObj : TestAssets) { TAssetPtr<UAnimSequence> AnimAsset(SoftObj); UAnimSequence* AnimObj = AnimAsset.Get(); if (AnimObj) { GEngine->AddOnScreenDebugMessage(-1, 3, FColor::Red, AnimObj->GetName()); } }}

列印結果:

ThirdPersonWalkThirdPersonRunThirdPerson_Jump/Game/Assets/ThirdPersonWalk.ThirdPersonWalk/Game/Assets/ThirdPersonRun.ThirdPersonRun/Game/Assets/ThirdPerson_Jump.ThirdPerson_Jump

如果注掉RequestAsyncLoad,直接執行AnimAssetsDeferred(),則列印結果為:

/Game/Assets/ThirdPersonWalk.ThirdPersonWalk/Game/Assets/ThirdPersonRun.ThirdPersonRun/Game/Assets/ThirdPerson_Jump.ThirdPerson_Jump

說明這三個資源確實是運行時非同步載入,而不是遊戲啟動時就被自動載入。

通過路徑字元串生成FSoftObjectPath

頭文件中定義一個變數

FSoftObjectPath SoftObj;

在cpp函數中通過路徑賦值(比如在BeginPlay()函數中)

SoftObj = FSoftObjectPath(TEXT("/Game/Mannequin/Animations/ThirdPersonWalk.ThirdPersonWalk"));FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager();AssetLoader.RequestAsyncLoad(SoftObj, FStreamableDelegate::CreateUObject(this, &AUMGTestGameModeBase::AnimAssetsDeferred));

回調函數中獲取對象

void ATestTD4Character::AssetsDeferred(){ TAssetPtr<UAnimSequence> WidgetAsset(SoftObj); UAnimSequence* AnimObj= WidgetAsset.Get(); if (AnimObj) { GEngine->AddOnScreenDebugMessage(-1, 3, FColor::Red, AnimObj->GetName()); }}

注意:UserWidget藍圖無法通過上述方式執行非同步載入,目前只測試了Animation、Mesh等資源是可行的。

同步載入

同步載入有兩種API:

  • FStreamableManager::LoadSynchronous

UAnimSequence* AimObj = AssetLoader.LoadSynchronous<UAnimSequence>(FSoftObjectPath(TEXT("/Game/Assets/ThirdPerson_Jump.ThirdPerson_Jump")));

  • FStreamableManager::RequestSyncLoad

TSharedPtr<FStreamableHandle> Handle = AssetLoader.RequestSyncLoad(FSoftObjectPath(TEXT("/Game/Assets/ThirdPerson_Jump.ThirdPerson_Jump")));if (Handle.IsValid()){ UAnimSequence* AnimiObj = Cast<UAnimSequence>(Handle->GetLoadedAsset());}

FStreamableManager的源碼注釋已經寫明:RequestAsyncLoad、RequestSyncLoad、LoadSynchronous等待延遲時間可能長達數秒。LoadSynchronous和RequestSyncLoad的內部實現是對非同步載入的封裝:調用FStreamableHandle::WaitUntilComplete()阻塞等待。RequestSyncLoad函數內部要麼會進行非同步載入並且調用WaitUntilComplete函數,要麼直接調用LoadObject函數 —— 哪個更快就調哪個。

資源內存釋放

用上述方式載入資源後(包括同步載入和非同步載入),如何再釋放資源並從內存中銷毀?兩種情況:

自動回收

只要對象失去引用後就會被自動釋放,無需手動Destroy。如果是非同步載入,對象只在回調函數中有效,回調函數執行完畢後,就會被標記為可收回狀態,如果此時ForceGC,則對象會立即銷毀。

手動回收

在執行載入時,將bManageActiveHandle標記為true,默認為false:表示是否手動管理FStreamableHandle。如果設置為true,則對象會一直常駐內存直到手動釋放。

FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager();UParticleSystem* AimObj = AssetLoader.LoadSynchronous<UParticleSystem>(FSoftObjectPath(AssetPath), true);

當對象不再需要時,再手動執行執行Unload。之後對象就會被自動回收:

FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager();AssetLoader.Unload(FSoftObjectPath(AssetPath));

Unload之後如果需要立即回收,可以執行ForceGC:

GEngine->ForceGarbageCollection(true);

在編輯器模式下,上述兩種回收方式都不起效,會一直常駐內存,只有在打包運行版本中才會生效。如果在編輯器運行模式下強制Destroy()或者MarkPendingKill(),則對象可以從內存中銷毀,但是無法再次Load,除非重啟編輯器。

注意事項

  • FStreamableManager::Unload()會Release掉和當前資源相關的所有FStreamableHandle。比如在三處位置載入了同一個資源,即使bManageActiveHandle設置為true,那麼只要調用Unload一次,就可以將這三個FStreamableHandle對象全部Release掉,即從內存中釋放該對象;如果對這3個FStreamableHandle對象分別執行Release,那麼只有當最後一個Handle被Release之後,該資源才會從內存中釋放。
  • 非同步載入時,誰先請求則誰的回調函數先執行,不會存在回調函數執行順序亂序的問題(除非修改TAsyncLoadPriority),因為引擎內部接收回調請求的容器使用的是TArray,且每次執行索引為0的回調,然後RemoveAt(0)。
  • 非同步載入時,如果資源還沒載入完成就執行ReleaseHandle()(假設載入時bManageActiveHandle為true),比如在執行回調函數之前執行ReleaseHandle,那麼當資源載入完成後(回調函數執行之後),會自動從內存中回收。不過該對象在回調函數中仍然有效,除非在回調函數內ForceGC。

void AMyPlayerController::TestAsyncLoadAndRelease(){ FStreamableDelegate Call; Call.BindUFunction(this, FName("AsyncLoadCallback")); FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager(); Handle = AssetLoader.RequestAsyncLoad(FSoftObjectPath(AssetPath), Call, 0, true); Handle->ReleaseHandle();}

參考

《Fortnite》開發經驗分享之運行時資源管理:Runtime Asset Management

https://answers.unrealengine.com/storage/temp/136465-runtimeassetmanagementin416.pdf?

answers.unrealengine.com


推薦閱讀:

真良心大廠EPIC,頁游廣告又有新素材了!
[UE4]Indirect Lighting Cache(間接光照緩存)
steam發布篇---2
我們的UE4項目——PixArk上線了[廣告]

TAG:虛幻4遊戲引擎 | 虛幻引擎 |