找回密码
 立即注册
查看: 266|回复: 0

UniTask中文使用指南(一)

[复制链接]
发表于 2022-10-4 07:43 | 显示全部楼层 |阅读模式
简介

UniTask优势

  • 与原生 Task/ValueTask/IValueTaskSource 高度兼容的行为
  • 为Unity提供一个高性能,0GC的async/await异步方案。
  • 基于值类型的UniTask<T>和自定义的 AsyncMethodBuilder 来实现0GC
  • 使所有 Unity 的 AsyncOperations 和 Coroutines 可等待
  • 基于 PlayerLoop 的任务( UniTask.Yield, UniTask.Delay, UniTask.DelayFrame, etc…) 可以替换所有协程操作
  • 对MonoBehaviour 消息事件和 uGUI 事件进行 可等待/异步枚举 拓展
  • 完全在 Unity 的 PlayerLoop 上运行,因此不使用Thread,并且同样能在 WebGL、wasm 等平台上运行。
  • 带有 Channel 和 AsyncReactiveProperty的异步 LINQ
  • 提供一个 TaskTracker EditorWindow 以追踪所有UniTask分配来预防内存泄漏

为什么需要 UniTask(自定义类似Task对象)?
因为原生 Task 太重,与 Unity 线程(单线程)不匹配。UniTask 不使用线程和SynchronizationContext/ExecutionContext,因为 Unity 的异步对象由 Unity 的引擎层自动调度。它实现了更快和更低的分配,并且与Unity完全兼容。
<hr/>插件使用要求

UniTask 功能依赖于 C# 7.0(task-like custom async method builder feature) 所以需要的 Unity 最低版本是Unity 2018.3 ,官方支持的最低版本是Unity 2018.4.13f1.
<hr/>语法入门

使用UniTask所需的命名空间 using Cysharp.Threading.Tasks;
你可以返回一个形如 UniTask<T>(或 UniTask) 的类型,这种类型是为Unity定制的,作为替代原生Task<T>的轻量级方案,为Unity集成的 0GC,快速调用,0消耗的 async/await 方案
async UniTask<string> DemoAsync()
{
    // 你可以等待一个Unity异步对象
    var asset = await Resources.LoadAsync<TextAsset>("foo");
    var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
    await SceneManager.LoadSceneAsync("scene2");

    // .WithCancellation 会启用取消功能,GetCancellationTokenOnDestroy 表示获取一个依赖对象生命周期的Cancel句柄,当对象被销毁时,将会调用这个Cancel句柄,从而实现取消的功能
    var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());

    // .ToUniTask 可接收一个 progress 回调以及一些配置参数,Progress.Create是IProgress<T>的轻量级替代方案
    var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));

    // 等待一个基于帧的延时操作(就像一个协程一样)
    await UniTask.DelayFrame(100);

    // yield return new WaitForSeconds/WaitForSecondsRealtime 的替代方案
    await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
   
    // 可以等待任何 playerloop 的生命周期(PreUpdate, Update, LateUpdate, 等...)
    await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);

    // yield return null 替代方案
    await UniTask.Yield();
    await UniTask.NextFrame();

    // WaitForEndOfFrame 替代方案 (需要 MonoBehaviour(CoroutineRunner))
    await UniTask.WaitForEndOfFrame(this); // 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);

    // 多线程示例,在此行代码后的内容都运行在一个线程池上
    await UniTask.SwitchToThreadPool();

    /* 工作在线程池上的代码 */

    // 转回主线程
    await UniTask.SwitchToMainThread();

    // 获取异步的 webrequest
    async UniTask<string> GetTextAsync(UnityWebRequest req)
    {
        var op = await req.SendWebRequest();
        return op.downloadHandler.text;
    }

    var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
    var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
    var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));

    // 构造一个async-wait,并通过元组语义轻松获取所有结果
    var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);

    // WhenAll简写形式
    var (google2, bing2, yahoo2) = await (task1, task2, task3);

    // 返回一个异步值,或者你也可以使用`UniTask`(无返回值), `UniTaskVoid`(即发即弃[直接调用异步操作而不等待])
    return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}

