有一种卡顿的特征极其鲜明:平时帧率平稳,走到特定位置、放出特定技能、打开特定界面时卡一下,短的几十毫秒,长的半秒。十有八九是主线程同步加载。这一篇讲它的机制、排查证据链和治理手段,基于 UE5.3 的 AsyncLoading2(EDL,事件驱动加载器)。
机制:为什么"加载"会卡主线程
异步加载的 IO 和解序列化本来就在专用线程(AsyncLoadingThread)上跑,主线程理论上只接收完成通知。但有一批 API 是同步语义的:LoadObject、LoadPackage、SyncLoadAsset、FStreamableManager::LoadSynchronous,以及所有内部调用它们的封装(CreateWidget 同步创建带硬引用的 UI、SpawnActor 引用未加载资产、关卡流送的阻塞加载)。
同步语义的实现只有一个:FlushAsyncLoading()——主线程停住,把"我现在就要的对象"相关的所有未完成加载现场消费完才返回。于是卡顿时长可以写成:
$$T_{stall} \approx \frac{W_{remaining}}{v_{async}}$$
$W_{remaining}$ 是触发同步点时还没做完的加载工作量(剩余包的反序列化 + PostLoad + 依赖链),$v_{async}$ 是异步管线的吞吐。注意分子是剩余量不是总量:异步管线早就开跑了,主线程只是最后那一下等收尾——所以同样的资产,预热越充分,同步点等得越短;完全没预热的冷路径,等的就是全部。
还有一个隐蔽来源:异步加载完成后的对象需要在主线程做 PostLoad 和引用修补,这部分工作有每帧时间片限制(r.AsyncLoadingTimeLimit,默认几毫秒)。时间片小、这一帧又涌进来大量完成对象时,"消化"会拖好几帧——表现不是单次大卡,而是持续数帧的小掉帧。调大这个时间片能压平,代价是单帧负载变重,是平滑和时延的取舍。
证据链:怎么确认是同步加载
Insights 里的签名很直白,按确认顺序看:
- GameThread 轨道上出现
FlushAsyncLoading/LoadPackage长条:这就是主线程在等。点开调用栈能看到是哪个同步 API 发起的; - 卡顿帧的 AsyncLoad 轨道同时 busy:说明异步管线本来就在干活,佐证"等了剩余量";
- 抓不到 Insights 时用日志侧面验证:
-fileopenlog记录每次文件打开,卡顿时刻的时间戳附近如果有一串包加载记录,方向就定了; stat streaming/stat asyncload看加载队列深度:常态化有积压,说明预载策略整体缺位。
同步加载的触发点在代码里也有规律:搜 LoadSynchronous、LoadObject、CreateWidget、LoadStreamLevel(bShouldBlockOnLoad=true)这些调用,凡是跑在游戏流程里的(而不是一次性初始化里的),都是嫌疑犯。
治理:把同步点改成预热
确认了元凶,治理思路只有一条:让同步调用发生时,它要的东西已经加载完。手段按侵入度排:
- 引用软化 + 显式预载:把硬引用改成
TSoftObjectPtr/FSoftObjectPath,在流程上游(进战斗前、打开界面前一拍)用FStreamableManager::RequestAsyncLoad发起加载,回调或下一帧再使用。同步调用变成"必然命中已加载",Flush 等的就是零; - PrimaryAssetBundle 预载:GAS 技能、关卡配套资源这类成组资产,用 AssetManager 的 Bundle 机制在阶段切换时整批预载,比散落的手动调用好维护;
- 关卡流送去阻塞:
LoadStreamLevel默认阻塞,改成异步后监听完成事件再切换 Gameplay 逻辑;World Partition 项目确认 cell 的加载策略不是 visibility 驱动下的阻塞路径; - 时间片调节:
r.AsyncLoadingTimeLimit和PriorityAsyncLoadingExtraTime只解决"消化抖动",不解决同步等待,别拿它当主药方。
改造时的一个判断标准:同步加载在一次性初始化路径(启动、进关卡)里可以接受,在高频运行路径(战斗、移动、UI 交互)里必须消灭。前者卡一次没人记得,后者每次出现都在消耗玩家耐心。
和 GC、物理的区分
三种"周期性卡顿"容易混:同步加载卡在触发点(和玩家行为强相关)、GC 尖刺有固定节奏(增量周期的全量 purge)、物理同步卡在查询密集处。Insights 里三者轨道不同(AsyncLoad / GarbageCollection / Physics),签名分得开。混在一起的场景(加载触发分配高峰诱发 GC)先治理加载,GC 压力往往跟着降。
加载卡顿是性能问题里回报最高的一类:根因集中、证据明确、改法套路化。把项目里所有运行路径上的同步加载扫一遍,通常一天的改造能消掉一大半的实测卡顿。