前言
談起“消息機(jī)制”這個(gè)詞,我們都會(huì)想到Windows的消息機(jī)制,系統(tǒng)將鍵盤(pán)鼠標(biāo)的行為包裝成一個(gè)Windows Message,然后系統(tǒng)主動(dòng)將這些Windows Message派發(fā)給特定的窗口,實(shí)際上消息是被Post到特定窗口所在線程的消息隊(duì)列,應(yīng)用程序的消息循環(huán)再不斷的從消息隊(duì)列當(dāng)中獲取消息,然后再派發(fā)給特定窗口類(lèi)的窗口過(guò)程來(lái)處理,在窗口過(guò)程中完成一次用戶交互。
其實(shí),WPF的底層也是基于Win32的消息系統(tǒng),那么對(duì)于WPF應(yīng)用程序來(lái)說(shuō),它是如何跟Win32的消息交互,這里到底存在一個(gè)什么樣的機(jī)制?接下來(lái)我會(huì)通過(guò)下面幾篇博文介紹這個(gè)消息機(jī)制:
WPF的消息機(jī)制(一)-讓?xiě)?yīng)用程序動(dòng)起來(lái)
WPF的消息機(jī)制(二)-WPF內(nèi)部的5個(gè)窗口
(1)隱藏消息窗口
?。?)處理激活和關(guān)閉的消息的窗口和系統(tǒng)資源通知窗口
?。?)用于UI窗口繪制的可見(jiàn)窗口
?。?)用于用戶交互的可見(jiàn)窗口
WPF的消息機(jī)制(三)-WPF輸入事件的來(lái)源
WPF的消息機(jī)制(四)-WPF中UI的更新
讓?xiě)?yīng)用程序動(dòng)起來(lái)
談到WPF的消息,首先應(yīng)該知道DispactherObject以及Dispatcher在WPF系統(tǒng)中的作用。
WPF大部分的對(duì)象都是從DispatcherObject派生的,從這里派生的對(duì)象具有一個(gè)明顯的特征,那就是:修改對(duì)象時(shí)所在的線程,和創(chuàng)建對(duì)象時(shí)所在線程必須為同一個(gè)線程,這就是微軟所謂的線程親緣性(Thread affinity)的最簡(jiǎn)單理解。那么誰(shuí)能保證線程親緣性呢?那就是Dispacher了。從DispatcherObject派生的類(lèi)型繼承三個(gè)重要的成員:Dispatcher屬性,CheckAccess(), VerifyAccess()方法。其中后面兩個(gè)方法就是檢驗(yàn)線程親緣性的。按照WPF的實(shí)現(xiàn),如果你自己定義了個(gè)WPF的類(lèi)型,并且是DispatcherObject的子類(lèi),你就必須在public的成員定義的邏輯開(kāi)始處,調(diào)用base.Dispatcher.VerifyAccess(),檢驗(yàn)線程親緣性。那么Dispatcher到底還做了什么事情呢?
首先,我們看一下一個(gè)WPF的Application在啟動(dòng)之后都走了哪些邏輯:

通過(guò)調(diào)用堆棧可以看出,藍(lán)色的部分是啟動(dòng)了一個(gè)線程,VisualStudio在Host的進(jìn)程當(dāng)中運(yùn)行當(dāng)前應(yīng)用程序;紅色的部分是從Application.Main函數(shù)開(kāi)始執(zhí)行,經(jīng)過(guò)幾個(gè)函數(shù)到達(dá)Dispatcher.Run(),最后到達(dá)Dispather.PushFrameInpl()方法。那么一個(gè)Application在Run之后,為什么要調(diào)用Dispatcher.Run()呢,他做了些什么事情你?如果通過(guò)Reflector仔細(xì)查看Application.Run(),你會(huì)發(fā)現(xiàn)里面實(shí)際起作用的代碼并不多,最后都是Dispatcher.Run在做事情。那么一個(gè)Application啟動(dòng)之后,按照以前對(duì)Win32的消息機(jī)制的理解,當(dāng)應(yīng)用程序啟動(dòng)后,必須進(jìn)入消息循環(huán),對(duì)于WPF,也是一樣的。那么WPF應(yīng)用程序是在什么地方進(jìn)入消息循環(huán)呢?其實(shí)這就是Dispatcher.Run()做的事情。查看上圖最后一步Dispacther.PushFrameImpl()的代碼,你會(huì)看到有下面的一段代碼:

