资源加载与异步加载
自动关联目录:资源加载与异步加载
加载的本质是一次磁盘 IO + 一次反序列化。前者慢(毫秒到百毫秒级),后者占用主线程。所以"加载卡顿"几乎总是在主线程上做了同步加载。
一句话定位:所有能在后台做的加载都该异步;所有异步加载都要有人持有 handle,否则加载完立刻被 GC。
四种方式
| 方式 | 同步/异步 | 适用 |
|---|---|---|
LoadObject<T>() / StaticLoadObject | 同步 | 只在启动时、编辑器工具、确定很小的资产 |
TSoftObjectPtr::LoadSynchronous() | 同步 | 同上,但走软引用,写法方便 |
FStreamableManager::RequestAsyncLoad | 异步 | 运行时默认选择 |
UAssetManager::LoadPrimaryAsset | 异步 | 按 PrimaryAsset 主键加载,可带 Bundle |
// 同步(会卡)
UTexture2D* Tex = LoadObject<UTexture2D>(nullptr, TEXT("/Game/Icons/Sword.Sword"));
// 异步(推荐)
FSoftObjectPath Path(TEXT("/Game/Icons/Sword.Sword"));
TSharedPtr<FStreamableHandle> Handle =
UAssetManager::GetStreamableManager().RequestAsyncLoad(Path,
[Path]()
{
UTexture2D* Tex = Cast<UTexture2D>(Path.ResolveObject());
});软引用
| 类型 | 用途 |
|---|---|
TSoftObjectPtr<T> | 蓝图/C++ 里引用资产但不强制加载 |
TSoftClassPtr<T> | 软引用一个类 |
FSoftObjectPath | 路径本身,可作字典 key、可序列化 |
FStringAssetReference | 旧名,等价 |
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UTexture2D> Icon; // 编辑器可见,运行时不加载
// 需要时
Icon.LoadSynchronous(); // 同步
UAssetManager::GetStreamableManager().RequestAsyncLoad(Icon.ToSoftObjectPath(), Callback);把 TSoftObjectPtr 换成硬引用(UTexture2D*)的代价是:外层资产一加载,它就被一起加载。角色数据里硬引用几十张贴图 = 打开角色界面就要等几十个 IO。
Handle 与保活
这是异步加载最容易出错的地方:加载完成后,如果你不持有任何强引用,对象会被 GC 回收。
class UMyComponent : public UActorComponent
{
UPROPERTY() // 1. UPROPERTY 构成 GC 引用边
TArray<TObjectPtr<UTexture2D>> LoadedTextures;
TSharedPtr<FStreamableHandle> Handle; // 2. 持有 handle,加载期间保持有效
void Load()
{
Handle = UAssetManager::GetStreamableManager().RequestAsyncLoad(Paths,
[this](){ Handle->GetLoadedAssets(LoadedTextures); });
}
};| 要点 | 说明 |
|---|---|
Handle 释放 | 引用计数归零 → 资产可被 GC |
UPROPERTY 容器 | 加载完要把结果存进 UPROPERTY,否则仍会被回收 |
ReleaseHandle() | 主动释放,允许卸载 |
CancelHandle() | 取消未完成的加载 |
WaitUntilComplete() | 强制同步等待,等于把异步写成同步,慎用 |
典型 bug:异步回调里只把对象塞进 TArray<UTexture2D*>(未标 UPROPERTY),回调结束后对象消失,表现为"偶尔加载不出来"。
卡顿排查
| 现象 | 原因 | 验证 |
|---|---|---|
| 进入关卡瞬间掉帧 | 关卡里硬引用过多,一次性同步加载 | stat streaming / Insights 看 FAsyncLoadingThread 是否被阻塞 |
| 打开某个 UI 卡顿 | UI 蓝图硬引用了大批贴图 | SizeMap 看该 Widget 的依赖大小 |
| 每隔一段时间小卡顿 | 后台异步加载触发了 FlushAsyncLoading | Unreal Insights 里找 FlushAsyncLoading |
| 移动端特别明显 | IO 慢,且包内是压缩的 | 看 .ucas 压缩设置 |
// 找出是不是同步加载:在同步加载处打断点,或开日志
LogStreaming Verbose关键命令:
| 命令 | 作用 |
|---|---|
stat streaming | 流式加载状态、待加载数 |
DumpLoadedAssets | 列出当前已加载资产(查"为什么内存下不来") |
obj list class=Texture2D | 统计某类资产实例数 |
s.EventDrivenLoaderEnabled 1 | 事件驱动加载器(EDL),UE 默认开启 |
s.AsyncLoadingTimeLimit | 每帧异步加载的时间预算(毫秒) |
s.PriorityAsyncLoadingExtraTime | 高优先级加载的额外时间 |
FlushAsyncLoading() 是卡顿之王:它会阻塞主线程直到所有异步加载完成。加载界面结束后立刻调它,就是一次大卡顿。
预加载策略
| 策略 | 做法 | 适用 |
|---|---|---|
| 启动时全量 | 全同步加载 | 只有很小的项目 |
| 分帧预加载 | 每帧请求一批,用 s.AsyncLoadingTimeLimit 控预算 | 通用 |
| Bundle 分组 | 见 AssetManager | 列表/详情分离 |
| 关卡流式 | Level Streaming,LoadStreamLevel | 大世界 |
| World Partition | UE5 的自动流式 | 超大场景 |
// 分帧预加载:每帧请求一个,避免一次性把所有 IO 压到一帧
void UPreloader::Tick(float DT)
{
if (PendingIndex < PendingPaths.Num())
{
UAssetManager::GetStreamableManager().RequestAsyncLoad(PendingPaths[PendingIndex++]);
}
}卸载
| 手段 | 说明 |
|---|---|
| 释放 handle | Handle.Reset(),允许下次 GC 回收 |
| 移除 UPROPERTY 引用 | 断开引用边 |
CollectGarbage | 真正回收(有卡顿,见 GC) |
| 卸载 Package | 关卡切换时自动进行 |
"内存降不下来"的常见原因:引用没断干净。用 obj refs name=XXX 看谁还在持有它。
常见坑
| 坑 | 说明 |
|---|---|
运行时用 LoadObject | 主线程同步 IO,必卡顿 |
异步加载后没存进 UPROPERTY | 加载完立刻被 GC,随机失效 |
回调里用裸 this | 对象可能已销毁,用 TWeakObjectPtr 或 TWeakPtr |
调用了 FlushAsyncLoading | 把异步变同步 |
| 硬引用一大堆贴图 | 加载链被拖长 |
| 以为卸载了但没有 | 引用还在,obj refs 查一下 |
| 在构造函数里加载资源 | 构造会被跑很多次(CDO),应为 BeginPlay 或按需 |
| 移动端没开 Pak 压缩权衡 | 压缩省体积但慢,需实测(见 Pak 与 IoStore) |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/20754657.html
更新于:2026年10月10日