深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)
在上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中我们发现 Dispatcher.Invoke
方法内部是靠 Dispatcher.PushFrame
来确保“不阻塞地等待”的。然而它是怎么做到“不阻塞地等待”的呢?
阅读本文将更深入地了解 Dispatcher 的工作机制。
本文是深入了解 WPF Dispatcher 的工作原理系列文章的一部分:
Dispatcher.PushFrame 是什么?
如果说上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中的 Invoke
算是偏冷门的写法,那 ShowDialog
总该写过吧?有没有好奇过为什么写 ShowDialog
的地方可以等新开的窗口返回之后继续执行呢?
var w = new FooWindow();
w.ShowDialog();
Debug.WriteLine(w.Bar);
看来我们这次有必要再扒开 Dispatcher.PushFrame
的源码看一看了。不过在看之前,我们先看一看 Windows Forms 里面 DoEvents
的实现,这将有助于增加我们对源码的理解。
DoEvents
Windows Forms 里面的 DoEvents
允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。
[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;
}
首先我们需要拿出本文一开始的结论——调用 Dispatcher.PushFrame
可以在不阻塞 UI 线程的情况下等待。
在此基础之上,我们仔细分析此源码的原理,发现是这样的:
- 添加了一个
Background
(4) 优先级的DispatcherOperation
,执行的操作就是调用ExitFrame
方法。(如果不明白这句话,请回过头再看看 Invoke/InvokeAsync 这部分 。) - 调用
Dispatcher.PushFrame
以便在不阻塞 UI 线程的情况下等待。 - 由于用户输入的优先级是
Input
(5),UI 响应的优先级是Loaded
(6),渲染的优先级是Render
(7),每一个都比Background
(4)高,于是只要有任何 UI 上的任务,都会先执行,直到没有任务时才会执行ExiteFrame
方法。(如果不知道为什么,依然请回过头再看看 Invoke/InvokeAsync 这部分 。) - 当
ExitFrame
被执行时,它会设置DispatcherFrame.Continue
为false
。
为了让 DoEvents
实现它的目标,它必须能够在中间插入了 UI 和渲染逻辑之后继续执行后续代码才行。于是,我们可以大胆猜想,设置 DispatcherFrame.Continue
为 false
的目标是让 Dispatcher.PushFrame(frame);
这一句的等待结束,这样才能继续后面代码的执行。
好了,现在我们知道了一个不阻塞等待的开关:
- 调用
Dispatcher.PushFrame(frame);
来不阻塞地等待; - 设置
frame.Continue = false
来结束等待,继续执行代码。
知道了这些,再扒 Dispatcher.PushFrame
代码会显得容易许多。
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
则会导致 _frameDepth
字段增 1。于是,一个个的 DispatcherFrame
就这样一层层嵌套起来。
再看看 while
循环。
while(frame.Continue)
{
if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
break;
TranslateAndDispatchMessage(ref msg);
}
还记得 DoEvents
节里我们说到的开关吗?就是这里的 frame.Continue
。看到这段代码是不是很明确了?如果设置为 false
,则退出循环,于是 PushFrame
方法返回,同时 _frameDepth
字段减 1。在一个个的 frame.Continue
都设置为 false
以至于后,程序将从 Main
函数退出。
如果 frame.Continue
一直保持为 true
呢?那就进入了“死循环”。可是这里我们需要保持清醒,因为“死循环”意味着阻塞,意味着无法在中间插入其它的 UI 代码。所以要么是 GetMessage
让我们能继续处理窗口消息,要么是 TranslateAndDispatchMessage
让我们能继续处理窗口消息。(至于为什么只要能处理消息就够了,我们上一篇说到过,Dispatcher
任务队列的处理就是利用了 Windows 的消息机制。)
然而,这两个方法内部都调用到了非托管代码,很难通过阅读代码了解到它处理消息的原理。但是通过 .Net Framework 源码调试技术我发现 TranslateAndDispatchMessage
方法似乎并没有被调用到,GetMessage
始终在执行。我们有理由相信用于实现非阻塞等待的关键在 GetMessage
方法内部。.Net Framework 源码调试技术请参阅:调试 ms 源代码 - 林德熙。
于是去 GetMessage
方法内,找到了 UnsafeNativeMethods.ITfMessagePump
类型的变量 messagePump
。这是 Windows 消息循环中的重要概念。看到这里,似乎需要更了解消息循环才能明白实现非阻塞等待的关键。不过我们可以再次通过调试 .Net Framework 的源码来了解消息循环在其中做的重要事情。
调试源码以研究 PushFrame 不阻塞等待的原理
为了开始调试,我为主窗口添加了触摸按下的事件处理函数:
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。
至此,PushFrame
能够做到不阻塞 UI 线程的情况下继续响应消息的原理得以清晰地梳理出来。
如果希望更详细地了解 WPF 中的 Dispatcher 对消息循环的处理,可以参考:详解WPF线程模型和Dispatcher - 踏雪无痕 - CSDN博客。
结论
- 每一次
PushFrame
都会开启一个新的消息循环,记录_frameDepth
加 1; - 在新的消息循环中,会处理各种各样的 Windows 消息,其中有的以事件的形式转发,有的是执行加入到
PriorityQueue<DispatcherOperation>
队列中的任务; - 在显式地退出
PushFrame
时,新开启的消息循环将退出,并继续此前PushFrame
处的代码执行; - 当所有的
PushFrame
都退出后,程序结束。 PushFrame
的while
循环是真的阻塞着主线程,但循环内部会处理消息循环,以至于能够不断地处理新的消息,看起来就像没有阻塞一样。(这与我们平时随便写代码阻塞主线程导致无法处理消息还是有区别的。)
参考资料
- PushFrame/DispatcherFrame
- Windows 消息循环
- 调试 .Net Framework 源码
深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)的更多相关文章
- 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分)
深耕 WPF 开发的各位程序员大大们一定避不开使用 Dispatcher.跨线程访问 UI 当然免不了用到它,将某个任务延迟到当前任务之后执行也会用到它.Dispatcher.Invoke.Dispa ...
- WPF Dispatcher 一次小重构
几个月之前因为项目需要,需要实现一个类似于WPF Dispatcher类的类,来实现一些线程的调度.之前因为一直做Asp.Net,根本没有钻到这个层次去,做的过程中,诸多不顺,重构了四五次,终于实现, ...
- 菜鸟学Struts2——Struts工作原理
在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...
- springMVC 的工作原理和机制
工作原理上面的是springMVC的工作原理图: 1.客户端发出一个http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet的请求映射路径(在web. ...
- springMVC 的工作原理和机制(转)
工作原理上面的是springMVC的工作原理图: 1.客户端发出一个http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet的请求映射路径(在web. ...
- Struts2(二):工作原理
struts可查看源码:https://github.com/apache/struts 在学习struts2之前,我先看了一些比较早版本对struts2的工作原理相关的介绍,顺便抄写过来,用来帮助自 ...
- [JavaEE,MVC] Struts工作原理
基本概念 Struts是Apache 基金会Jakarta 项目组的一个Open Source 项目,它采用MVC模式,能够很好地帮助java 开发者利用J2EE开发Web应用.和其他的java架构一 ...
- .net Remoting 的工作原理是什么?
webservice和.net remoting都是用来通信的框架,它们最大的优点是可以像调用本地对象一样调用远程对象 区别:1.webservice是用的应用层协议http封装的,所以它可以被很多其 ...
- Spring MVC中DispatcherServlet工作原理探究
转:http://blog.csdn.net/zhouyuqwert/article/details/6853730 下面类图将主要的类及方法抽离出来,以便查看方便,根据类的结构来说明整个请求是如何工 ...
随机推荐
- Android -- 网络图片查看器,网络html查看器, 消息机制, 消息队列,线程间通讯
1. 原理图 2. 示例代码 (网络图片查看器) (1) HttpURLConnection (2) SmartImageView (开源框架:https://github.com/loopj/an ...
- Android -- ContentProvider, 读取和保存系统 联系人
1. 示例代码 需要的读写联系人的权限 <uses-permission android:name="android.permission.WRITE_CONTACTS"/& ...
- 推荐给开发者的11个PHP框架(转)
PHP框架对于Web开发者来说是非常有用的工具,它可以帮助使用者更快.更容易的完成项目.根据调查,PHP仍是Web开发中最受欢迎和最实用的平台之一.当谈及Web开发时,很多人依然会选择使用PHP框架, ...
- PHP函数前面添加@的作用
@是PHP提供的错误信息屏蔽的专用符号. 比如在一个函数前使用@ @mysql_query 不会出现Warning, 而原来mysql_query 在遇到错误时会在页面上访提示Warning. @是可 ...
- Python编程
1.pip的使用.安装 pip show 显示输出版本 pip -V 是否安装成功 pip --help 查看相关帮助
- 设计模式--抽象工厂模式C++实现
抽象工厂模式C++实现 1定义 为创建一组相关或者依赖的对象提供一个接口,且无需指定他们的具体类 2类图 3实现 class AbstractProduct { protected: Abstract ...
- python 爬虫005-爬虫实例
实例一:扒取猫眼电影TOP100 的信息 #!/usr/bin/env python # -*- coding: utf-8 -*- """ 扒取猫眼电影TOP100 的 ...
- 1-15-2-RAID5 企业级RAID磁盘阵列的搭建(RAID1、RAID5、RAID10)
RAID5的搭建 第一步:添加四个磁盘,开机并检查(略过) 第二步:使用fdisk命令分别对四个磁盘进行分区,效果如下图: 第三步:使用mdadm命令创建RAID5磁盘阵列 [root@localho ...
- 同一主机,不同域名绑定不同网站(IIS主机头实现方法)
说明: 网站A域名:www.weba.com 网站A路径:D:\website\weba 网站B域名:www.webb.com 网站B路径:D:\website\webb 公用主机IP:111.222 ...
- C# ASP.NET 验证码
使用C# ASP.NET 获取 验证码的代码书写 一般都采用异步 点击 前台验证码图片 请求一次 : 前台图片代码: <img id="imgvalidatecode" sr ...