UniTask入门注意事项

①.约束

这是与.NET Standard 2.1 中引入的ValueTask/IValueTaskSource相同的约束:

  • 多次await实例
  • 多次调用 AsTask
  • 在操作尚未完成时调用 .Result 或 .GetAwaiter().GetResult(),多次调用也是不允许的
混用上述行为更是不被允许的,如果您执行上述任何操作,则结果是未定义。

错误示范
var task = UniTask.DelayFrame(10);
await task;
await task; // 寄了, 抛出异常

②.需要多次await

如果实在需要多次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() (由UniTask内部缓存的结果)
脚本示例:
private async UniTaskVoid DoAsync(CancellationToken token)
{
    try
    {
        var uniTask = GetAsync("Unity", 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 执行await对象的相应原生生命周期方法时返回(如果条件满足的话),而WithCancellation 和 ToUniTask 是从指定的 PlayerLoop 生命周期执行时返回

3.*.ToUniTask(IProgress,PlayerLoopTiming,CancellationToken);
<hr/>延续操作(返回值元组)

1.UniTask.WhenAll(全部完成后...)
2.UniTask.WhenAny(任一完成后...)
脚本示例:
//加载完三张图片后延续
public async UniTaskVoid LoadManyAsync()
{
    // 并行加载.
    var (a, b, c) = await UniTask.WhenAll(
        LoadAsSprite("foo"),
        LoadAsSprite("bar"),
        LoadAsSprite("baz"));
}

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 [这两者的转换是无消耗的])
  • *.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 工厂方法(如DelayFrame)有一个CancellationToken cancellationToken = default的参数
  • Unity 的一些异步操作也有WithCancellation(CancellationToken)和ToUniTask(..., CancellationToken cancellation = default)的拓展方法

使用方法+脚本使用案例
1.传递原生CancellationTokenSource
var cts = new CancellationTokenSource();

cancelButton.onClick.AddListener(() =>
{
    cts.Cancel();
});

await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token);

await UniTask.DelayFrame(1000, cancellationToken: cts.Token);

2.传递MonoBehaviour 的扩展方法创建的GetCancellationTokenOnDestroy
// 这个CancellationTokenSource和this GameObject生命周期相同,当this GameObject Destroy的时候,就会执行Cancel
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());

3.对于async方法链式调用需要链式取消,所有异步方法都建议最后一个参数接受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("Canceled!");
}
同样可以用CancellationToken.ToUniTask创建一个UniTask,当CancellationToken被取消时,这个UniTask将被成功终止。

5.自定义生命周期

  • 当检测到取消时,所有方法都会向上游抛出并传播OperationCanceledException,当异常(不限于OperationCanceledException)没有在异步方法中处理时,它将最终传播到UniTaskScheduler.UnobservedTaskException。接收到的未处理异常的默认行为是将日志写入异常。
  • OperationCanceledException是一个特殊的异常,会被UnobservedTaskException.无视
  • 可以使用UniTaskScheduler.UnobservedExceptionWriteLogType更改日志级别
  • 如果要使用自定义行为,请为UniTaskScheduler.UnobservedTaskException设置一个委托
  • 如果要取消异步 UniTask 方法中的行为,请手动抛出OperationCanceledException
手动抛出OperationCanceledException脚本示例:
public async UniTask<int> FooAsync()
{
    await UniTask.Yield();
    throw new OperationCanceledException();
}

6.如果您处理异常但想忽略(传播到全局cancellation处理的地方),请使用异常过滤器
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(抑制Cancellation抛出)它将返回(bool IsCanceled, T Result),而不是抛出
var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();
if (isCanceled)
{
    // ...
}
注意:只有当你直接调用SuppressCancellationThrow到最源头的方法时才会抑制抛出。否则,返回值将被转换,但整个管道将不会抑制throws
<hr/>超时处理

