戏说WPF DispatcherTimer

本文主要是对下述几个问题的解答,如果小伙伴已经知道答案,请忽略本文。

  1. DispatcherTimer的作用,与Dispatcher有什么瓜葛?
  2. 为什么不能在构造DispatcherTimer时传入DipatcherPriority.Inactive优先级?

  3. DispatcherTimer是如何实现定时作业的?

问题1、DispatcherTimer的作用,与Dispatcher有什么瓜葛?

Dispatcher(这里我们把Dispatcher简单的看做是一个线程)需要处理各种各样的任务,比如响应键盘输入、鼠标移动、渲染界面等,这些任务有各自的优先级,而DispatcherTimer就是要将一个指定优先级的任务插入到Dispatcher的任务队列中,并让其在指定时间间隔(Interval)之后被处理。需要注意的是,Dispatcher只保证在DispatcherTimer的时间间隔到达之前不会去处理该定时器的任务,至于定时器的任务具体会在什么时候被处理,取决于任务自身的优先级以及Dispatcher当时的任务队列情况。

问题2、为什么不能在构造DispatcherTimer时传入DipatcherPriority.Inactive优先级?

在回答第二个问题之前,我们先来看看DispatcherPriority,

enum DispatcherPriority
{
    Invalid = -1,
    Inactive = 0,
    SystemIdle = 1,
    ApplicationIdle = 2,
    ContextIdle = 3,
    Background = 4,
    Input = 5,
    Loaded = 6,
    Render = 7,
    DataBind = 8,
    Normal = 9,
    Send = 10,
}

 从上往下,数字越大优先级越高。OK,我们言归正传。

用过DispatcherTimer的小伙伴应该知道,在实例化DispatcherTimer时,构造函数中有一个DispatcherPriority参数(如果没有,缺省为DispatcherPriority.Background),不知道小伙伴们有没有试着传入DispatcherPriority.Inactive?实际上这个优先级是不能被接受的?为什么呢?且听我慢慢道来。

图1 DispatcherTimer简化的类图

当我们创建了DispatcherTimer的实例,有两种方式可以启动它:

  1. 设置属性IsEnabled=true;
  2. 调用Start()。

实际上,不管是哪种方式,有效的工作都是由Restart()做的,那么让我们通过源码来看一看这个方法吧。

private void Restart()
{
    lock(_instanceLock)
    {
        if (_operation != null)
        {
            return;
        }

        _operation = _dispatcher.BeginInvoke(
            DispatcherPriority.Inactive,
            new DispatcherOperationCallback(FireTick),
            null);
        
        _dueTimeInTicks = Environment.TickCount + (int) _interval.TotalMilliseconds;
        
        if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess())
        {
            Promote();
        }
        else
        {
            _dispatcher.AddTimer(this);
        }
    }
}

我们看到,Restart()会调用Dispatcher.BeginInvoke(),该方法内部会根据传入的参数产生一个DispatcherOperation实例(就是一个Dispatcher任务),并将其加入到Dispatcher的任务队列中等待处理,最后将这个实例返回给Restart()。仔细看代码的小伙伴会发现,此时BeginInvoke()中传入的第一个参数是DispatcherPriority.Inactive,为什么要用Inactive这个优先级而不用其他的?到这里我不得不请出幕后大佬Dispatcher了,让我们一起来窥一窥大佬的做事风格吧。

大佬Dispatcher的简介

话说大佬Dispatcher天生就和Thread有着密不可分的关系:每个UI线程必须至少持有一个Dispatcher来调度线程任务,而每个Dispatcher也必须在一个指定的线程上工作(Dispatcher的工作线程在Dispatcher被创建时就指定了,即创建该Dispatcher的线程)。另外,Dispatcher做事讲究轻重缓急,所以它将自己的任务按优先级分为3档:

  1. 前台优先级任务(高优先级),范围从DispatcherPriority.Loaded至 DispatcherPriority.Send;
  2. 后台优先级任务,范围从DispatcherPriority.Background至 DispatcherPriority.Input;
  3. 空闲优先级任务,范围从DispatcherPriority.SystemIdle至 DispatcherPriority.ContextIdle

