WPF DispatcherPriority 小析
长久以来DispatcherPriority对我都有种神秘的色彩,因为我甚至不知道如何“真正”的使用它。
一.执行原理
使用Dispatcher.BeginInvoke的时候,其中一个参数便是要指定合适的DispatcherPriority。之前当设置这个参数的时候我往往凭着感觉或是穷举——一种一个失败接着换个继续试的方法。而用BeginInvoke这个函数的目的无非是为了让参数中指定的委托执行的慢些(不在当时马上执行),或是在多线程操作避免当前线程操作了控件而引发错误。
当然马上执行也是可以的,无非是把DispatcherPriority指定为Send。
其实除了Send以外,其他都是通过消息队列的形式——用PostMessage发一个自定义消息,然后在下次消息队列读取这条消息的时候再判断处理。
这种方式有什么好处?
这可以达到异步。有时候异步常常会和同时进行相挂钩,而在这里是把Dispatcher.BeginInvoke中要执行的委托操作存放到某处,先执行Dispatcher.BeginInvoke后面的操作。后面的操作执行,然后呈现出画面会令使用者感觉不会太“卡”。使用者感觉卡的一个重要因素是画面呈现的不及时。执行完呈现之后还要记得把Dispatcher.BeginInvoke中的委托拿出来执行,用什么方式最好呢?可能有人会说用个Timer来不停检查是否Dispatcher.BeginInvoke之后代码已经完结。可问题在于你怎么知道Dispatcher.BeginInvoke之后代码已经完结了呢?
回本溯源Dispatcher.BeginInvoke之前的代码又是谁引发的呢?
当我们把这个代码放在某个按钮的Click事件中时,是否想过Click事件的源泉?对,没错是消息队列。我们的GetMessage,也就是说当我们Dispatcher.BeginInvoke之后的代码结束时,他会回到GetMessage那个While循环处。什么你创建了一个线程,没有做消息循环,在其中创建的按钮没有反应?那我想弱弱的问句,您确定用的是WPF框架?当你用另外一个线程创建UI时,也必须要开启消息循环,因为这个是windows的基础。
既然要回到GetMessage,那么就是要把消息记录到GetMessage下面的消息中即可,所以用了PostMessage来发送消息。
二.执行方式
除了两个无效DispatcherPriority(Inactive,Invalid)和马上执行的Send外,其他的其实可以分成两种,前台和后台。
那什么是前台呢?
前台就是马上发送个PostMessage自定义消息。
那什么是后台呢?
后台就是如果当前执行的系统消息是输入消息,那么不会马上发送PostMessage消息,而是执行一个Timer,其实Timer也是种消息消息,只是速度更快罢了,当发现不再是输入消息之后就和前台线程一样用PostMessage来发送消息。为什么一定要等到输入消息结束后再执行?这是为了更准确的输入,如Ctrl+C这样的快捷键、鼠标连续点击,这样就不会造成BUG。
前台线程有哪些呢?
按照最快到最慢执行的顺序分别是Normal,DataBind,Render,Load。
为什么要分大小?
刚才说了Dispatcher.BeginInvoke会把委托存放到一个地方,这个地方当然是一个可以记录集合的地方,因为一段代码中Dispatcher.BeginInvoke可以执行多次,然后系统收到消息去集合中取一笔数据,那该取哪一笔呢? 简单的用队列或堆栈显然不合适。有了DispatcherPriority才可以令使用者有更多选择和搭配的机会。
既然是为了安排从集合取出的顺序,为什么不用简单的数字,而用枚举,并且取了类似Render这样的名字?
这也是我长久以来困惑最大的地方。我想是不是WPF框架预先的使用了一些数字权限,而在这些数字中做了特定的事,所以用名字来命名这件特定的事,使理解更加容易。
三.执行时机
Normal:这个是最普通的了,也是全限最高的,Invoke实际就是用了这个。
DataBind:目前未知,看名字似乎是和数据绑定有关。
Render:用PostRender给后端的Micore模块(3.5为wpfgfx_v0300.dll)发送图像的数据。
Loaded:主要发送Load和UnLoad和事件和布局事件,其实Load和UnLoad也是布局事件,如Load事件是把控件加入到某个VisualTree之后便引发,UnLoad是移出VisualTree后引发。比如TabControl中间的Content那块就是不停的切换。在布局当中我们还比较关心的是一个事,那就是何时套模板?除了我们手动调用ApplyTemplate外,系统是如何调用?对该函数简单的下个断点,便可以得到答案是MeasureCore。那MeasureCore又是如何引发?答案是当加入VisualTree便会引发控件的InvalidateMeasure,InvalidateMeasure是异步的,对布局不会马上产生影响,简单的用工具看下源码便会知道那个函数的作用是把控件控件放到一个队列里面,不过放的控件并不是当前的控件,而是沿着可视树找到的根元素。这样其实就减少了Measure的次数,因为Measure会对父子控件都有影响。而从根元素传递下来,实际上是节省了开支。而这样想来申请模板的时候在MeasureCore中做也是合情合理的,因为Measure就以为着为布局申请空间,自然要知道控件应用数据后长什么样。
Input:输入事件
Background:其实InvalidateMeasure把根元素加入集合中后,还是需要暂时不执行,那么异步的结构已经有BeginInvoke,自然是使用原有的,他就是使用了Background。可以理解的原因,一般输入都会引起布局的变化,模板申请好马上用的事件,最好在此或之后。
ContextIdle:可以检测垃圾回收,避免内存泄露。使用示例请见我的上篇BLOG——WPF Binding实现自推 中最后倒数第二段。
ApplicationIdle:InvalidateMeasure使用的一种
SystemIdle:以上都执行完之后
当然这些可能有误,留着以后慢慢补充了。