1.CancelAfterSlim
超时是取消的一种变体。您可以通过CancellationTokenSouce.CancelAfterSlim(TimeSpan)设置超时,并将 CancellationToken 传递给异步方法
脚本示例:
var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5s超时

try
{
    await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(cts.Token);
}
catch (OperationCanceledException ex)
{
    if (ex.CancellationToken == cts.Token)
    {
        UnityEngine.Debug.Log("Timeout");
    }
}
注意
CancellationTokenSouce.CancelAfter是一个原生的api。但是在 Unity 中你不应该使用它,因为它依赖于线程计时器,而上文的CancelAfterSlim是 UniTask 的扩展方法,它使用 PlayerLoop 代替。

2. CreateLinkedTokenSource (将超时与其他cancellation一起使用)
var cancelToken = new CancellationTokenSource();
cancelButton.onClick.AddListener(()=>
{
    cancelToken.Cancel(); // 点击按钮后取消
});

var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 设置5s超时

try
{
    // 链接token
    var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token);

    await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token);
}
catch (OperationCanceledException ex)
{
    if (timeoutToken.IsCancellationRequested)
    {
        UnityEngine.Debug.Log("Timeout.");
    }
    else if (cancelToken.IsCancellationRequested)
    {
        UnityEngine.Debug.Log("Cancel clicked.");
    }
}

3.TimeoutController (优化减少每个调用异步方法超时的 CancellationTokenSource 分配)
a.脚本示例:
TimeoutController timeoutController = new TimeoutController(); // 复用timeoutController

async UniTask FooAsync()
{
    try
    {
        // 你可以通过 timeoutController.Timeout(TimeSpan) 传递到 cancellationToken.
        await UnityWebRequest.Get("http://foo").SendWebRequest()
            .WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5)));
        timeoutController.Reset(); // 当await完成后调用Reset(停止超时计时器,并准备下一次复用)
    }
    catch (OperationCanceledException ex)
    {
        if (timeoutController.IsTimeout())
        {
            UnityEngine.Debug.Log("timeout");
        }
    }
}

b.TimeoutController与其他取消源一起使用
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外部,无法停止超时任务,*.Timeout表示超时时忽略结果。
如果您将一个CancellationToken传递给该方法,它将从任务内部执行,因此可以停止正在运行的任务
<hr/>获取进度

一些Unity的异步操作具有ToUniTask(IProgress<float> progress = null, ...)扩展方法
您不应该使用原生的new System.Progress<T>,因为它每次都会导致GC分配。改为使用Cysharp.Threading.Tasks.Progress。

创建Progress ( progress factory)
方法:

  • Create
  • CreateOnlyValueChanged(仅在进度值更新时调用)
  • 实现IProgress(没有 lambda 分配)
Create使用示例:
//定义
var progress = Progress.Create<float>(x => Debug.Log(x));
//-------------------------以下为示例-------------------------------------
var request = await UnityWebRequest.Get("http://google.co.jp")
    .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("http://google.co.jp")
            .SendWebRequest()
            .ToUniTask(progress: this);
    }
}
<hr/>PlayerLoop

基础使用:
UniTask 在自定义PlayerLoop上运行
基于 playerloop 的方法例如Delay、DelayFrame、asyncOperation.ToUniTask等),它们的参数可填入PlayerLoopTiming
可以检查 Unity 的默认 playerloop的PlayerLoopList.md并注入 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返回下一个调用
UniTask.Yield(PlayerLoopTiming.Update) 在 PreUpdate上调用,它返回相同的帧
UniTask.NextFrame() 保证返回下一帧,您可以认为它的行为与yield return null一致

