在上一篇中我们分析了Win32和WinForm编写GUI应用程序会面对的主要问题。总结下来最重要的就是:如何高效的从Worker线程中更新界面。所以首先让我们看看WPF中是如何达到这个目的的。
DispatcherObject的使用
DispatcherObject类有两个成员方法,CheckAccess和VerifyAccess。CheckAccess功能和WinForm中Control.InvokeRequired属性相同,当调用线程与对象的创建线程不是同一个实例时,返回False。VerifyAccess则是抛出异常。
DispatcherObject还有一个Dispatcher的属性,返回控件的创建Dispatcher。通过其Invoke方法,我们可以将界面更新的操作Marshal到控件的创建线程。如下例:
2 if (!textbox.CheckAccess)
3 {
4 textbox.Dispatcher.Invoke(DispatcherPriority.Send, delegate { });
5 }
CheckAccess()的实现方法类似于上篇的最后一个例子,通过比较两个线程实例,因而速度很快,只需要几个IL指令。继承DispatcherObject的类,在内部都调用VerifyAccess来做判断,因而当非创建线程调用时,就会抛出异常。这样的好处是我们可以较早的发现代码的问题,不像WinForm那样不确定的发生。
好了,下面我们来看看真正做事情的Dispatcher类,在这之前让我们再回顾一个Win32的消息循环。
Win32 消息循环
在Win32中,消息Pump的建立是通过循环的调用GetMessage,当收到WM_Quit类型的消息是,退出。而我们对窗口或控件属性的修改,如颜色,位置都实际都是调用PostMessage或SendMessage。当调用线程与窗口的Owner线程是同一个,则SendMessage直接调用窗口函数;如不同则将消息放置在窗口对应的消息队列中,由GetMessage取出并进行处理。WinForm实际是建立在Win32的基础上,所以当我们写lable.Text = "Hello World"时,实际是调用了PostMessage等方法。
下面我们看看Win32消息队列的特点和限制:
- 任劳任怨的从消息队列中拿出消息,并调用对应的窗口处理函数
- 当消息Post进队列后就无法控制了
- 消息的处理支持有限的优先级,但都由OS控制,用户代码不能控制和改变优先级
- Application.DoEvents只有在处理完消息队列中所有消息后才返回,当然Win32没有这个限制,因为Message Pump就是用户创建的
- 可以添加钩子,偷偷的干些坏事
WPF中的Dispatcher的主要功能类似于Win32中消息队列,当并不是替换消息循环,而是建立在消息循环之上。首先,让我们看看Dispatcher的结构:
线程相关的Dispatcher的创建
当我们在一个线程上第一次创建WPF对象时,DispatcherObject构造函数会调用Dispatcher.CurrentDispatcher赋值给私有变量_dispatcher。Dispatcher.CurrentDispatcher通过Thread.CurrentThread来找到该线程对应的Dispatcher实例,如不存在则创建一个。Dispatcher的构造函数,首先创建了一个具有11个级优先级(DispatcherPriority)的优先队列PriorityQueue,这个优先队列用来保存不同优先级的操作。然后调用Win32 API RegisterClassEx注册窗口处理函数,并调用CreateWindowEx创建一个不可见的窗口。
Dispatcher有两个静态方法用来建立消息循环,定义如下:
2 {
3 public static void Run()
4 {
5 PushFrame(new DispatcherFrame());
6 }
7 public static void PushFrame(DispatcherFrame frame) {…}
8 }
尽管是静态方法,实际上是调用当前线程对应的Dispatcher实例的方法。Application.Run()内部实际就是调用了Dispatcher.Run(),进而调用Dispatcher.PushFrame(…)。当我们创建一个WPF项目时,VS自动生成的App.xaml,编译后的代码为
2 {
3 WpfHello.App app = new WpfHello.App();
4 app.InitializeComponent();
5 app.Run();
6 }
当调用PushFrame()或Run()后,内部开始循环调用Win32 GetMessage方法,而建立消息循环。如下面的例子中,在VS缺省的WPF项目中,创建了一个新的线程,在该线程中创建一个新的WPF窗口,并启动消息循环:
2 {
3 protected override void OnStartup(StartupEventArgs e)
4 {
5 // Create a new thread
6 Thread monitorThread = new Thread(QueueMonitor.ThreadMain);
7 monitorThread.SetApartmentState(ApartmentState.STA);
8 monitorThread.Start(this);
9
10 base.OnStartup(e);
11 }
12 }
13
14 class QueueMonitor
15 {
16 public static void ThreadMain(Object obj)
17 {
18 QueueMonitor monitor = new QueueMonitor(obj as Application);
19 }
20
21 private Application m_target;
22 private DispatcherFrame m_frame;
23 private MonitorWindow m_window;
24 public QueueMonitor(Application target)
25 {
26 m_target = target;
27
28 m_frame = new DispatcherFrame();
29 m_window = new MonitorWindow();
30
31 m_window.Closed += MonitorWindow_Closed;
32 m_window.Visibility = Visibility.Visible;
33
34 Dispatcher.Run();
35 // Dispatcher.PushFrame(m_frame);
36 }
37
38 void MonitorWindow_Closed(object sender, EventArgs e)
39 {
40 Dispatcher.ExitAllFrames();
41 // m_frame.Continue = false;
42 }
43 }
消息循环的控制
上个例子中,我们也可以使用PushFrame方法。相比于Run(),我们可以通过PushFrame的参数DispatcherFrame对象来控制什么时候退出消息循环。如需退出当前层的循环,只需将对应的DispatcherFrame.Continue赋值false。我们可以嵌套的调用PushFrame,这点上类似Application.DoEvents,更具灵活性,但我们仍不能只要求处理特定优先级的消息。
另外可以调用Dispatcher.ExitAllFrames方法退出当前线程的所有消息循环。Dispatcher还提供一个方法DisableProcessing,该方法可以暂停消息循环处理消息。下面是这个方法的使用方法:
2 {
3 Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Render, delegate { });
4 }
该方法返回的DispatcherProcessingDisabled对象实现了IDisposable接口,在Dispose中恢复消息循环。该方法也可以嵌套的调用。
下面我们来看看如何投递消息,及Dispatcher如何处理的。
Dispatcher优先队列
Dispatcher的Invoke和BeginInvoke方法,用来向Dispatcher的优先队列放置任务,前者是同步方法,后者是异步。这两个方法,第二个参数是个delegate对象,代表要执行的任务。第一个参数定义任务的优先级,具体定义可以看DispatcherPriority枚举类型。总的来说,我们可以划分为两类,Background和Foreground,另一个比较特殊的是DispatcherPriority.Send。
当在Invoke中指定Send时,且CheckAccess为真,则Invoke直接调用delegate执行任务。如果不是,则调用BeginInvoke,并等待结果,或超时。
BeginInvoke执行时,首先将该任务封装成DispatcherOperation对象,并放置在对应的优先队列中。然后判断是Background还是Foreground,如果是Foreground,则调用PostMessage往Win32消息队列中投递一条消息,然后返回。如果是Background,则检查是否有Timer,如没有则创建一个Timer,Timer会不断循环的投递消息到Win32消息队列,来触发消息的处理。当有Foreground消息是,则删掉Timer。通过这种方式,系统在空闲的时候可以处理Background消息。
当有Win32消息投递到Win32队列时,注册的窗口函数被执行,从优先队列中取出一个DispatcherOperation来执行。完成后,则投递新的Win32消息来触发下次执行,或等待Timer消息。
BeginInvoke的返回值则为DispatcherOperation对象,通过她我们可以取消,等待,或者调整该任务的优先级。在后面的系列中,我们在具体看不同Foreground优先级的使用。
优先队列钩子Hook
与Win32类似,我们也可以对消息的处理添加Hook,可以添加下面的Event Handler:
2 Dispatcher.CurrentDispatcher.Hooks.OperationCompleted += ;
3 Dispatcher.CurrentDispatcher.Hooks.OperationPosted += ;
4 Dispatcher.CurrentDispatcher.Hooks.OperationPriorityChanged +=
Event Handler的参数中,我们可以获得对应的Dispatcher和DispatcherOperation对象。通过这种办法,我们可以过滤,查询或改变任务的优先级等操作。
DispatcherTimer
DispatcherTimer类似WinForm中的Timer。我们可以在构造函数中指定优先级,Dispatcher实例等等。
总结
总的来说,与Win32比较,如果给WPF中的线程和消息循环的机制打分的话,我觉得可以打4分的高分。WPF解决了Win32中没有优先级,跨线程调用性能的问题,友好的编程接口。如果说不足的话,如果PushFrame可以支持优先级,对Reentrancy的问题,可能能更好的控制;另外,DispatcherOperation无法获得名字,如我们要开发一个队列监视程序的话很不方便。这些我认为可以扣0.5分,那另外0.5分是什么呢?
最后,如果我们看这些类的话,全部都在Windowsbase.dll中,也就是说System.Windows.Threading中的类,如DispatcherObject,Dispatcher也可以用于其他系统中。我们甚至可以利用这些在我们的系统中。