而且,Dispatcher还有个习惯,就是在做事之前先检查一下事情的优先级是否在自己的3档优先级范围内,如果不在,那管你是谁,就不理你!

 

 

 

 

 

 

 

而我们看到的是,DispatcherTimer.Restart()方法传给Dispatcher的任务优先级是Inactive,这实际上是在低声下气的告诉Dispatcher:“老大,我现有个事情要麻烦你,请记得找个时间帮我做一下。”任务是拜托出去了,但是Restart()方法不放心啊,“要是Dispatcher不帮我做可咋整?”,于是又找到Promote()和Dispatcher的手下AddTimer(),让它俩帮忙传达一下。

话说这个Promote()作用可真不小,DispatcherTimer的任务能不能被处理全靠它了。既然如此重要,就让我们擦亮眼睛,一起来看看吧,duang...

internal void Promote()
{
    lock(_instanceLock)
    {
        if(_operation != null)
        {
            _operation.Priority = _priority;
        }
    }
}

 哈哈,是不是有一种上当受骗的感觉?就一行有效代码,能起到什么作用?!可别小瞧了它,Promote()中一行有效代码的作用是把Restart()中产生的任务(DispatcherOperation)的优先级设置为用户期望的优先级,但是在这个设值操作的背后发生了很多事情:在改变任务优先级的同时会唤醒Dispatcher的消息处理链路,消息处理链路会向Windows操作系统(以下简称“Windows”)发送一条消息,Windows收到消息后立刻通知Dispatcher,Dispatcher则会从自己的任务队列中取出优先级最高的任务并处理。另外,为了不漏掉其他需要处理的任务,Dispatcher在从队列中取出任务的同时会再次向Windows发送消息,如此往复,直到Dispatcher处理完所有任务。大致流程如下:

可是,Promote()答应给Restart()帮忙是有条件的(_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()),那如果不符合这个条件呢?Restart()只好另请Dispatcher.AddTimer()这位高明啦。

因为AddTimer()属于Dispatcher范畴,源码这里就不展示了,只简单的说一说该方法的目的。这个方法所做的事情就是把当前的DispatcherTimer实例放到Dispatcher内部维护的一个DispatcherTimer列表里,然后向Windows发送一个设置定时器的消息,Windows收到消息后,会立刻通知Dispatcher。

可即便如此,DispatcherTimer任务(DispatcherOperation)的优先级还是Inactive啊?也就是说这个任务还是不会被Dispatcher提上处理日程!不要着急,对大佬Dispatcher来说,茶要喝,活也得干,它也不想下了班还有一屁股事情没处理。这不,当Dispatcher收到消息后,它发现这是一条与定时器相关的消息,于是让那些时间间隔已经到达的定时器从它的定时器列表中出列,并命令它们把它们的任务提到(DispatcherTimer.Promote())自己的处理日程上,这样一来就没有滞留的定时器任务了。(所以,到这里你应该明白DispatcherPriority.Inactive是DispatcherTimer的一个保留优先级,是任务占位用的。)流程如下,

需要说明的是,Promote()只有在定时器的时间间隔已经到达之后才会被调用。那你有没有想过如果有多个定时器同时到达了时间间隔呢?话说Dispatcher也没辙,只能一个个处理。不妨做个试验,创建两个优先级不同,Interval相同的定时器,让高优先级的定时器做耗时操作,你会发现低优先级定时器的任务会在高优先级的被处理完之后被处理。

问题3、DispatcherTimer是如何实现定时作业的?

其实这个问题的答案在回答问题2的过程中已经给出了:DispatcherTimer的定时作业是依靠Dispatcher实现的,而Dispatcher只保证在DispatcherTimer期望的时间间隔到达之前不去处理它的任务。

结语

本文结合代码简要的分析了DispatcherTimer、Dispatcher的工作方式。抛砖引玉,如果小伙伴们想要深入的了解,还需要自己去阅读源码。这里推荐几个获取源码的方法:

  1. 安装Resharper插件到VS,Resharper的反编译功能很强大;
  2. Reflector工具;
  3. wpf本身已经在github上开源了(https://github.com/dotnet/wpf)。
posted @ 2019-01-19 23:30  叶落劲秋  阅读(4953)  评论(0编辑  收藏  举报