2.PlayerLoopTiming.Update类似于yield return null
PlayerLoopTiming.Update调用时机:
PlayerLoopTiming.Update类似于yield return null但在 Update(Update 和 uGUI 事件(button.onClick, etc…) 前被调用(在ScriptRunBehaviourUpdate时被调用)

3.yield return null调用时机
在ScriptRunDelayedDynamicFrameRate时被调用

4.PlayerLoopTiming.FixedUpdate类似于WaitForFixedUpdate

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("UniTaskPlayerLoop ready? " + 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();
// 最小化 is Update | FixedUpdate | LastPostLateUpdate
PlayerLoopHelper.Initialize(ref loop, InjectPlayerLoopTimings.Minimum);
<hr/>使用Microsoft.CodeAnalysis.BannedApiAnalyzers

需要注入PlayerLoopTiming,可以为InjectPlayerLoopTimings.Minimum设置BannedSymbols.txt
F:Cysharp.Threading.Tasks.PlayerLoopTiming.Initialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastInitialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.EarlyUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastEarlyUpdate; Isn't injected this PlayerLoop in this project.d
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastFixedUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PostLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.TimeUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastTimeUpdate; Isn't injected this PlayerLoop in this project.<hr/>可以将 RS0030严重性配置为错误



<hr/>当前注意事项


  • UniTask.Yield(without CancellationToken) 是一种特殊类型,返回YieldAwaitable并在 YieldRunner 上运行。它是最轻量和最快的
  • PlayerLoopTiming.LastPostLateUpdate不等同于协程的yield return new WaitForEndOfFrame()
  • 协程的 WaitForEndOfFrame 似乎在 PlayerLoop 完成后运行。一些需要协程结束帧(Texture2D.ReadPixels, ScreenCapture.CaptureScreenshotAsTexture, CommandBuffer, 等) 的方法在 async/await 时无法正常工作,在这些情况下,请将 MonoBehaviour(coroutine runner) 传递给UniTask.WaitForEndOfFrame. 例如,await UniTask.WaitForEndOfFrame(this);是yield return new WaitForEndOfFrame()轻量级0GC的替代方案
  • LoadSceneAsync在等待之后,它会导致开始和继续的不同顺序。所以建议不要使用LoadSceneAsync.ToUniTask
  • 在堆栈跟踪中,您可以检查在 playerloop 中的运行位置
  • 如导入Entities包会将自定义playerloop重置为默认值BeforeSceneLoad并注入 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报告错误, 如果您不需要等待(即发即弃),那么使用UniTaskVoid会更好,不过要解除警告,您需要在尾部添加Forget()

返回值写法:

  • Task->UniTask
  • Task<T>->UniTask<T>
  • void->UniTaskVoid

Forget()脚本示例:
public async UniTaskVoid FireAndForgetMethod()
{
    // do anything...
    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("UniTask.Action");
        await UniTask.Delay(1000);
    }
);
等同于
Action asyncAction =
    () =>
    {
        UniTask.Void(
            async () =>
            {
                Debug.Log("UniTask.Void");
                await UniTask.Delay(1000);
            }
        );
    };
等同于
Action asyncAction =
    () =>
    {
        Func<UniTaskVoid> asyncAction = async () =>
        {
            Debug.Log("UniTask.Void");
            await UniTask.Delay(1000);
        };
        asyncAction().Forget();
    };

补充:
UniTask.Void 直接执行异步委托,无返回值 (即发即弃,Foget会被自动调用)
UniTask.Void(
           async () =>
           {
               Debug.Log("UniTask.Void");
               await UniTask.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在该方法被调用的时候,立即创建一个新的UniTaskUniTask在Create()被调用的那一刻就开始执行了
UniTask.Defer将UniTask创建延迟到await的时机只能await一次,但比Lazy轻
UniTask.Lazy将UniTask创建延迟到await的时机生成AsyncLazy,AsyncLazy.Task可以被await任意次数;它比Defer的成本更高

脚本演示:
UniTask.Create 如何快速创建UniTask,并且立即执行
UniTask.Create(
  async ()=>
  {
    Debug.Log("Create");
    await UniTask.Delay(1000);
    return "11";
  });


UniTask.Defer 快速创建UniTask,创建时不执行,await时才执行
var defer = UniTask.Defer(
    async () =>
    {
       Debug.Log("defer");
       await UniTask.Delay(1000);
       return "defer";
    }
);
await defer;
//注意这里不能多次await,否则报错

UniTask.Lazy 创建AsyncLazy类型的对象,在创建时不执行,在await时执行,与Defer不同,可以await任意次数
var asyncLazy = UniTask.Lazy(
  async () =>
  {
    Debug.Log("asyncLazy");
    await UniTask.Delay(1000);
    return "asyncLazy";
  }
);
await asyncLazy.Task;
await asyncLazy.Task;
<hr/>等待JobSystem的JobHandle

WaitAsync已被添加到JobSystem中的JobHandle。
通过使用这个,你可以切换到任何PlayerLoopTime,然后等待完成
<hr/>UniTaskTracker

对于检查(泄露的)UniTasks 很有用。您可以在Window -> UniTask Tracker中打开跟踪器窗口


窗口按钮功能

  • Enable AutoReload(Toggle) - 自动重新加载
  • Reload - 重新加载视图(重新扫描内存中UniTask实例,并刷新界面)
  • GC.Collect - 调用 GC.Collect
  • Enable Tracking(Toggle) - 开始跟踪异步/等待 UniTask。性能影响:低
  • Enable StackTrace(Toggle) - 在任务启动时捕获 StackTrace。性能影响:高
UniTaskTracker 仅用于调试用途,因为启用跟踪和捕获堆栈跟踪很有用,但会对性能产生重大影响。推荐的用法是启用跟踪和堆栈跟踪以查找任务泄漏并在完成时禁用它们。
<hr/>与原生Task的API对比

使用原生类型:
.NET TypeUniTask Type
IProgress<T>---
CancellationToken---
CancellationTokenSource---

使用 UniTask 类型:
.NET TypeUniTask Type
Task/ValueTaskUniTask
Task<T>/ValueTask<T>UniTask<T>
async voidasync UniTaskVoid
+= async () => { }UniTask.Void, UniTask.Action, UniTask.UnityAction
---UniTaskCompletionSource
TaskCompletionSource<T>UniTaskCompletionSource<T>/AutoResetUniTaskCompletionSource<T>
ManualResetValueTaskSourceCore<T>UniTaskCompletionSourceCore<T>
IValueTaskSourceIUniTaskSource
IValueTaskSource<T>IUniTaskSource<T>
ValueTask.IsCompletedUniTask.Status.IsCompleted()
ValueTask<T>.IsCompletedUniTask<T>.Status.IsCompleted()
new Progress<T>Progress.Create<T>
CancellationToken.Register(UnsafeRegister)CancellationToken.RegisterWithoutCaptureExecutionContext
CancellationTokenSource.CancelAfterCancellationTokenSource.CancelAfterSlim
Channel.CreateUnbounded<T>(false){ SingleReader = true }Channel.CreateSingleConsumerUnbounded<T>
IAsyncEnumerable<T>IUniTaskAsyncEnumerable<T>
IAsyncEnumerator<T>IUniTaskAsyncEnumerator<T>
IAsyncDisposableIUniTaskAsyncDisposable
Task.DelayUniTask.Delay
Task.YieldUniTask.Yield
Task.RunUniTask.RunOnThreadPool
Task.WhenAllUniTask.WhenAll
Task.WhenAnyUniTask.WhenAny
Task.CompletedTaskUniTask.CompletedTask
Task.FromExceptionUniTask.FromException
Task.FromResultUniTask.FromResult
Task.FromCanceledUniTask.FromCanceled
Task.ContinueWithUniTask.ContinueWith
TaskScheduler.UnobservedTaskExceptionUniTaskScheduler.UnobservedTaskException
<hr/>UniTask其他注意事项

线程池限制
大多数 UniTask 方法在单个线程 (PlayerLoop) 上运行,只有UniTask.Run(Task.Run等效)和UniTask.SwitchToThreadPool在线程池上运行。如果您使用线程池,它将无法与 WebGL 等平台兼容。
UniTask.Run现在已弃用。你可以改用UniTask.RunOnThreadPool。并且还要考虑是否可以使用UniTask.Create或UniTask.Void。

promise对象池配置
UniTask 积极缓存异步promise对象以实现零分配(有关技术细节,请参阅博客文章UniTask v2 — Unity 的零分配异步/等待,使用异步 LINQ)。默认情况下,它缓存所有promise
方法:
TaskPool.SetMaxPoolSize 可以配置最大池子数量
TaskPool.GetCacheSizeInfo 返回池中当前缓存的对象

IEnumerator.ToUniTask 限制
不支持WaitForEndOfFrame,WaitForFixedUpdate,Coroutine
生命周期与StartCoroutine不一样,它使用指定PlayerLoopTiming,并且默认情况下PlayerLoopTiming.Update在 MonoBehaviour的Update和StartCoroutine的循环之前运行。
如果您想要从协程到异步的完全兼容转换,请使用IEnumerator.ToUniTask(MonoBehaviour coroutineRunner)重载。它在参数 MonoBehaviour 的实例上执行 StartCoroutine 并等待它在 UniTask 中完成。

关于UnityEditor
UniTask 可以像编辑器协程一样在 Unity 编辑器上运行。但是,有一些限制
UniTask.Delay 的 DelayType.DeltaTime、UnscaledDeltaTime 无法正常工作,因为它们无法在编辑器中获取 deltaTime。因此在 EditMode 上运行,会自动将 DelayType 更改为DelayType.Realtime等待正确的时间。
所有 PlayerLoopTiming 都在EditorApplication.update生命周期上运行。
带-batchmode与-quit不起作用,因为Unity不会运行EditorApplication.update并在一帧后退出。相反,不要使用-quit,用EditorApplication.Exit(0)手动退出.

Profiler下的分配
在 UnityEditor 中,分析器显示编译器生成的 AsyncStateMachine 的分配,但它只发生在调试(开发)构建中。C# 编译器将 AsyncStateMachine 生成为 Debug 构建的类和 Release 构建的结构。

Unity 从 2020.1 开始支持代码优化选项(右下脚)


您可以将 C# 编译器优化更改为 release 以删除开发版本中的 AsyncStateMachine 分配。此优化选项也可以通过设置Compilation.CompilationPipeline-codeOptimization和Compilation.CodeOptimization。

UniTaskSynchronizationContext
Unity 的默认 SynchronizationContext(UnitySynchronizationContext) 在性能上是一个很差的实现。UniTask 绕过SynchronizationContext(和ExecutionContext) ,因此不使用它,但如果存在async Task,则仍然使用它。UniTaskSynchronizationContext是UnitySynchronizationContext性能更好的替代品。
案例:
public class SyncContextInjecter
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    public static void Inject()
    {
        SynchronizationContext.SetSynchronizationContext(new UniTaskSynchronizationContext());
    }
}
这是一个可选的选择,并不总是推荐;UniTaskSynchronizationContext性能不如async UniTask,并且不是完整的 UniTask 替代品。它也不保证与UnitySynchronizationContext完全兼容

.NET Core支持见官方.RM


API References

UniTask 的工厂方法
UniTaskAsyncEnumerable 的工厂/扩展方法

这篇讲的是UniTask本体功能的使用,下一篇会讲到UniTask其他的拓展内容,
创作不易,转载请注明出处,觉得文章不错记得给俺点个赞哈~

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-13 02:07 , Processed in 0.092713 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表