[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推薦閱讀:
※真良心大廠EPIC,頁游廣告又有新素材了!
※[UE4]Indirect Lighting Cache(間接光照緩存)
※steam發布篇---2
※我們的UE4項目——PixArk上線了[廣告]