很明顯,橙色的部分是一個(gè)循環(huán),看起來(lái)是不是很眼熟,跟Win32編程碰到的消息循環(huán)是否很像?對(duì)了,這就是WPF應(yīng)用程序進(jìn)入了消息循環(huán)。循環(huán)調(diào)用GetMessage方法從當(dāng)前線程的消息隊(duì)列當(dāng)中不停的獲取消息,取出一個(gè)msg之后,交給TranslateAndDispatchMessage方法Dispatch到不同的窗口過(guò)程去處理。這樣以來(lái),任何需要應(yīng)用程序處理的消息通過(guò)這個(gè)過(guò)程,被不同的窗口處理了,應(yīng)用程序就動(dòng)起來(lái)了。
下面的一篇我會(huì)介紹WPF當(dāng)中的Win32窗口,正是這些窗口,處理著來(lái)自系統(tǒng),或者來(lái)自應(yīng)用程序內(nèi)部的消息。
(1)隱藏消息窗口
(2)處理激活和關(guān)閉的消息的窗口和系統(tǒng)資源通知窗口
(3)用于用戶交互的可見(jiàn)窗口
(4)用于UI窗口繪制的可見(jiàn)窗口
WPF的消息機(jī)制(三)-WPF輸入事件的來(lái)源
WPF的消息機(jī)制(四)-WPF中UI的更新
WPF內(nèi)部的5個(gè)窗口
對(duì)于Windows系統(tǒng)來(lái)說(shuō),它是一個(gè)消息系統(tǒng),消息系統(tǒng)的核心就是窗口。對(duì)于WPF來(lái)說(shuō)也是如此。那么WPF內(nèi)部為什么需要窗口,又存在哪些窗口呢?
在上一篇,我們頻繁的提及“線程”,“Dispatcher”其實(shí),運(yùn)行WPF應(yīng)用程序所在的線程就是WPF所謂的UI線程,在Application.Run之后,調(diào)用Dispatcher.Run時(shí)會(huì)檢查當(dāng)前線程是否已經(jīng)存在了一個(gè)Dispatcher對(duì)象,如果沒(méi)有就構(gòu)造一個(gè),在這里,一個(gè)線程對(duì)應(yīng)一個(gè)Dispatcher。因此,WPF的對(duì)象在獲取this.Dispatcher屬性時(shí),不同對(duì)象取的都是同一個(gè)Dispatcher實(shí)例。另外,前面提到的“消息循環(huán)”,“消息隊(duì)列”等都是Win32應(yīng)用程序的概念,我們知道,提起這些概念,必然會(huì)跟Win32的“窗口”,“Handle”,“WndProc”之類(lèi)的概念離不開(kāi),那么WPF里面究竟有沒(méi)有“窗體”,“Handle”,“WndProc”呢?
我想說(shuō)的是:有,還不止一個(gè),只不過(guò)沒(méi)有暴露出來(lái),外面不需要關(guān)心這些。
通常情況下,一個(gè)WPF應(yīng)用程序在運(yùn)行起來(lái)的時(shí)候,后臺(tái)會(huì)創(chuàng)建5個(gè)Win32的窗口,幫助WPF系統(tǒng)來(lái)處理操作系統(tǒng)以及應(yīng)用程序內(nèi)部的消息。在這5個(gè)窗口中,只有一個(gè)是可見(jiàn)的,可以處理輸入事件與用戶交互,其他4個(gè)窗口都是不可見(jiàn)的,幫助WPF處理來(lái)自其他方面的消息。接下來(lái)我會(huì)來(lái)介紹究竟這5個(gè)Win32的窗口如何幫助WPF處理消息,我會(huì)根據(jù)每個(gè)窗口創(chuàng)建的順序來(lái)介紹。
隱藏消息窗口
創(chuàng)建時(shí)機(jī):在Application的構(gòu)造函數(shù)調(diào)用基類(lèi)DispatcherObject的構(gòu)造函數(shù)的時(shí)候,會(huì)創(chuàng)建一個(gè)Dispatcher對(duì)象,在Dispatcher的私有構(gòu)造函數(shù)當(dāng)中。
用途:實(shí)現(xiàn)WPF線程模型的異步調(diào)用。
談到異步調(diào)用,相信許多人都不陌生。WinForm下,我們通常為了使一些花費(fèi)較多時(shí)間的方法調(diào)用不影響UI的響應(yīng),會(huì)將這個(gè)操作分為很多步,然后使用BeginInvoke調(diào)用每一步,這樣UI響應(yīng)就不會(huì)被阻塞。BeginInvoke的本質(zhì)是往消息隊(duì)列當(dāng)中PostMessage,而不是直接調(diào)用,與此同時(shí),UI行為(MouseMove)導(dǎo)致系統(tǒng)也往消息隊(duì)列當(dāng)中PostMessage更新UI,但由于彼此花費(fèi)的時(shí)間很短,就感覺(jué)兩個(gè)消息是被同時(shí)處理似的,界面就不會(huì)覺(jué)得被阻塞了。WPF同樣面臨這樣的問(wèn)題,他是如何解決的呢?在這里Window 1#起著至關(guān)重要的作用。通過(guò)下面一副圖我們來(lái)看看Window 1#在做什么事情?
WPF也是通過(guò)BeginInvoke來(lái)解決的,而Wpf的BeginInvoke是在Dispatcher上面暴露了,因?yàn)檎麄€(gè)消息系統(tǒng)都是Dispatcher在協(xié)調(diào)。從上面圖可以看出Dispatcher在調(diào)用BeginInvoke之后所經(jīng)歷的流程,最終是什么時(shí)候Foo()被真正執(zhí)行的。
第一步,就是將調(diào)用的Delegate和優(yōu)先級(jí)包裝成一個(gè)DispatcherOperation放入Dispatcher維護(hù)的優(yōu)先級(jí)隊(duì)列當(dāng)中,這個(gè)Queue是按DispatcherPriority排序的,總是高優(yōu)先級(jí)的DispatcherOperation先被處理。關(guān)于優(yōu)先級(jí)相關(guān)知識(shí)可以參考MSDN對(duì)WPF線程模型的解釋。
第二步,往當(dāng)前線程的消息隊(duì)列當(dāng)中Post一個(gè)名為MsgProcessQueue的Message。這個(gè)消息是WPF自己定義的,見(jiàn)Dispatcher的靜態(tài)構(gòu)造函數(shù)當(dāng)中的
_msgProcessQueue=UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");
這個(gè)消息被Post到消息隊(duì)列之前,還要設(shè)置MSG.Handle,這個(gè)Handle就是Window 1#的Handle。指定Handle是為了在消息循環(huán)Dispatch消息的時(shí)候,指定哪個(gè)窗口的WndProc(窗口過(guò)程)處理這個(gè)消息。在這里所有BeginInvoke引起的消息都是Window1#的窗口過(guò)程來(lái)處理的。
第三步,消息循環(huán)讀取消息。
第四步,系統(tǒng)根據(jù)獲取消息的Handle,發(fā)現(xiàn)跟Window1#的Handle相同,那么這個(gè)消息派發(fā)到Window1#的窗口過(guò)程,讓其處理。
第五步,在窗口過(guò)程中,優(yōu)先級(jí)隊(duì)列當(dāng)中取一個(gè)DispatcherOperation。
第六步,執(zhí)行DispatcherOperation.Invoke方法,Invoke方法的核心就是調(diào)用DispatcherOperation構(gòu)造時(shí)傳入的Delegate,也就是Dispatcher.BeginInvoke傳入的Delegate。最終這個(gè)Foo()方法就被執(zhí)行了。
通過(guò)上面的六步過(guò)程,一次Dispatcher.BeginInvoke就被處理完成。而這個(gè)過(guò)程需要消息不斷的流動(dòng),就必須加入消息隊(duì)列,最后還要特定的窗口過(guò)程處理,而核心的東西就是這個(gè)隱藏的Window1#,他在WPF當(dāng)中只負(fù)責(zé)處理異步調(diào)用,其他的消息他不關(guān)心,剩余的4個(gè)窗口在處理。這個(gè)Window1#在WPF當(dāng)中被包了一層殼子,如果感興趣,你可以去查看類(lèi)型MessageOnlyHwndWrapper。
處理應(yīng)用程序激活和系統(tǒng)關(guān)閉的窗口(Window 2#)
創(chuàng)建時(shí)機(jī):在調(diào)用Application.Run之后,運(yùn)行到Application.EnsureHwndSource()方法當(dāng)中。
用途:派發(fā)Application的Activated,Deactivated,SessionEnding事件。
WPF為了安全起見(jiàn)沒(méi)有讓UI窗口來(lái)處理應(yīng)用程序激活,反激活,以及操作系統(tǒng)關(guān)閉時(shí)對(duì)應(yīng)的消息,而是內(nèi)部創(chuàng)建了一個(gè)隱藏的窗口,專(zhuān)門(mén)用來(lái)接收WM_ACTIVATEAPP和WM_QUERYENDSESSION兩個(gè)Windows消息。從線程的消息隊(duì)列拿到這兩個(gè)消息后,會(huì)觸發(fā)WPF的Application.Activated,Application.Deactivated,Application.SessionEnding這三個(gè)事件。
更詳細(xì)的可參考Application類(lèi)型的EnsureHwndSource(),AppFilterMessage(),這兩個(gè)方法。
上面的過(guò)程可用下圖描述:
系統(tǒng)資源更改通知窗口(Window 4#)
創(chuàng)建時(shí)機(jī):Application的MainWindow的Xaml被反序列化成對(duì)象之后,需要確認(rèn)Window的ThemeStyle的時(shí)候。
用途:處理當(dāng)操作系統(tǒng)的Theme發(fā)生改變后,以及諸如SystemColors,SystemFonts,電源,顯示器等跟系統(tǒng)關(guān)聯(lián)的資源發(fā)生改變時(shí),更新WPF這邊的表現(xiàn)。
WPF在應(yīng)用出現(xiàn)的MainWindow在初始化完成后,會(huì)創(chuàng)建一個(gè)隱藏的窗口,專(zhuān)門(mén)處理來(lái)自系統(tǒng)相關(guān)資源更新后的消息,比如WM_ThemeChanged,WM_SystemColorChanged,WM_DisplayChange,WM_PowerBroadcast等等。跟Window2#的初衷類(lèi)似,為了安全起見(jiàn),沒(méi)有通過(guò)可見(jiàn)的UI窗口來(lái)處理這些消息,而是內(nèi)容創(chuàng)建了這個(gè)隱藏的Window4#窗口來(lái)處理這些消息,確保UI窗口可以安全的更新由于系統(tǒng)Theme及相關(guān)資源改變后的表現(xiàn)。
上面的過(guò)程可用下圖描述:
也許你會(huì)問(wèn)為什么先講解了Window4,而沒(méi)有講用于用戶交互的可見(jiàn)窗口(Window3)?那是因?yàn)閃indow3的內(nèi)容比較多,而Window2#和Window4#相對(duì)比較獨(dú)立,因此,放在本文當(dāng)中介紹,關(guān)于Window3的詳細(xì)描述,將在下一篇介紹, 敬請(qǐng)關(guān)注!