|
文章重写声明:
跟 @烟雨迷离半世殇道个歉,上次发布这篇文章的时候,复制了UniTask中文文档中文章的翻译内容,但没有著名出处,这里将文章进行重写并公开声明哈:
以下文章中每一个字都是亲自翻译UniTask官网的原文后再写上的。
文章的内容与原文不同,进行了重新的编排和案例的补充,大家根据自己的喜好进行食用。
简介
UniTask的特点
- 为 Unity 提供有效的无GC异步/await集成。
- 基于Struct `UniTask<T>` 的自定义 AsyncMethodBuilder,实现零GC,使所有Unity的异步操作和协程可以await
- 基于PlayerLoop的Task( `UniTask.Yield`、 `UniTask.Delay`、 `UniTask.DelayFrame` 等)这使得能够替换所有协程操作
- MonoBehaviour 消息事件和 uGUI 事件为可使用Await/AsyncEnumerable
- 完全在 Unity 的 PlayerLoop 上运行,因此不使用线程,可在 WebGL、wasm 等平台上运行。
- 异步 LINQ,具有Channel和 AsyncReactiveProperty
- 防止内存泄漏的 TaskTracker 窗口
- 与Task/ValueTask/IValueTaskSource 的行为高度兼容
<hr/>为什么需要 UniTask(自定义类似Task对象)?
因为原生 Task 太重,与 Unity 线程(单线程)不匹配。UniTask 不使用线程和SynchronizationContext/ExecutionContext,因为 Unity 的异步对象由 Unity 的引擎层自动调度。它实现了更快和更低的分配,并且与Unity完全集成。
<hr/>插件使用要求
UniTask 功能依赖于 C# 7.0(类似Task的自定义异步方法生成器特性),所以需要的Unity版本是在Unity 2018.3之后,官方支持的最低版本是Unity 2018.4.13f1。
<hr/>语法入门
使用UniTask所需的命名空间 using Cysharp.Threading.Tasks;
您可以将类型返回为 struct UniTask<T>(或 UniTask),它是 Task<T> 的Unity专用轻量级替代方案
,实现0开销(0GC和快速执行)的async/await Unity集成
async UniTask<string> DemoAsync()
{
//您可以await Unity async对象
var asset = await Resources.LoadAsync<TextAsset>(&#34;foo&#34;);
var txt = (await UnityWebRequest.Get(&#34;https://...&#34;).SendWebRequest()).downloadHandler.text;
await SceneManager.LoadSceneAsync(&#34;scene2&#34;);
//.WithCancellation 启用取消方法,GetCancellationTokenOnDestroy 与 GameObject 的生命周期同步
var asset2 = await Resources.LoadAsync<TextAsset>(&#34;bar&#34;).WithCancellation(this.GetCancellationTokenOnDestroy());
//.ToUniTask 接受进度回调(和完整的参数),Progress.Create 是 IProgress<T> 的轻量级替代品
var asset3 = await Resources.LoadAsync<TextAsset>(&#34;baz&#34;).ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
//像协程一样,是await基于帧的操作
await UniTask.DelayFrame(100);
//替换 yield return new WaitForSeconds/WaitForSecondsRealtime
await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
//产生任何播放器循环时间(PreUpdate、Update、LateUpdate 等...)
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
//替换 yield return null
await UniTask.Yield();
await UniTask.NextFrame();
//替换 WaitForEndOfFrame(需要 MonoBehaviour(CoroutineRunner))
await UniTask.WaitForEndOfFrame(this); //这是 MonoBehaviour
//替换 yield return new WaitForFixedUpdate(同 UniTask.Yield(PlayerLoopTiming.FixedUpdate))
await UniTask.WaitForFixedUpdate();
//替换 yield return WaitUntil
await UniTask.WaitUntil(() => isActive == false);
//WaitUntil 的帮助方法
await UniTask.WaitUntilValueChanged(this, x => x.isActive);
//您可以await IEnumerator 协程
await FooCoroutineEnumerator();
//您可以await标准Task
await Task.Run(() => 100);
//多线程,此代码下运行在 ThreadPool 上
await UniTask.SwitchToThreadPool();
/*在线程池上工作*/
//返回主线程(与 UniRx 中的 `ObserveOnMainThread` 相同)
await UniTask.SwitchToMainThread();
//获取async网络请求
async UniTask<string> GetTextAsync(UnityWebRequest req)
{
var op = await req.SendWebRequest();
return op.downloadHandler.text;
}
var task1 = GetTextAsync(UnityWebRequest.Get(&#34;http://google.com&#34;));
var task2 = GetTextAsync(UnityWebRequest.Get(&#34;http://bing.com&#34;));
var task3 = GetTextAsync(UnityWebRequest.Get(&#34;http://yahoo.com&#34;));
//并发async await并通过元组语法轻松获取结果
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
//WhenAll的简写,tuple可以直接await
var (google2, bing2, yahoo2) = await (task1, task2, task3);
//返回async值。(或者您可以使用 `UniTask`(无结果)、`UniTaskVoid`(即发即弃))。
return (asset as TextAsset)?.text ?? throw new InvalidOperationException(&#34;Asset not found&#34;);
}
<hr/>UniTask入门注意事项
①.约束
这与NET Standard 2.1 中引入的[ValueTask/I ValueTaskSource]约束类似:
以下操作绝不应该在ValueTask实例上执行:
- await实例多次
- 多次调用 AsTask
- 在操作尚未完成时使用.Result 或.GetAwaiter().GetResult(),或者多次使用它们
如果执行上述任何操作,结果都是未定义的。
错误示范:
var task = UniTask.DelayFrame(10);
await task;
await task; // 抛出异常
②.如果需要支持await多次
可以使用:
1.UniTask.Lazy 可用于延迟运行UniTask (返回值为AsyncLazy)
- a.因为AsyncLazy是awaitable,所以可以直接await
- b.AsyncLazy可多重await
- c.如果想把它转换成一个UniTask,使用AsyncLazy.Task
脚本示例:
//定义
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)
//-------------------------以下为示例-------------------------------------
var asyncLazy = UniTask.Lazy(Factory);
await asyncLazy; //可以直接await
await asyncLazy.Task; //转换为 UniTask
//UniTask.Lazy 结果可以await任意次数
2.*.Preserve() (内部缓存的结果)
脚本示例:
private async UniTaskVoid DoAsync(CancellationToken token)
{
try
{
var uniTask = GetAsync(&#34;Unity&#34;, token);
// 转换成UniTask,可以用Preserve() await任意次数。
var reusable = uniTask.Preserve();
await reusable;
await reusable;
}
catch (InvalidOperationException e)
{
Debug.LogException(e);
}
}
3.UniTaskCompletionSource(这个会在下文讲到)
③.UniTaskV2删除了UniTask.Result/IsCompleted,需要用GetAwaiter()
④.支持Unity中的异步操作(await),需要引用using Cysharp.Threading.Tasks;
支持的操作:
- AsyncOperation
- ResourceRequest
- AssetBundleRequest
- AssetBundleCreateRequest
- UnityWebRequestAsyncOperation
- AsyncGPUReadbackRequest
- IEnumerator
⑤.UniTask提供了三种模式的扩展方法
1.* await asyncOperation;
AssetBundleRequest 有 `asset` 和 `allAssets`,默认为await返回 `asset`。如果你想得到 `allAssets`,你可以使用 `AwaitForAllAssets()` 方法。
2.* .WithCancellation(CancellationToken);
`WithCancellation` 是 `ToUniTask`的简单版本,两者都会返回 `UniTask`
await 直接从 PlayerLoop 的原生生命周期返回,而 WithCancellation 和 ToUniTask 则从指定的 PlayerLoopTiming 返回
3.*.ToUniTask(IProgress,PlayerLoopTiming,CancellationToken);
<hr/>延续操作(返回值元组)
1.UniTask.WhenAll(全部完成后...)
2.UniTask.WhenAny(任一完成后...)
脚本示例:
//加载完三张图片后延续
public async UniTaskVoid LoadManyAsync()
{
// 并行加载.
var (a, b, c) = await UniTask.WhenAll(
LoadAsSprite(&#34;foo&#34;),
LoadAsSprite(&#34;bar&#34;),
LoadAsSprite(&#34;baz&#34;));
}
async UniTask<Sprite> LoadAsSprite(string path)
{
var resource = await Resources.LoadAsync<Sprite>(path);
return (resource as Sprite);
}
<hr/>转换操作
Task转换
*.AsUniTask (Task -> UniTask)(UniTask<T> -> UniTask [这两者的转换是0消耗的])
*.AsAsyncUnitUniTask (UniTask -> UniTask<AsyncUnit>)
UniTaskCompletionSource<T> Task回调转换为 UniTask
TaskCompletionSource<T>额外讲解
1.UniTaskCompletionSource<T>是原生TaskCompletionSource<T>的轻量级版本。
2.问 原生TaskCompletionSource<T>用来干啥的?
答:用来返回异步回调任务的状态
脚本示例:
public Task<Args> SomeApiWrapper()
{
TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>();
var obj = new SomeApi();
// 任务完成后会收到回调
obj.Done += (args) =>
{
//这将通知SomeApiWrapper的调用者,该任务刚刚完成
tcs.SetResult(args);
}
// 开始任务
obj.Do();
return tcs.Task;
}
UniTaskCompletionSource脚本示例:
public UniTask<int> WrapByUniTaskCompletionSource()
{
var utcs = new UniTaskCompletionSource<int>();
// 当完成时,调用 utcs.TrySetResult();
// 当失败时,调用 utcs.TrySetException();
// 当取消时,调用 utcs.TrySetCanceled();
return utcs.Task; //返回 UniTask<int>
}
异步转换为协程
*.ToCoroutine()
<hr/>取消和异常处理
简述:
CancellationToken表示异步的生命周期
一些UniTask工厂方法有一个CancellationToken cancellationToken = default参数。此外,Unity的一些异步操作有WithCancellation(CancellationToken)和ToUniTask(..., CancellationToken cancellation = default)扩展方法
使用方法+脚本使用案例
1.标准的CancellationTokenSource,将CancellationToken传递给参数
var cts = new CancellationTokenSource();
cancelButton.onClick.AddListener(() =>
{
cts.Cancel();
});
await UnityWebRequest.Get(&#34;http://google.co.jp&#34;).SendWebRequest().WithCancellation(cts.Token);
await UniTask.DelayFrame(1000, cancellationToken: cts.Token);
2.传递MonoBehaviour 的扩展方法建立 `GetCancellationTokenOnDestroy`
// 这个CancellationToken的生命周期与GameObject相同
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());
3.对于传播Cancellation(取消来源),所有的异步方法都建议在最后一个参数中接受CancellationToken cancellationToken,并将CancellationToken从根部传到末端
await FooAsync(this.GetCancellationTokenOnDestroy());
// ---
async UniTask FooAsync(CancellationToken cancellationToken)
{
await BarAsync(cancellationToken);
}
async UniTask BarAsync(CancellationToken cancellationToken)
{
await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken);
}
4.WaitUntilCanceled示例
private void Start()
{
var token = this.GetCancellationTokenOnDestroy();
WaitForCanceledAsync(token).Forget();
}
private async UniTaskVoid WaitForCanceledAsync(CancellationToken token)
{
await token.WaitUntilCanceled();
Debug.Log(&#34;Canceled!&#34;);
}
同样可以用CancellationToken.ToUniTask创建一个UniTask,当CancellationToken被取消时,这个UniTask将被成功终止。
5.自定义生命周期
当检测到取消时,所有方法都会抛出`OperationCanceledException`并向上游传播。当异常(不限于`OperationCanceledException`)在异步方法中没有被处理时,它最终被传播到`UniTaskScheduler.UnobservedTaskException`。收到未处理的异常的默认行为是将日志作为异常写入。
可以使用`UniTaskScheduler.UnobservedExceptionWriteLogType`来改变日志级别。
如果你想使用自定义行为,请为`UniTaskScheduler.UnobservedTaskException`设置一个action。
还有`OperationCanceledException`是一个特殊的异常,这在`UnobservedTaskException`中会被默默地忽略。
如果你想在一个异步UniTask方法中取消行为,请手动抛出OperationCanceledException:
public async UniTask<int> FooAsync()
{
await UniTask.Yield();
throw new OperationCanceledException();
}
6.如果你处理了一个异常,但想忽略(传播到global cancellation handling),请使用一个异常过滤器
public async UniTask<int> BarAsync()
{
try
{
var x = await FooAsync();
return x * 2;
}
catch (Exception ex) when (!(ex is OperationCanceledException)) // when (ex is not OperationCanceledException) at C# 9.0
{
return -1;
}
}
7.throws/catch `OperationCanceledException` 会稍微繁重,因此如果你关心性能,请使用 `UniTask.SuppressCancellationThrow` 来避免 OperationCanceledException 抛出。它会返回 `(bool IsCanceled, T Result)`,而不是抛出。
var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();
if (isCanceled)
{
// ...
}
注意:只有当你直接调用到最源头方法时才会抑制抛出。否则,返回值将被转换,但整个管道将不会抑制抛出
<hr/>超时处理
1.CancelAfterSlim
超时是取消的一种变体。您可以通过CancellationTokenSouce.CancelAfterSlim(TimeSpan)设置超时,并将 CancellationToken 传递给异步方法
脚本示例:
var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5秒超时
try
{
await UnityWebRequest.Get(&#34;http://foo&#34;).SendWebRequest().WithCancellation(cts.Token);
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == cts.Token)
{
UnityEngine.Debug.Log(&#34;Timeout&#34;);
}
}
注意
CancellationTokenSouce.CancelAfter是一个标准api。但是在Unity中你不应该使用它,因为它依赖于线程定时器。而上文中的CancelAfterSlim是UniTask的扩展方法,它使用PlayerLoop代替。
2. CreateLinkedTokenSource (timeout 与其他cancellation(取消来源)一起使用)
var cancelToken = new CancellationTokenSource();
cancelButton.onClick.AddListener(()=>
{
cancelToken.Cancel(); //点击按钮取消
});
var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); //5秒超时
try
{
//组合token
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token);
await UnityWebRequest.Get(&#34;http://foo&#34;).SendWebRequest().WithCancellation(linkedTokenSource.Token);
}
catch (OperationCanceledException ex)
{
if (timeoutToken.IsCancellationRequested)
{
UnityEngine.Debug.Log(&#34;Timeout.&#34;);
}
else if (cancelToken.IsCancellationRequested)
{
UnityEngine.Debug.Log(&#34;Cancel clicked.&#34;);
}
}
3.TimeoutController (优化减少每次调用异步方法超时的CancellationTokenSource的GC分配)
a.脚本示例:
TimeoutController timeoutController = new TimeoutController(); //设置到字段以供重用。
async UniTask FooAsync()
{
try
{
//您可以将 timeoutController.Timeout(TimeSpan) 传递给 cancelToken。
await UnityWebRequest.Get(&#34;http://foo&#34;).SendWebRequest()
.WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5)));
timeoutController.Reset(); //成功时调用Reset(停止超时计时器并准备重用)。
}
catch (OperationCanceledException ex)
{
if (timeoutController.IsTimeout())
{
UnityEngine.Debug.Log(&#34;timeout&#34;);
}
}
}
b.TimeoutController 与其他cancellation(取消来源)一起使用
new TimeoutController(CancellationToken)
脚本示例:
TimeoutController timeoutController;
CancellationTokenSource clickCancelSource;
void Start()
{
this.clickCancelSource = new CancellationTokenSource();
this.timeoutController = new TimeoutController(clickCancelSource);
}
c.方法
- *.Timeout
- *.TimeoutWithoutException
d.注意事项:
UniTask 有 `.Timeout`, `.TimeoutWithoutException` 方法,但是,如果可能,不要使用这些,请通过 `CancellationToken`。因为 `.Timeout` 工作来自Task的外部,所以无法停止超时Task。 `.Timeout` 表示超时时忽略结果。
如果你将传递 `CancellationToken` 至方法,它会从工作内部执行,因此可以停止执行中的工作。
<hr/>获取进度
- Unity 的一些AsyncOperations具有 `ToUniTask(IProgress<float> progress = null,...)` 扩展方法
- 你不应该使用 standard `new System.Progress<T>`,因为它每次都会导致GC。请改用`Cysharp.Threading.Tasks.Progress` 。
创建Progress ( progress factory)
方法:
- Create
- CreateOnlyValueChanged(进度值已变更时,才会调用)
- 对调用方实现IProgress(没有 lambda GC)
Create使用示例:
//定义
var progress = Progress.Create<float>(x => Debug.Log(x));
//-------------------------以下为示例-------------------------------------
var request = await UnityWebRequest.Get(&#34;http://google.co.jp&#34;)
.SendWebRequest()
.ToUniTask(progress: progress);
实现IProgress脚本示例:
public class Foo : MonoBehaviour, IProgress<float>
{
public void Report(float value)
{
UnityEngine.Debug.Log(value);
}
public async UniTaskVoid WebRequest()
{
var request = await UnityWebRequest.Get(&#34;http://google.co.jp&#34;)
.SendWebRequest()
.ToUniTask(progress: this);
}
}
<hr/>PlayerLoop
基础使用:
UniTask 是在一个自定义的PlayerLoop上运行的。UniTask 基于PlayerLoop的方法(例如 `Delay`、 `DelayFrame`、 `asyncOperation.ToUniTask` 等...)接受这个 `PlayerLoopTiming`。
它指示何时运行,你可以查看PlayerLoopList.md用Unity 的默认PlayerLoop注入 UniTask 的自定义循环。
PlayerLoopTiming:
public enum PlayerLoopTiming
{
Initialization = 0,
LastInitialization = 1,
EarlyUpdate = 2,
LastEarlyUpdate = 3,
FixedUpdate = 4,
LastFixedUpdate = 5,
PreUpdate = 6,
LastPreUpdate = 7,
Update = 8,
LastUpdate = 9,
PreLateUpdate = 10,
LastPreLateUpdate = 11,
PostLateUpdate = 12,
LastPostLateUpdate = 13
#if UNITY_2020_2_OR_NEWER
TimeUpdate = 14,
LastTimeUpdate = 15,
#endif
}
常用playerLoop说明
1.yield return null和UniTask.Yield
yield return null和UniTask.Yield相似但不同。 yield return null总是返回下一帧,但UniTask.Yield返回下一个调用。也就是说,在PreUpdate上调用UniTask.Yield(PlayerLoopTiming.Update),会返回同一帧。UniTask.NextFrame()保证返回下一帧,你可以期望它的行为与yield return null完全相同。
2.PlayerLoopTiming.Update类似于yield return null
但是它在Update之前被调用(ScriptRunBehaviourUpdate调用Update和uGUI事件(button.onClick等)
3.yield return null调用时机
ScriptRunDelayedDynamicFrameRate调用 yield return null
4.PlayerLoopTiming.FixedUpdate类似于WaitForFixedUpdate
PlayerLoop 初始化:
- 默认情况下,UniTask的PlayerLoop在[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]初始化
- 在 BeforeSceneLoad 中调用方法的顺序是不确定的,因此如果要在其他 BeforeSceneLoad 方法中使用 UniTask,应尝试在此之前对其进行初始化,脚本示例:
// AfterAssembliesLoaded在BeforeSceneLoad之前被调用
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
public static void InitUniTaskLoop()
{
var loop = PlayerLoop.GetCurrentPlayerLoop();
Cysharp.Threading.Tasks.PlayerLoopHelper.Initialize(ref loop);
}
诊断 UniTask 的PlayerLoop:
1.PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()
诊断 UniTask 的PlayerLoop是否就绪
2.PlayerLoopHelper.DumpCurrentPlayerLoop
将所有当前PlayerLoop记录到控制台
脚本示例:
void Start()
{
UnityEngine.Debug.Log(&#34;UniTaskPlayerLoop ready? &#34; + PlayerLoopHelper.IsInjectedUniTaskPlayerLoop());
PlayerLoopHelper.DumpCurrentPlayerLoop();
}
删除未使用 PlayerLoopTiming 注入来略微优化循环开销
三个预设InjectPlayerLoopTimings
All (默认)
Standard(除LastPostLateUpdate外)
Minimum(Update | FixedUpdate | LastPostLateUpdate)
可以自定义组合
如 `InjectPlayerLoopTimings.Update | InjectPlayerLoopTimings.FixedUpdate | InjectPlayerLoopTimings.PreLateUpdate`
脚本示例(初始化时调用PlayerLoopHelper.Initialize(InjectPlayerLoopTimings)
var loop = PlayerLoop.GetCurrentPlayerLoop();
//最少是 Update | FixedUpdate | LastPostLateUpdate
PlayerLoopHelper.Initialize(ref loop, InjectPlayerLoopTimings.Minimum);
<hr/>配置Microsoft.CodeAnalysis.BannedApiAnalyzers
你可以像这样为InjectPlayerLoopTimings.Minimum设置BannedSymbols.txt
F:Cysharp.Threading.Tasks.PlayerLoopTiming.Initialization; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastInitialization; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.EarlyUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastEarlyUpdate; Isn&#39;t injected this PlayerLoop in this project.d
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastFixedUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreLateUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreLateUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PostLateUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.TimeUpdate; Isn&#39;t injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastTimeUpdate; Isn&#39;t injected this PlayerLoop in this project.可以将 `RS0030` 严重性配置为错误
<hr/>当前注意事项
1.UniTask.Yield(不带CancellationToken)是一个特殊的类型,返回YieldAwaitable并在YieldRunner上运行。它是最轻量级和最快的
2.PlayerLoopTiming.LastPostLateUpdate不等同于coroutine的yield return new WaitForEndOframe()
3.Coroutine的WaitForEndOfFrame似乎在PlayerLoop完成后才运行。一些需要coroutine的结束帧的方法(Texture2D.ReadPixels,ScreenCapture.CaptureScreenshotAsTexture,CommandBuffer等)在被替换成async/await后不能正确工作。在这些情况下,将MonoBehaviour(coroutine runnner)传递给UniTask.WaitForEndOfFrame。例如,await UniTask.WaitForEndOfFrame(this);是 &#34;yield return new WaitForEndOfFrame() &#34;轻量级的无分配的替代方案。
4.在UniTask中,await直接使用原生生命周期,而WithCancellation和ToUniTask使用指定的生命周期。这通常不是一个特别的问题,但是在LoadSceneAsync中,它会导致在await之后的Start和continuation的顺序不同。所以建议不要使用LoadSceneAsync.ToUniTask。
5.在 stacktrace 中,你可以检查它在 playerloop 中的运行位置。
6.如果你导入了Unity的Entities包,那就会在BeforeSceneLoad时将自定义PlayerLoop重置为默认值,并注入ECS的循环。当Unity在UniTask的initialize方法之后调用ECS的inject方法,UniTask将不再工作。
为了解决这个问题,你可以在ECS初始化后重新初始化UniTask的PlayerLoop,脚本示例:
// 获取ECS Loop。
var playerLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;
// 设置UniTask的PlayerLoop
PlayerLoopHelper.Initialize(ref playerLoop);
<hr/>返回值(*)
async void 与 async UniTaskVoid 对比:
1.async void是一个标准的C#Task系统,所以它不能在UniTask系统上运行。
2.async UniTaskVoid是async UniTask的一个轻量级版本,因为它没有可等待的完成,并立即向UniTaskScheduler.UnobservedTaskException报告错误。如果你不需要await(fire and forget),使用UniTaskVoid会更好,不过需要解除警告,你需要调用Forget()。
返回值写法:
- Task->UniTask
- Task<T>->UniTask<T>
- void->UniTaskVoid
Forget()脚本示例:
public async UniTaskVoid FireAndForgetMethod()
{
// 干点啥...
await UniTask.Yield();
}
public void Caller()
{
FireAndForgetMethod().Forget();
}
3.UniTask也有Forget方法,它与UniTaskVoid类似,具有相同的效果。然而,如果你完全不使用await,UniTaskVoid会更有效率。
脚本示例:
public async UniTask DoAsync()
{
// do anything...
await UniTask.Yield();
}
public void Caller()
{
DoAsync().Forget();
}
4.`UniTaskVoid` 也可以用于 MonoBehaviour 的 `Start` 方法。
脚本示例:
class Sample : MonoBehaviour
{
async UniTaskVoid Start()
{
// async init code.
}
}
<hr/>注册到Event的异步委托(lambda)
不要使用async void。可以使用UniTask.Action或UniTask.UnityAction,它们都通过async UniTaskVoid lambda创建一个委托。
将异步委托转换为 Action/UnityAction并注册到Event的脚本示例:
Action actEvent;
UnityAction unityEvent; // 特别是在uGUI中使用
// 这是不好的: async void
actEvent += async () => { };
unityEvent += async () => { };
// 这是好的: 通过lamada创建Action
actEvent += UniTask.Action(async () => { await UniTask.Yield(); });
unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });
注意:
Action asyncAction = UniTask.Action(
async () =>
{
Debug.Log(&#34;UniTask.Action&#34;);
await UniTask.Delay(1000);
}
);
等同于
Action asyncAction =
() =>
{
UniTask.Void(
async () =>
{
Debug.Log(&#34;UniTask.Void&#34;);
await UniTask.Delay(1000);
}
);
};
等同于
Action asyncAction =
() =>
{
Func<UniTaskVoid> asyncAction = async () =>
{
Debug.Log(&#34;UniTask.Void&#34;);
await UniTask.Delay(1000);
};
asyncAction().Forget();
};
补充:
UniTask.Void 直接执行异步委托,无返回值 (即发即弃,Foget会被自动调用)
UniTask.Void(async()=>{Debug.Log(&#34;UniTask.Void&#34;);awaitUniTask.Delay(1000);});
<hr/>委托中生成UniTask的三种工厂方法
脚本定义:
public static UniTask<T> Create<T>(Func<UniTask<T>>factory)
public static UniTask<T> Defer<T>(Func<UniTask<T>>factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>>factory)
他们仨参数都相同,但它们的行为略有不同
方法名 | 创建时机 | 备注(执行时机,注意事项) | UniTask.Create | 在该方法被调用的时候,立即创建一个新的UniTask | UniTask在Create()被调用的那一刻就开始执行了 | UniTask.Defer | 将UniTask创建延迟到await的时机 | 只能await一次,但比Lazy轻 | UniTask.Lazy | 将UniTask创建延迟到await的时机 | 生成AsyncLazy,AsyncLazy.Task可以被await任意次数;它比Defer的成本更高 |
脚本演示:
UniTask.Create 如何快速创建UniTask,并且立即执行
UniTask.Create(
async ()=>
{
Debug.Log(&#34;Create&#34;);
await UniTask.Delay(1000);
return &#34;11&#34;;
});
UniTask.Defer 快速创建UniTask,创建时不执行,await时才执行
var defer = UniTask.Defer(
async () =>
{
Debug.Log(&#34;defer&#34;);
await UniTask.Delay(1000);
return &#34;defer&#34;;
}
);
await defer;
//注意这里不能多次await,否则报错
UniTask.Lazy 创建AsyncLazy类型的对象,在创建时不执行,在await时执行,与Defer不同,可以await任意次数
var asyncLazy = UniTask.Lazy(
async () =>
{
Debug.Log(&#34;asyncLazy&#34;);
await UniTask.Delay(1000);
return &#34;asyncLazy&#34;;
}
);
await asyncLazy.Task;
await asyncLazy.Task;
<hr/>等待JobSystem的JobHandle
WaitAsync已被添加到JobSystem中的JobHandle。
通过使用这个,你可以切换到任何PlayerLoopTime,然后等待完成
<hr/>UniTaskTracker
对检查(泄漏的)UniTask很有用。你可以在Window -> UniTask Tracker中打开追踪器窗口。
窗口按钮功能
- Enable AutoReload(Toggle) - 自动重新加载
- Reload -重新加载
- GC.Collect - 调用GC.Collect
- Enable Tracking(Toggle) - 开始跟踪async/await UniTask。性能影响:低
- Enable StackTrace(Toggle) - 当Task启动时捕获堆栈跟踪。性能影响:高
UniTaskTracker仅用于调试,因为启用跟踪和捕获堆栈跟踪很有用,但对性能影响很大。推荐的用法是同时启用跟踪和堆栈跟踪,以发现Task泄漏,并在完成后同时禁用它们。
<hr/>与原生Task的API对比
使用原生类型:
.NET 类型 | UniTask 类型 | IProgress<T> | --- | CancellationToken | --- | CancellationTokenSource | --- | 使用 UniTask 类型:
.NET 类型 | UniTask 类型 | Task/ ValueTask | UniTask | Task<T>/ ValueTask<T> | UniTask<T> | async void | async UniTaskVoid | += async () => { } | UniTask.Void, UniTask.Action, UniTask.UnityAction | --- | UniTaskCompletionSource | TaskCompletionSource<T> | UniTaskCompletionSource<T>/ AutoResetUniTaskCompletionSource<T> | ManualResetValueTaskSourceCore<T> | UniTaskCompletionSourceCore<T> | IValueTaskSource | IUniTaskSource | IValueTaskSource<T> | IUniTaskSource<T> | ValueTask.IsCompleted | UniTask.Status.IsCompleted() | ValueTask<T>.IsCompleted | UniTask<T>.Status.IsCompleted() | new Progress<T> | Progress.Create<T> | CancellationToken.Register(UnsafeRegister) | CancellationToken.RegisterWithoutCaptureExecutionContext | CancellationTokenSource.CancelAfter | CancellationTokenSource.CancelAfterSlim | Channel.CreateUnbounded<T>(false){ SingleReader = true} | Channel.CreateSingleConsumerUnbounded<T> | IAsyncEnumerable<T> | IUniTaskAsyncEnumerable<T> | IAsyncEnumerator<T> | IUniTaskAsyncEnumerator<T> | IAsyncDisposable | IUniTaskAsyncDisposable | Task.Delay | UniTask.Delay | Task.Yield | UniTask.Yield | Task.Run | UniTask.RunOnThreadPool | Task.WhenAll | UniTask.WhenAll | Task.WhenAny | UniTask.WhenAny | Task.CompletedTask | UniTask.CompletedTask | Task.FromException | UniTask.FromException | Task.FromResult | UniTask.FromResult | Task.FromCanceled | UniTask.FromCanceled | Task.ContinueWith | UniTask.ContinueWith | TaskScheduler.UnobservedTaskException | UniTaskScheduler.UnobservedTaskException | <hr/>UniTask其他注意事项
线程池限制
大多数 UniTask 方法在单个线程(PlayerLoop)上运行,只有 `UniTask.Run`( `Task.Run` 等效的)和 `UniTask.SwitchToThreadPool` 在线程池上运行。如果你使用线程池,它将无法与 WebGL 等一起工作。
`UniTask.Run` 现在已被取代。你可以改用 `UniTask.RunOnThreadPool`。并且还要考虑是否可以使用 `UniTask.Create` 或 `UniTask.Void`
promise对象池配置
UniTask积极地缓存异步promise对象,以实现零GC(关于技术细节,见博文UniTask v2 - Zero Allocation async/await for Unity, with Asynchronous LINQ)。默认情况下,它缓存了所有的promise
方法:
TaskPool.SetMaxPoolSize 每种类型的缓存大小
TaskPool.GetCacheSizeInfo 返回当前池中的缓存对象
IEnumerator.ToUniTask 限制
不支持WaitForEndOframe/WaitForFixedUpdate/Coroutine
生命周期与StartCoroutine不一样,它使用指定的PlayerLoopTiming,默认的PlayerLoopTiming.Update会在MonoBehaviour的Update和StartCoroutine的循环之前运行。
如果你想完全兼容从coroutine到async的转换,使用IEnumerator.ToUniTask(MonoBehaviour coroutineRunner)重载。它在参数MonoBehaviour的一个实例上执行StartCoroutine,并在UniTask中等待它的完成。
对于UnityEditor
- UniTask 可以像编辑器协同程序一样在 Unity 编辑器上运行。但是,也有一些限制,
UniTask.Delay 的 DelayType.DeltaTime、UnscaledDeltaTime 无法正常工作,因为它们无法在编辑器中获取 deltaTime。
- 因此在 EditMode 上运行时,会自动将 DelayType 改为 `DelayType.Realtime`,await合适的时间。
- 所有的PlayerLoopTiming都在EditorApplication.update的定时下运行。
- `-batchmode` 和`-quit` 不起作用,因为 Unity 不会运行 `EditorApplication.update` 并在一帧后退出。
- 请不要使用 `-quit`,用`EditorApplication.Exit(0)`手动退出 。
Profiler下的GC
在UnityEditor中,Profiler显示了编译器生成的AsyncStateMachine的分配,但它只发生在调试(开发)构建中。C#编译器在 &#34;Debug 构建 &#34;时将AsyncStateMachine生成为类,在 &#34;Release构建 &#34;时生成为结构。
Unity从2020.1开始支持代码优化选项(右下脚)。
你可以将C#编译器优化改为release,以在开发构建中移除AsyncStateMachine的GC。这个优化选项也可以通过Compilation.CompilationPipeline-codeOptimization,和Compilation.CodeOptimization来设置。
UniTaskSynchronizationContext
Unity默认的SynchronizationContext(UnitySynchronizationContext)在性能上是一个很差的实现。UniTask绕过了SynchronizationContext(和ExecutionContext),所以它不使用它,但如果存在于asyn Task中,仍然使用它。UniTaskSynchronizationContext是UnitySynchronizationContext的替代品,其性能更好。
案例:
public class SyncContextInjecter
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
public static void Inject()
{
SynchronizationContext.SetSynchronizationContext(new UniTaskSynchronizationContext());
}
}
这是一个可选的,并不总是被推荐;UniTaskSynchronizationContext的性能不如async UniTask,也不是一个完整的UniTask替代品。它也不能保证与UnitySynchronizationContext在行为上完全兼容。
<hr/>.NET Core支持见官方.RM
<hr/>API References
UniTask 的工厂方法
UniTaskAsyncEnumerable 的工厂/扩展方法
这篇讲的是UniTask本体功能的使用,下一篇会讲到UniTask其他的拓展内容,
创作不易,转载请注明出处,觉得文章不错记得给俺点个赞哈~ |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|