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

UniTask中文使用指南(一)

[复制链接]
发表于 2022-10-12 08:39 | 显示全部楼层 |阅读模式
文章重写声明:

跟 @烟雨迷离半世殇道个歉,上次发布这篇文章的时候,复制了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>("foo");
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
await SceneManager.LoadSceneAsync("scene2");

//.WithCancellation 启用取消方法,GetCancellationTokenOnDestroy 与 GameObject 的生命周期同步
var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());

//.ToUniTask 接受进度回调(和完整的参数),Progress.Create 是 IProgress<T> 的轻量级替代品
var asset3 = await Resources.LoadAsync<TextAsset>("baz").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("http://google.com"));
var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));

//并发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("Asset not found");
}
<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("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 的原生生命周期返回,而 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("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 [这两者的转换是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("http://google.co.jp").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("Canceled!");
}
同样可以用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("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 (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("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的GC分配)
a.脚本示例:
TimeoutController timeoutController = new TimeoutController(); //设置到字段以供重用。

async UniTask FooAsync()
{
    try
    {
        //您可以将 timeoutController.Timeout(TimeSpan) 传递给 cancelToken。
        await UnityWebRequest.Get("http://foo").SendWebRequest()
            .WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5)));
        timeoutController.Reset(); //成功时调用Reset(停止超时计时器并准备重用)。
    }
    catch (OperationCanceledException ex)
    {
        if (timeoutController.IsTimeout())
        {
            UnityEngine.Debug.Log("timeout");
        }
    }
}

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("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上运行的。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("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();
//最少是 Update | FixedUpdate | LastPostLateUpdate
PlayerLoopHelper.Initialize(ref loop, InjectPlayerLoopTimings.Minimum);
<hr/>配置Microsoft.CodeAnalysis.BannedApiAnalyzers

你可以像这样为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.可以将 `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);是 "yield return new WaitForEndOfFrame() "轻量级的无分配的替代方案。
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("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");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在该方法被调用的时候,立即创建一个新的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

对检查(泄漏的)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/ 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对象,以实现零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#编译器在 "Debug 构建 "时将AsyncStateMachine生成为类,在 "Release构建 "时生成为结构。
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其他的拓展内容,
创作不易,转载请注明出处,觉得文章不错记得给俺点个赞哈~

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-11-22 01:38 , Processed in 0.093718 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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