You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI中如何实现与屏幕刷新率同步的全局定时器?为何跟不上?

.NET MAUI 实现与屏幕刷新率同步的全局定时器

问题现状

需要创建一个与屏幕刷新率同步的全局定时器,用于驱动UI动画更新并计算帧间隔deltaTime。当前基于IDispatcherTimer的实现逻辑上看似合理,但实际运行时tick速率远低于屏幕刷新率(刷新率约59,实际tick速率仅29-53且波动大),且CPU占用极低。

用户原实现代码:

GlobalTicker 类

public class GlobalTicker {

    //delta time solver:
    public float deltaTime = 0f;
    DateTime deltaTimeDateTime = DateTime.UtcNow;

    //app run time:
    public float appRunTime = 0f;

    //UPDATE TIMER;
    public IDispatcherTimer timer;

    //SCREEN MONITOR:
    private float refreshRate = 120f;

    public GlobalTicker() {

        //get init screen info:
        refreshRate = DeviceDisplay.Current.MainDisplayInfo.RefreshRate;

        //make timer
        timer = Application.Current.Dispatcher.CreateTimer(); 
        timer.Interval = TimeSpan.FromSeconds(1 / refreshRate); //run every 1/120 seconds or 1/60 seconds or so depending on refresh rate
        timer.IsRepeating = true;
        timer.Tick += (s, e) => tickPassed();
        timer.Start();

    }

    public void tickPassed() {
        MainThread.BeginInvokeOnMainThread(() => {

            deltaTime = (float)(DateTime.UtcNow - deltaTimeDateTime).TotalSeconds;
            appRunTime += deltaTime;
            deltaTimeDateTime = DateTime.UtcNow;

            Debug.WriteLine("TICK RATE " + 1 / deltaTime + " REFRESH RATE " + refreshRate + " TIMER INTERVAL RATE " + 1f / timer.Interval.TotalSeconds);

        });
    }
}

MainPage 中使用

public partial class MainPage : ContentPage {

    public GlobalTicker globalTicker;

    public MainPage() {

        globalTicker = new();
        InitializeComponent();

    }
}

无法同步的原因

  1. IDispatcherTimer 调度特性限制:该定时器依赖UI线程消息循环,优先级低于UI渲染、用户输入等操作,系统不会严格按照设定间隔触发,短间隔场景下误差会被放大。
  2. 冗余线程调度:tickPassed方法本身就在UI线程(Dispatcher线程)执行,额外调用MainThread.BeginInvokeOnMainThread会增加不必要的调度延迟。
  3. 系统定时器精度限制:普通Dispatcher定时器的精度受系统时钟调度粒度影响,无法满足和屏幕刷新率严格同步的需求。

正确实现方案

方案1:利用MAUI动画系统VSync回调(推荐)

MAUI的动画系统本身与屏幕垂直同步(VSync)绑定,通过动画回调可直接获取与刷新率完全同步的tick信号,是最可靠的实现方式:

public class GlobalTicker
{
    public float DeltaTime { get; private set; }
    public float AppRunTime { get; private set; }
    private DateTime _lastTickTime;
    private Animation _syncAnimation;

    // 对外暴露Tick事件,传递帧间隔deltaTime
    public event Action<float> Tick;

    public GlobalTicker()
    {
        _lastTickTime = DateTime.UtcNow;
        StartSyncAnimation();
    }

    private void StartSyncAnimation()
    {
        _syncAnimation = new Animation(_ =>
        {
            var currentTime = DateTime.UtcNow;
            DeltaTime = (float)(currentTime - _lastTickTime).TotalSeconds;
            AppRunTime += DeltaTime;
            _lastTickTime = currentTime;

            // 触发自定义更新逻辑
            Tick?.Invoke(DeltaTime);

            // 循环启动动画,持续触发VSync回调
            StartSyncAnimation();
        }, 0, 1, Easing.Linear);

        // 启动短时长动画,确保触发VSync回调
        _syncAnimation.Commit(Application.Current.MainPage, "GlobalTickerSync", length: 1);
    }

    public void Stop()
    {
        _syncAnimation?.Abort();
    }
}

方案2:高分辨率定时器校准实现

若需基于定时器实现,可使用System.Timers.Timer配合动态校准,减少调度误差:

public class GlobalTicker : IDisposable
{
    public float DeltaTime { get; private set; }
    public float AppRunTime { get; private set; }
    private DateTime _lastTickTime;
    private System.Timers.Timer _highResTimer;
    private float _targetInterval;
    private bool _isProcessingTick; // 防止tick处理重入

    public event Action<float> Tick;

    public GlobalTicker()
    {
        var refreshRate = DeviceDisplay.Current.MainDisplayInfo.RefreshRate;
        _targetInterval = 1f / refreshRate;
        _lastTickTime = DateTime.UtcNow;

        // 初始化高分辨率定时器,间隔转为毫秒
        _highResTimer = new System.Timers.Timer(_targetInterval * 1000);
        _highResTimer.AutoReset = true;
        _highResTimer.Elapsed += OnHighResTimerElapsed;
        _highResTimer.Start();
    }

    private void OnHighResTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        if (_isProcessingTick) return;
        _isProcessingTick = true;

        var currentTime = DateTime.UtcNow;
        DeltaTime = (float)(currentTime - _lastTickTime).TotalSeconds;
        AppRunTime += DeltaTime;
        _lastTickTime = currentTime;

        // 动态校准定时器间隔,修正系统调度误差
        var actualInterval = DeltaTime;
        if (Math.Abs(actualInterval - _targetInterval) > 0.001f)
        {
            _highResTimer.Interval = Math.Max(1, (_targetInterval - (actualInterval - _targetInterval)) * 1000);
        }

        // 在UI线程触发自定义更新逻辑
        MainThread.BeginInvokeOnMainThread(() =>
        {
            Tick?.Invoke(DeltaTime);
            _isProcessingTick = false;
        });
    }

    public void Dispose()
    {
        _highResTimer?.Stop();
        _highResTimer?.Dispose();
    }
}

统一使用方式

在MainPage中订阅Tick事件执行动画更新:

public partial class MainPage : ContentPage
{
    private GlobalTicker _globalTicker;

    public MainPage()
    {
        InitializeComponent();
        _globalTicker = new GlobalTicker();
        _globalTicker.Tick += OnGlobalTickerTick;
    }

    private void OnGlobalTickerTick(float deltaTime)
    {
        Debug.WriteLine($"TICK RATE {1/deltaTime:F1} | REFRESH RATE {DeviceDisplay.Current.MainDisplayInfo.RefreshRate:F1} | DELTA {deltaTime:F4}");
        // 此处执行UI动画更新逻辑
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        _globalTicker.Tick -= OnGlobalTickerTick;
        // 若使用方案2,需释放资源
        // _globalTicker.Dispose();
    }
}

关键优化说明

  • 方案1完全依赖MAUI动画的VSync机制,tick信号与屏幕刷新率严格同步,无额外调度误差。
  • 方案2通过高分辨率定时器+动态校准,尽可能缩小系统调度带来的间隔误差。
  • 移除冗余的线程调度调用,减少延迟。
  • 加入重入保护,避免tick处理叠加导致的延迟累积。

内容的提问来源于stack exchange,提问作者mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 12:45:00