资源加载与异步加载

自动关联目录:资源加载与异步加载

加载的本质是一次磁盘 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 的依赖大小
每隔一段时间小卡顿后台异步加载触发了 FlushAsyncLoadingUnreal 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 PartitionUE5 的自动流式超大场景
// 分帧预加载:每帧请求一个,避免一次性把所有 IO 压到一帧
void UPreloader::Tick(float DT)
{
    if (PendingIndex < PendingPaths.Num())
    {
        UAssetManager::GetStreamableManager().RequestAsyncLoad(PendingPaths[PendingIndex++]);
    }
}

卸载

手段说明
释放 handleHandle.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)