们已经知道,win32/MFC/WinForm/WPF 都依靠消息循环驱动,让程序跑起来。
这里就介绍 WPF 中是如何使用消息循环来驱动程序的。
1. 背景
只听说过 Dispatcher ,哪里来的消息循环?
WPF 启动运行堆栈:
csharp
> WpfApp1.exe!WpfApp1.App.OnStartup(System.Windows.StartupEventArgs e) 行 17 C#
PresentationFramework.dll!System.Windows.Application..ctor.AnonymousMethod__1_0(object unused)
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs)
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler)
WindowsBase.dll!System.Windows.Threading.DispatcherOperation.InvokeImpl()
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
WindowsBase.dll!MS.Internal.CulturePreservingExecutionContext.Run(MS.Internal.CulturePreservingExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
WindowsBase.dll!System.Windows.Threading.DispatcherOperation.Invoke()
WindowsBase.dll!System.Windows.Threading.Dispatcher.ProcessQueue()
WindowsBase.dll!System.Windows.Threading.Dispatcher.WndProcHook(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled)
WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled)
WindowsBase.dll!MS.Win32.HwndSubclass.DispatcherCallbackOperation(object o)
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs)
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler)
WindowsBase.dll!System.Windows.Threading.Dispatcher.LegacyInvokeImpl(System.Windows.Threading.DispatcherPriority priority, System.TimeSpan timeout, System.Delegate method, object args, int numArgs)
WindowsBase.dll!MS.Win32.HwndSubclass.SubclassWndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam)
[本机到托管的转换]
[托管到本机的转换]
WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame)
PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore)
PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window)
WpfApp1.exe!WpfApp1.App.Main()
可以发现 倒数第 4 行的PushFrameImpl
这个方法。
去看其源码,就发现了熟悉的消息循环 :
csharp
private void PushFrameImpl(DispatcherFrame frame)
{
SynchronizationContext synchronizationContext = null;
SynchronizationContext synchronizationContext2 = null;
MSG msg = default(MSG);
_frameDepth++;
try
{
synchronizationContext = SynchronizationContext.Current;
synchronizationContext2 = new DispatcherSynchronizationContext(this);
SynchronizationContext.SetSynchronizationContext(synchronizationContext2);
try
{
while (frame.Continue && GetMessage(ref msg, IntPtr.Zero, 0, 0))
{
TranslateAndDispatchMessage(ref msg);
}
if (_frameDepth == 1 && _hasShutdownStarted)
{
ShutdownImpl();
}
}
finally
{
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
}
finally
{
_frameDepth--;
if (_frameDepth == 0)
{
_exitAllFrames = false;
}
}
}
主要的 GetMessage 和 TranslateAndDispatchMessage 代码:
csharp
private bool GetMessage(ref MSG msg, IntPtr hwnd, int minMessage, int maxMessage)
{
UnsafeNativeMethods.ITfMessagePump messagePump = GetMessagePump();
try
{
if (messagePump == null)
{
return UnsafeNativeMethods.GetMessageW(ref msg, new HandleRef(this, hwnd), minMessage, maxMessage);
}
messagePump.GetMessageW(ref msg, hwnd, minMessage, maxMessage, out var result);
return result switch
{
-1 => throw new Win32Exception(),
0 => false,
_ => true,
};
}
finally
{
if (messagePump != null)
{
Marshal.ReleaseComObject(messagePump);
}
}
}
csharp
private void TranslateAndDispatchMessage(ref MSG msg)
{
bool flag = false;
if (!ComponentDispatcher.RaiseThreadMessage(ref msg))
{
UnsafeNativeMethods.TranslateMessage(ref msg);
UnsafeNativeMethods.DispatchMessage(ref msg);
}
}
可以理解为:Dispatcher 对消息循环的操作进行了"封装" 。
那,Dispatcher 是谁?
2. Dispatchcer
大部分WPF对象,都是 DispatcherObject。这意味着,可以在 DispatcherObject 中(如 Window 中), 使用 this.Dispatchcer 获取到 Dispatchcer 。
一般我们会通过三种方式获取 Dispatchcer :
csharp
// App.Current.Dispatcher;(Application.Current.Dispatcher)
var dispatcher1 = App.Current.Dispatcher;
// CurrentDispatcher;
var dispatcher2 = System.Windows.Threading.Dispatcher.CurrentDispatcher;
// System.Windows.Threading.DispatcherObject.Dispatcher;
var dispatcher3 = this.Dispatcher;
可分为两类:
-
当前线程的 Dispatcher:
System.Windows.Threading.Dispatcher.CurrentDispatcher;
-
创建对应对象的 Dispatcher:
App.Current.Dispatcher;
DispatcherObject.Dispatcher;
Dispatcher 和线程是什么关系?
- Dispatcher 属于线程(与线程一一对应)。
在单 UI 线程应用程序中
,WPF的 UI 对象在获取 this.Dispatcher 属性时,不同对象取的都是同一个Dispatcher实例。(因为都是同一个 UI 线程创建的。)- 在默认的 WPF UI 线程中: App.Current.Dispatcher = DispatcherObject.Dispatcher
所有的线程(UI线程,普通线程)都有 Dispatcher 吗?
是的。
在所有线程中,调用 System.Windows.Threading.Dispatcher.CurrentDispatcher
都会得到一个属于这个线程的 Dispatcher 对象。(不用的时候不会创建)
所以:如果你想在一个后台线程中,使用 Dispatcher.CurrentDispatcher.Invoke
将操作封送到 UI 线程,是做不到的。因为这时候获取到的 Dispatcher 不是UI线程的 Dispatcher, 而是当前线程自己的 Dispatcher。
你最常用 Dispatcher 做什么?
跨线程访问UI
为什么不能直接跨线程访问UI?
3. Dispatcher 如何实现跨线程的调用
最常使用 Dispatcher 的创建就是,在后台线程更新 UI ,那 Dispatcher 是如何做到的呢。
当你调用
csharp
private void Foo()
{
this.Width = 100;
}
Application.Current.Dispatcher.Invoke(() =>
{
Foo();
});
时,Dispatcher 究竟做了什么,把操作转移到 UI 线程上去了。
- 将调用的
Delegate
和优先级包装成一个DispatcherOperation
放入Dispatcher维护的优先级队列当中,这个Queue是按DispatcherPriority
排序的,总是高优先级的 DispatcherOperation 先被处理。 - 往当前线程的消息队列当中Post一个名为
MsgProcessQueue
的 Message。(这个消息是WPF自己定义的。)这个消息被Post到消息队列之前,还要设置MSG.Handle
,这个Handle就是Window 1#的Handle。指定Handle是为了在消息循环Dispatch消息的时候,指定哪个窗口的 WndProc 处理这个消息。 - 消息循环读取消息。
- 系统根据获取消息的Handle,发现跟Window1#的Handle相同,那么这个消息派发到Window1#的窗口过程,让其处理。
- 在窗口过程中,优先级队列当中取一个DispatcherOperation。
- 执行 DispatcherOperation.Invoke 方法,Invoke方法的核心就是调用 DispatcherOperation 构造时传入的Delegate,也就是 Dispatcher.BeginInvoke 传入的Delegate。最终这个Foo()方法就被执行了。
4 回顾
- WPF 底层仍然靠信息循环来驱动。
- Dispatcher 使用消息循环来实现跨进程的委托调用。
- Dispatcher 属于线程,需要理解当前拿到的 Dispatcher 到底是哪个 Dispatcher 。