在上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中我们发现 Dispatcher.Invoke 方法内部是靠 Dispatcher.PushFrame 来确保“不阻塞地等待”的。然而它是怎么做到“不阻塞地等待”的呢?

阅读本文将更深入地了解 Dispatcher 的工作机制。


本文是深入了解 WPF Dispatcher 的工作原理系列文章的一部分:

  1. Invoke/InvokeAsync 部分
  2. PushFrame 部分(本文)

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 线程的情况下等待。

在此基础之上,我们仔细分析此源码的原理,发现是这样的:

  1. 添加了一个 Background(4) 优先级的 DispatcherOperation,执行的操作就是调用 ExitFrame 方法。(如果不明白这句话,请回过头再看看 Invoke/InvokeAsync 这部分 。)
  2. 调用 Dispatcher.PushFrame 以便在不阻塞 UI 线程的情况下等待。
  3. 由于用户输入的优先级是 Input(5),UI 响应的优先级是 Loaded(6),渲染的优先级是 Render(7),每一个都比 Background(4)高,于是只要有任何 UI 上的任务,都会先执行,直到没有任务时才会执行 ExiteFrame 方法。(如果不知道为什么,依然请回过头再看看 Invoke/InvokeAsync 这部分 。)
  4. ExitFrame 被执行时,它会设置 DispatcherFrame.Continuefalse

为了让 DoEvents 实现它的目标,它必须能够在中间插入了 UI 和渲染逻辑之后继续执行后续代码才行。于是,我们可以大胆猜想,设置 DispatcherFrame.Continuefalse 的目标是让 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;}}
}

这里有两个点值得我们研究:

  1. _frameDepth 字段。
  2. 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.InvokeShowDialog 都是为了执行 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博客。

结论

  1. 每一次 PushFrame 都会开启一个新的消息循环,记录 _frameDepth 加 1;
  2. 在新的消息循环中,会处理各种各样的 Windows 消息,其中有的以事件的形式转发,有的是执行加入到 PriorityQueue<DispatcherOperation> 队列中的任务;
  3. 在显式地退出 PushFrame 时,新开启的消息循环将退出,并继续此前 PushFrame 处的代码执行;
  4. 当所有的 PushFrame 都退出后,程序结束。
  5. PushFramewhile 循环是真的阻塞着主线程,但循环内部会处理消息循环,以至于能够不断地处理新的消息,看起来就像没有阻塞一样。(这与我们平时随便写代码阻塞主线程导致无法处理消息还是有区别的。)

参考资料

  • PushFrame/DispatcherFrame

    • Dispatcher.cs
    • c# - WPF DispatcherFrame magic - how and why this works? - Stack Overflow
    • c# - For what is PushFrame needed? - Stack Overflow
    • multithreading - WPF - Dispatcher PushFrame() - Stack Overflow
    • DispatcherFrame Class (System.Windows.Threading)
    • DispatcherFrame. Look in-Depth - CodeProject
  • Windows 消息循环
    • Message loop in Microsoft Windows - Wikipedia
    • c# - Understanding the Dispatcher Queue - Stack Overflow
    • 详解WPF线程模型和Dispatcher - 踏雪无痕 - CSDN博客
  • 调试 .Net Framework 源码
    • 调试 ms 源代码 - 林德熙

深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)相关推荐

  1. Struts2的工作原理

    Struts2是在Struts1的基础上发展而来的,Struts是WebWork和Struts1的集合,采用的正是WebWork的核心,更多的是WebWork. 下载的Struts2源代码文件 主要的 ...

  2. struts2中struts.xml和web.xml文件解析及工作原理

    转自:https://www.cnblogs.com/printN/p/6434526.html web.xml <?xml version="1.0" encoding=& ...

  3. --------------springMVC的开篇,以及底层执行流程,配置视图解析器,静态资源的访问,流程图,工作原理...

    springMVC: 一:创建第一个项目 01.引入需要的jar包 web webmvc context context-support 02.在web.xml文件中 配置我们需要的核心控制器 Dis ...

  4. springMVC 的工作原理和机制

    转载自 https://www.cnblogs.com/zbf1214/p/5265117.html 工作原理 上面的是springMVC的工作原理图: 1.客户端发出一个http请求给web服务器, ...

  5. Struts2的工作原理及工作流程

    众所周知,Struts2是个非常优秀的开源框架,我们能用Struts2框架进行开发,同时能 快速搭建好一个Struts2框架,但我们是否能把Struts2框架的工作原理用语言表达清楚,你表达的原理不需 ...

  6. Java三大器之过滤器(Filter)的工作原理和代码演示

    一.Filter简介 Filter也称之为过滤器,它是Servlet技术中最激动人心的技术之一,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp, Servlet, ...

  7. Spring MVC的工作原理和机制

    Spring  MVC的工作原理和机制 参考: springMVC 的工作原理和机制 - 孤鸿子 - 博客园 https://www.cnblogs.com/zbf1214/p/5265117.htm ...

  8. Appium学习日记(一)——Appium工作原理及其主要组件

    Appium工作原理及其主要组件 Appium的工作原理(how Appium works)   Appium的核心是一个服务器,它侦听符合API规范WebDriver的传入HTTP请求.对于那些过去 ...

  9. spring web.xml中 过滤器(Filter)的工作原理和代码演示

    一.Filter简介 Filter也称之为过滤器,它是Servlet技术中最激动人心的技术之一,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp, Servlet, ...

最新文章

  1. 安装Phoenix时./sqlline.py执行报错File ./sqlline.py, line 27, in module import argparse ImportError: No ...
  2. DB设计原则(一)字段名定义避免二义性。
  3. 于企业而言,Linux 与 Windows 哪个更安全?
  4. QT. 学习之路 一
  5. 面绘制经典算法:MarchingCube实现(C++ OpenGl代码篇)
  6. Android的服务(Service)(三)Service客户端的绑定与跨进程
  7. Leetcode题库 145.二叉树的后序遍历(递归 C实现)
  8. 【物理女神】谁是中国第一位物理学女博士?
  9. 用 Mars Remote API 轻松分布式执行 Python 函数
  10. 【转】构建C1000K的服务器(1) – 基础
  11. mysql 关闭autocommit_mysql禁用autocommit,以及遇到的问题
  12. lightoj 1016
  13. Python+OpenCV:Hough直线检测(Hough Line Transform)
  14. Linux下查看网卡实时流量工具
  15. 20155238 2016-2017-2 《Java程序设计》第六周学习总结
  16. 图片怎么转换成文字?几个好用的方法快来查阅
  17. MongoDBCompass使用教程
  18. 游戏策划:为什么我的儿子不沉迷游戏
  19. 多媒体视频开发_(3) ffmpeg获取视频的总帧数
  20. 基于递归回溯算法实现八皇后游戏问题

热门文章

  1. 漫画风格迁移神器 AnimeGANv2:快速生成你的漫画形象
  2. 关于陌陌直播 可能和你想的不太一样
  3. AD16出现Your license is already used on computer “DESKTOP-6B2RPUI“ using product “ Altium Designer“.
  4. css如何设置高亮显示,Javascript实现CSS代码高亮显示
  5. RAN-in-the-Cloud:为 5G RAN 提供云经济性
  6. Java: Date转sql date
  7. 左移寄存器vhdl_双向移位寄存器VHDL设计
  8. Vue从零开始之Vue组件
  9. 滑动门技术实现的导航菜单
  10. 微信小程序获取扫描二维码后携带的参数