在我们介绍UniTask之前,先来复习一下协程。

协程与异步加载

卡顿的根源在于一帧里干的事太多,CPU算不完,画面就会卡住,而协程就是将一大堆耗时任务拆开分到很多帧慢慢做,不挤在同一帧。

比如说,假设你要一次性加载100张图片,

普通写法Update或普通函数里一帧直接循环加载100张,这一帧CPU瞬间爆满,画面直接卡死、掉帧。

协程写法就是一帧只加载5张,然后yield return null让出这一帧剩余时间,下一帧在接着加载5张,把压力分摊到几十帧里,每一帧工作量都很小,就不会卡。

yield return null返回出这一帧剩余的时间后不是什么都不干,而是将CPU时间让出去,这时候这些时间CPU可能会给:

  • 其他所有脚本的Update
  • 其他正在跑的协程
  • 等等。。

需要注意的是:协程并不能让加载变快,而是避免游戏卡死,这里可以举一个例子来区分二者:

  • 情况A:不分帧,一帧怼完
    • 游戏画面直接卡死、黑屏、鼠标不动、UI 点不了
  • 情况B:协程分帧慢慢加载
    • 游戏画面照常刷新、UI 能动、按钮能点、动画在播、背景音乐不停

那要怎么才能真正提高游戏的加载速度呢?异步加载。


异步加载就是说页面不用等某个资源加载完,再继续渲染后面内容,而是在后台悄悄加载,加载完成后再回调使用,不阻塞主线程、不卡住画面。

UniTask

UniTask 是专为Unity引擎打造的高性能、零GC异步编程库。


这时候就有小伙伴要问了,小草小草,GC是什么啊?所以,我们先来回顾一下GC的概念。

GC(Garbage Collection),顾名思义,他是垃圾回收的意思。

我们都知道,内存分为两块:栈、堆。

  • 栈内存:速度极快、空间小、自动分配自动释放。
    • 存:int、float、bool、结构体(struct)、局部变量。
    • 用完立刻自己消失,完全不用 GC 管。
  • 堆内存:空间大、速度慢、需要人管理。
    • 存:所有 class 类对象、new 出来的东西、字符串、数组、集合。

GC在垃圾回收的时候做了三件事:

  • 标记:遍历所有内存,标记哪些对象还在被使用,哪些已经废弃。
  • 清除:把废弃的垃圾对象直接删掉,释放内存。
  • 整理(压缩):把零散空闲内存拼在一起,避免内存碎片。

当堆内存不够用了或者达到系统阈值时,就会触发GC。

但是现在有一个关键问题:GC工作时,会短暂暂停游戏主线程,表现就是:瞬间卡顿、掉帧。而协程的一大痛点在于:每次yield return null或者返回别的什么,都要经历一次装箱,也就是把值类型装成引用类型的object,这样会频繁触发GC,导致卡顿。

而UniTask基于值(struct)设计,零GC,解决Unity异步开发的核心痛点。

使用UniTask在 Window - Package Manager - 左上角+号 - Add package from git URL - 输入UniTask的Git地址:

1
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask

下载UniTask导入项目:

  • 需要引用命名空间
1
using Cysharp.Threading.Tasks;
  • 核心规则:

    • 异步方法返回值:async UniTask 或者 async UniTask<T>
    • 内部方法用await
    • 物体销毁时用GetCancellationTokenOnDestory()防内存泄漏
  • 延时等待

1
2
3
4
// 等待1秒,受 TimeScale 影响 
await UniTask.Delay(1000);
// 等待1秒,不受 TimeScale 影响
await UniTask.Delay(1000, ignoreTimeScale:true);
  • 等待下一帧、等待 N 帧
1
2
await UniTask.Yield(); // 等下一帧
await UniTask.DelayFrame(5);// 等5帧