boxmoe_header_banner_img

菜就多练喵

文章导读

[C#][Unity][Ib_Core]Ib_Async——基于UniTask的通用延迟调用方法<Ib_Async.DelayDoSomethingUniTask>


avatar
Ib_Mccf 2025年11月8日 59

UniTask地址:GitHub – Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

功能:令一个方法在n秒后被调用。

特点:一行语句即可实现延迟调用,支持Action的多参数传入,适当操作可避免产生GC。

示例:三秒后死亡。

void Start()
{
    Debug.Log($"[{Time.time}]>>调用时间");
    Ib_Async.DelayDoSomethingUniTask(3,Die,this.GetCancellationTokenOnDestroy());
}

void Die()
{
    Debug.Log($"[{Time.time}]>>执行时间 => 我死了");
}

输出结果:

与其他延迟方法对比(每帧调用500次延迟方法):

1、基于UniTask的Ib_Async。可以看到每帧调用了Ib_Async.DelayDoSomethingUniTask500次,耗时2.79ms左右,没有GC的产生。但是在IncreaseGold方法转换成Action<int>的时候产生了62.5KB的GC,为了避免这部分产生的GC我们可以在Start里提前将IncreaseGold转换成Action<int>。

public int gold = 0;
void IncreaseGold(int cnt)
{
    gold += cnt;
}

private void Update()
{
    Test_DelayDoSomethingUnitask();
}

void Test_DelayDoSomethingUnitask()
{
    for (int i = 0; i < 500; i++)
    {
        Ib_Async.DelayDoSomethingUniTask(3,IncreaseGold,1,this.GetCancellationTokenOnDestroy());            
    }
}

简单修改后,避免了频繁创建Action<int>,可以看到每帧调用500次后没有GC产生。

    public int gold = 0;
    void IncreaseGold(int cnt)
    {
        gold += cnt;
    }
    private Action<int> increaseGoldAction;
    void Start()
    {
        increaseGoldAction = IncreaseGold;
    }
    private void Update()
    {
        Test_DelayDoSomethingUnitask();
    }
    void Test_DelayDoSomethingUnitask()
    {
        for (int i = 0; i < 500; i++)
        {
            Ib_Async.DelayDoSomethingUniTask(3,increaseGoldAction,1,CancellationToken.None);            
        }
    }

2、通过协程来延迟调用(协程也同样可以实现通用的延迟方法,这里就简单实现了)。可以看出每帧耗时仅1.56ms左右,比UniTask快上了一些,但是产生了44.9KB的GC,因为每次启动协程以及yield return new WaitForSeconds(delay);的时候都会进行内存分配。我们可以复用WaitForSeconds来避免一部分的GC。

    public int gold = 0;
    void IncreaseGold(int cnt)
    {
        gold += cnt;
    }
    private Action<int> increaseGoldAction;
    void Start()
    {
        increaseGoldAction = IncreaseGold;
    }
    private void Update()
    {
        Test_DelayDoSomethingUnitask();
    }
    void Test_DelayDoSomethingUnitask()
    {
        for (int i = 0; i < 500; i++)
        {
            StartCoroutine(DelayDoSomething(3, increaseGoldAction, 1));
            // Ib_Async.DelayDoSomethingUniTask(3,increaseGoldAction,1,CancellationToken.None);            
        }
    }
    IEnumerator DelayDoSomething(float delay,Action<int> action,int arg1)
    {
        yield return new WaitForSeconds(delay);
        action?.Invoke(arg1);
    }

缓存WaitForSeconds,避免频繁创建产生GC。这次的GC减少到了39.1KB,但是开启协程的开销无法避免。

    public int gold = 0;
    void IncreaseGold(int cnt)
    {
        gold += cnt;
    }
    private Action<int> increaseGoldAction;
    void Start()
    {
        increaseGoldAction = IncreaseGold;
    }
    private void Update()
    {
        Test_DelayDoSomethingUnitask();
    }
    void Test_DelayDoSomethingUnitask()
    {
        for (int i = 0; i < 500; i++)
        {
            StartCoroutine(DelayDoSomething(3, increaseGoldAction, 1));
            // Ib_Async.DelayDoSomethingUniTask(3,increaseGoldAction,1,CancellationToken.None);            
        }
    }
    
    Dictionary<float,WaitForSeconds> waitForSecondsDict = new Dictionary<float,WaitForSeconds>();

    //缓存WaitForSeconds
    private WaitForSeconds TryGetWaitForSeconds(float delay)
    {
        float key = Mathf.Round(delay * 1000f) / 1000f;
        if (waitForSecondsDict.TryGetValue(key, out WaitForSeconds wait)) return wait;
        wait = new WaitForSeconds(key);
        waitForSecondsDict[key] = wait;
        return wait;
    }

    IEnumerator DelayDoSomething(float delay,Action<int> action,int arg1)
    {
        yield return TryGetWaitForSeconds(delay);
        action?.Invoke(arg1);
    }

结论:经过每帧500次的调用测试,可以看出基于UniTask的Ib_Async虽然相比协程cpu耗时略高,但是在GC上有巨大的优势,在频繁调用的场景下性能稳定性更佳,这一点额外的耗时是值得的。

注意事项:

尽量避免()=>{};闭包,如下捕获了temp变量产生了78.1KB的GC:

    void Test_DelayDoSomethingUnitask()
    {
        for (int i = 0; i < 500; i++)
        {
            int temp = i;
            Ib_Async.DelayDoSomethingUniTask(3, 
                () => {
                    increaseGoldAction?.Invoke(temp);
                }
                ,CancellationToken.None);
        }
    }

解决方法:使用Ib_Async的传入参数替代。78.1KB => 0B

    void Test_DelayDoSomethingUnitask()
    {
        for (int i = 0; i < 500; i++)
        {
            int temp = i;
            Ib_Async.DelayDoSomethingUniTask(3,increaseGoldAction,temp,CancellationToken.None);
        }
    }

无参数延迟方法的主要实现:

通过UniTask来异步等待。

public static class Ib_Async
{
    /// <summary>
    /// 无参数的延迟方法,若action带参数,请使用多参数的延迟方法,避免使用闭包()=>{};   
    /// </summary>
    /// <param name="delay">延迟时间</param>
    /// <param name="action">延迟后执行的方法</param>
    /// <param name="cts">Unitask取消令牌</param>
    /// <param name="cancelCallBack">取消后的回调</param>
    public static void DelayDoSomethingUniTask(float delay, Action action, CancellationToken cts, Action cancelCallBack = null)
    {
        if (action != null) DealDelayDoSomethingUniTaskCore(delay, action, cts, cancelCallBack).Forget();
        else Debug.Log("[Action] to delay is Null");
    }
    
    private static async UniTaskVoid DealDelayDoSomethingUniTaskCore(float delay, Action action, CancellationToken cts, Action cancelCallBack)
    {
        try
        {
            if (delay > 0)
            {
                await UniTask.Delay(TimeSpan.FromSeconds(delay), cancellationToken: cts);
            }
            else
            {
                await UniTask.Yield(PlayerLoopTiming.Update, cts);
            }

            action?.Invoke();
        }
        catch (OperationCanceledException)//UniTask被取消时的回调
        {
            cancelCallBack?.Invoke();
            Debug.Log("DelayDoSomethingActionUniTask was canceled.");
        }
    }
}

Ib_Async源码:支持Action至多五个传入参数的重载,避免()=>{}闭包产生的GC。如有需求可自行拓展更多的传入参数。

namespace Ib_Core
{
    using System.Threading;
    using Cysharp.Threading.Tasks;
    using System;

    #region 值类型Action封装

    internal interface IActionInvoker
    {
        void Invoke();
    }

    internal readonly struct ActionInvoker : IActionInvoker
    {
        private readonly Action _action;

        public ActionInvoker(Action action)
        {
            _action = action;
        }

        public void Invoke() => _action?.Invoke();
    }

    internal readonly struct ActionInvoker<T1> : IActionInvoker
    {
        private readonly Action<T1> _action;
        private readonly T1 _arg1;

        public ActionInvoker(Action<T1> action, T1 arg1)
        {
            _action = action;
            _arg1 = arg1;
        }

        public void Invoke() => _action?.Invoke(_arg1);
    }

    internal readonly struct ActionInvoker<T1, T2> : IActionInvoker
    {
        private readonly Action<T1, T2> _action;
        private readonly T1 _arg1;
        private readonly T2 _arg2;

        public ActionInvoker(Action<T1, T2> action, T1 arg1, T2 arg2)
        {
            _action = action;
            _arg1 = arg1;
            _arg2 = arg2;
        }

        public void Invoke() => _action?.Invoke(_arg1, _arg2);
    }

    internal readonly struct ActionInvoker<T1, T2, T3> : IActionInvoker
    {
        private readonly Action<T1, T2, T3> _action;
        private readonly T1 _arg1;
        private readonly T2 _arg2;
        private readonly T3 _arg3;

        public ActionInvoker(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3)
        {
            _action = action;
            _arg1 = arg1;
            _arg2 = arg2;
            _arg3 = arg3;
        }

        public void Invoke() => _action?.Invoke(_arg1, _arg2, _arg3);
    }

    internal readonly struct ActionInvoker<T1, T2, T3, T4> : IActionInvoker
    {
        private readonly Action<T1, T2, T3, T4> _action;
        private readonly T1 _arg1;
        private readonly T2 _arg2;
        private readonly T3 _arg3;
        private readonly T4 _arg4;

        public ActionInvoker(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
        {
            _action = action;
            _arg1 = arg1;
            _arg2 = arg2;
            _arg3 = arg3;
            _arg4 = arg4;
        }

        public void Invoke() => _action?.Invoke(_arg1, _arg2, _arg3, _arg4);
    }

    internal readonly struct ActionInvoker<T1, T2, T3, T4, T5> : IActionInvoker
    {
        private readonly Action<T1, T2, T3, T4, T5> _action;
        private readonly T1 _arg1;
        private readonly T2 _arg2;
        private readonly T3 _arg3;
        private readonly T4 _arg4;
        private readonly T5 _arg5;

        public ActionInvoker(Action<T1, T2, T3, T4, T5> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
        {
            _action = action;
            _arg1 = arg1;
            _arg2 = arg2;
            _arg3 = arg3;
            _arg4 = arg4;
            _arg5 = arg5;
        }

        public void Invoke() => _action?.Invoke(_arg1, _arg2, _arg3, _arg4, _arg5);
    }

    #endregion

    /// <summary>
    /// 负责异步调用,支持延迟行为
    /// </summary>
    public static class Ib_Async
    {
        #region 延迟方法与重载

        /// <summary>
        /// 无参数的延迟方法,若action带参数,请使用多参数的延迟方法,避免使用闭包()=>{};   
        /// </summary>
        /// <param name="delay">延迟时间</param>
        /// <param name="action">延迟后执行的方法</param>
        /// <param name="cts">Unitask取消令牌</param>
        /// <param name="cancelCallBack">取消后的回调</param>
        public static void DelayDoSomethingUniTask(float delay, Action action, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker(action), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        /// <summary>
        /// 无参数的延迟方法,延迟1帧,若action带参数,请使用多参数的延迟方法,避免使用闭包()=>{};
        /// </summary>
        /// <param name="action">延迟后执行的方法</param>
        /// <param name="cts">Unitask取消令牌</param>
        /// <param name="cancelCallBack">取消后的回调</param>
        public static void DelayDoSomethingUniTask(Action action, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(0, new ActionInvoker(action), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        public static void DelayDoSomethingUniTask<T1>(float delay, Action<T1> action, T1 arg1, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker<T1>(action, arg1), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        public static void DelayDoSomethingUniTask<T1, T2>(float delay, Action<T1, T2> action, T1 arg1, T2 arg2, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker<T1, T2>(action, arg1, arg2), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        public static void DelayDoSomethingUniTask<T1, T2, T3>(float delay, Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker<T1, T2, T3>(action, arg1, arg2, arg3), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        public static void DelayDoSomethingUniTask<T1, T2, T3, T4>(float delay, Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker<T1, T2, T3, T4>(action, arg1, arg2, arg3, arg4), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        public static void DelayDoSomethingUniTask<T1, T2, T3, T4, T5>(float delay, Action<T1, T2, T3, T4, T5> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, CancellationToken cts, Action cancelCallBack = null)
        {
            if (action != null) DealDelayDoSomethingUniTaskCore(delay, new ActionInvoker<T1, T2, T3, T4, T5>(action, arg1, arg2, arg3, arg4, arg5), cts, cancelCallBack).Forget();
            else Ib_Log.Warning("[Action] to delay is Null");
        }

        #endregion

        #region 延迟核心方法

        private static async UniTaskVoid DealDelayDoSomethingUniTaskCore<TInvoker>(float delay, TInvoker invoker, CancellationToken cts, Action cancelCallBack)
            where TInvoker : struct, IActionInvoker
        {
            try
            {
                if (delay > 0)
                {
                    await UniTask.Delay(TimeSpan.FromSeconds(delay), cancellationToken: cts);
                }
                else
                {
                    await UniTask.Yield(PlayerLoopTiming.Update, cts);
                }

                invoker.Invoke();
            }
            catch (OperationCanceledException)
            {
                cancelCallBack?.Invoke();
                Ib_Log.Warning("DelayDoSomethingActionUniTask was canceled.");
            }
        }

        #endregion
    }
}



评论(2)

查看评论列表
评论头像
[…] 可以通过取消令牌随时结束循环任务,如下使用Ib_Async.DelayDoSomethingUniTask延迟调用方法在3秒后结束回血任务,自由控制任务的生命周期。还可以在创建循环任务时添加任务开始与结束的调用。 […]
评论头像
[…] 使用Ib_Async可自行添加延迟一帧调用的拓展: […]

发表评论

表情 颜文字

插入代码