在上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中我们发现
方法内部是靠
- Dispatcher.Invoke
来确保“不阻塞地等待”的。然而它是怎么做到“不阻塞地等待”的呢?
- Dispatcher.PushFrame
阅读本文将更深入地了解 Dispatcher 的工作机制。
本文是深入了解 WPF Dispatcher 的工作原理系列文章的一部分:
如果说上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中的
算是偏冷门的写法,那
- Invoke
总该写过吧?有没有好奇过为什么写
- ShowDialog
的地方可以等新开的窗口返回之后继续执行呢?
- ShowDialog
- var w = new FooWindow();
- w.ShowDialog();
- Debug.WriteLine(w.Bar);
看来我们这次有必要再扒开
的源码看一看了。不过在看之前,我们先看一看 Windows Forms 里面
- Dispatcher.PushFrame
的实现,这将有助于增加我们对源码的理解。
- DoEvents
Windows Forms 里面的
允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。
- DoEvents
- [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
- public void DoEvents()
- {
- DispatcherFrame frame = new DispatcherFrame();
- Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
- new DispatcherOperationCallback(ExitFrame), frame);
- Dispatcher.PushFrame(frame);
- }
- public object ExitFrame(object f)
- {
- ((DispatcherFrame)f).Continue = false;
- return null;
- }
首先我们需要拿出本文一开始的结论——调用
可以在不阻塞 UI 线程的情况下等待。
- Dispatcher.PushFrame
在此基础之上,我们仔细分析此源码的原理,发现是这样的:
(4) 优先级的
- Background
,执行的操作就是调用
- DispatcherOperation
方法。(如果不明白这句话,请回过头再看看Invoke/InvokeAsync 这部分。)
- ExitFrame
以便在不阻塞 UI 线程的情况下等待。
- Dispatcher.PushFrame
(5),UI 响应的优先级是
- Input
(6),渲染的优先级是
- Loaded
(7),每一个都比
- Render
(4)高,于是只要有任何 UI 上的任务,都会先执行,直到没有任务时才会执行
- Background
方法。(如果不知道为什么,依然请回过头再看看Invoke/InvokeAsync 这部分。)
- ExiteFrame
被执行时,它会设置
- ExitFrame
为
- DispatcherFrame.Continue
。
- false
为了让
实现它的目标,它必须能够在中间插入了 UI 和渲染逻辑之后继续执行后续代码才行。于是,我们可以大胆猜想,设置
- DoEvents
为
- DispatcherFrame.Continue
的目标是让
- false
这一句的等待结束,这样才能继续后面代码的执行。
- Dispatcher.PushFrame(frame);
好了,现在我们知道了一个不阻塞等待的开关:
来不阻塞地等待;
- Dispatcher.PushFrame(frame);
来结束等待,继续执行代码。
- frame.Continue = false
知道了这些,再扒
代码会显得容易许多。
- Dispatcher.PushFrame
这真是一项神奇的技术。以至于这一次我需要毫无删减地贴出全部源码:
- [SecurityCritical, SecurityTreatAsSafe ]
- private void PushFrameImpl(DispatcherFrame frame)
- {
- SynchronizationContext oldSyncContext = null;
- SynchronizationContext newSyncContext = null;
- MSG msg = new MSG();
- _frameDepth++;
- try
- {
- // Change the CLR SynchronizationContext to be compatable with our Dispatcher.
- oldSyncContext = SynchronizationContext.Current;
- newSyncContext = new DispatcherSynchronizationContext(this);
- SynchronizationContext.SetSynchronizationContext(newSyncContext);
- try
- {
- while(frame.Continue)
- {
- if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
- break;
- TranslateAndDispatchMessage(ref msg);
- }
- // If this was the last frame to exit after a quit, we
- // can now dispose the dispatcher.
- if(_frameDepth == 1)
- {
- if(_hasShutdownStarted)
- {
- ShutdownImpl();
- }
- }
- }
- finally
- {
- // Restore the old SynchronizationContext.
- SynchronizationContext.SetSynchronizationContext(oldSyncContext);
- }
- }
- finally
- {
- _frameDepth--;
- if(_frameDepth == 0)
- {
- // We have exited all frames.
- _exitAllFrames = false;
- }
- }
- }
这里有两个点值得我们研究:
字段。
- _frameDepth
循环部分。
- while
我们先看看
字段。每调用一次
- _frameDepth
就需要传入一个
- PushFrame
,在一次
- DispatcherFrame
期间再调用
- PushFrame
则会导致
- PushFrame
字段增 1。于是,一个个的
- _frameDepth
就这样一层层嵌套起来。
- DispatcherFrame
再看看
循环。
- while
- while(frame.Continue)
- {
- if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
- break;
- TranslateAndDispatchMessage(ref msg);
- }
还记得
节里我们说到的开关吗?就是这里的
- DoEvents
。看到这段代码是不是很明确了?如果设置为
- frame.Continue
,则退出循环,于是
- false
方法返回,同时
- PushFrame
字段减 1。在一个个的
- _frameDepth
都设置为
- frame.Continue
以至于后,程序将从
- false
函数退出。
- Main
如果
一直保持为
- frame.Continue
呢?那就进入了“死循环”。可是这里我们需要保持清醒,因为“死循环”意味着阻塞,意味着无法在中间插入其它的 UI 代码。所以要么是
- true
让我们能继续处理窗口消息,要么是
- GetMessage
让我们能继续处理窗口消息。(至于为什么只要能处理消息就够了,我们上一篇说到过,
- TranslateAndDispatchMessage
任务队列的处理就是利用了 Windows 的消息机制。)
- Dispatcher
然而,这两个方法内部都调用到了非托管代码,很难通过阅读代码了解到它处理消息的原理。但是通过 .Net Framework 源码调试技术我发现
方法似乎并没有被调用到,
- TranslateAndDispatchMessage
始终在执行。我们有理由相信用于实现非阻塞等待的关键在
- GetMessage
方法内部。.Net Framework 源码调试技术请参阅:调试 ms 源代码 - 林德熙。
- GetMessage
于是去
方法内,找到了
- GetMessage
类型的变量
- UnsafeNativeMethods.ITfMessagePump
。这是 Windows 消息循环中的重要概念。看到这里,似乎需要更了解消息循环才能明白实现非阻塞等待的关键。不过我们可以再次通过调试 .Net Framework 的源码来了解消息循环在其中做的重要事情。
- messagePump
为了开始调试,我为主窗口添加了触摸按下的事件处理函数:
- private void OnStylusDown(object sender, StylusDownEventArgs e)
- {
- Dispatcher.Invoke(() =>
- {
- Console.WriteLine();
- new MainWindow().ShowDialog();
- }, DispatcherPriority.Background);
- }
其中
和
- Dispatcher.Invoke
都是为了执行
- ShowDialog
而写的代码。
- PushFrame
只是为了让我打上一个用于观察的断点。
- Console.WriteLine()
运行程序,在每一次触摸主窗口的时候,我们都会命中一次断点。观察 Visual Studio 的调用堆栈子窗口,我们会发现每触摸一次命中断点时调用堆栈中会多一次
,继续执行,由于
- PushFrame
又会多一次
- ShowDialog
。于是,我们每触摸一次,调用堆栈中会多出两个
- PushFrame
。
- PushFrame
每次
之后,都会经历一次托管到本机和本机到托管的转换,随后是消息处理。我们的触摸消息就是从消息处理中调用而来。
- PushFrame
于是可以肯定,每一次
都将开启一个新的消息循环,由非托管代码开启。当
- PushFrame
出来的窗口关掉,或者
- ShowDialog
执行完毕,或者其它会导致
- Invoke
退出循环的代码执行时,就会退出一次
- PushFrame
带来的消息循环。于是,在上一次消息处理中被
- PushFrame
“阻塞”的代码得以继续执行。一层层退出,直到最后
- while
函数退出时,程序结束。
- Main
上图使用的是我在 GitHub 上的一款专门研究 WPF 触摸原理的测试项目:https://github.com/walterlv/ManipulationDemo。
至此,
能够做到不阻塞 UI 线程的情况下继续响应消息的原理得以清晰地梳理出来。
- PushFrame
如果希望更详细地了解 WPF 中的 Dispatcher 对消息循环的处理,可以参考:详解WPF线程模型和Dispatcher - 踏雪无痕 - CSDN博客。
都会开启一个新的消息循环,记录
- PushFrame
加 1;
- _frameDepth
队列中的任务;
- PriorityQueue<DispatcherOperation>
时,新开启的消息循环将退出,并继续此前
- PushFrame
处的代码执行;
- PushFrame
都退出后,程序结束。
- PushFrame
的
- PushFrame
循环是真的阻塞着主线程,但循环内部会处理消息循环,以至于能够不断地处理新的消息,看起来就像没有阻塞一样。(这与我们平时随便写代码阻塞主线程导致无法处理消息还是有区别的。)
- while
来源: http://blog.csdn.net/wpwalter/article/details/78093937