某項目需要將實時傳來的漁船數據進行數據可視化,我負責Windows客戶端的卡頓優化,此處的卡頓指界面無響應。
第一步是對客戶端的行為的觀察,觀察卡頓發生的條件以及是否有規律。經過觀察,客戶端在網絡良好的情況下卡頓4~6秒,網絡較差的情況下更長,得出結論①卡頓與網絡狀況有關。在網絡穩定的情況下觀察卡頓發生的時間間隔,發現從開始卡頓到下一次開始卡頓間隔大概是20秒,得出結論②卡頓是周期性的。通過這兩個結論可以進一步假設卡頓是由於周期性的網絡請求導致的,網絡良好的情況下響應時間短,卡頓也短,反之也成立。找到客戶端的配置文件,發現該請求的確是20秒一詞,假設成立。
第二步進行抓包,進一步驗證,由於本項目使用SOAP通過遠程過程調用來實現請求和響應,一個service有多種方法,為了確定具體是哪個請求,抓包具體看。同樣,當那個請求開始的時候,客戶端開始卡頓,直到那個請求結束,客戶端恢復了正常,已經可以確定就是那個請求引起的客戶端卡頓。
現在假設一種比較簡單的情況,客戶端沒有使用多線程,在UI線程中請求網絡,導致界面假死。遂翻看代碼,代碼中明晃晃的Thread thread = new Thread...
,的確使用了多線程,但有些怪怪的,大致代碼如下:
Thread thread = new Thread(new ThreadStart(delegate {
this.Dispatcher.Invoke(new Action(() => {
// 獲取控件數據並且構造參數param
...
// 請求網絡
responseText = WebServiceHandler.ObjectWebServer.getObject(param);
// 解析響應數據
Object o = JsonTools<Object>.DeserializeList(responseText);
// 更新控件
...
}));
}))
thread.Start();
只能看懂一部分,創建線程,然后使用delegate
委托實際上是一個匿名方法傳入ThreadStart
中作為Thread
的參數。this.Dispatcher.Invoke
部分看不懂,不過大概是將一個Action
作為一個委托傳給Invoke
方法。
於是Google之,查到如下資料:
In WPF, only the thread that created a DispatcherObject may access that object. For example, a background thread that is spun off from the main UI thread cannot update the contents of a Button that was created on the UI thread. In order for the background thread to access the Content property of the Button, the background thread must delegate the work to the Dispatcher associated with the UI thread. This is accomplished by using either Invoke or BeginInvoke. The operation is added to the event queue of the Dispatcher at the specified DispatcherPriority. Invoke is a synchronous operation; therefore, control will not return to the calling object until after the callback returns.
只有創建DispatcherObject
對象的線程才能訪問之,其他線程不可訪問該對象,比如UI線程創建的對象其他后台線程就不可訪問。那么如果想訪問該UI線程創建的控件,就必須將一個訪問該控件的方法委托給創建控件的UI線程的Dispatcher
去執行,才能假借UI線程之手去訪問控件。this.Dispatcher.Invoke
就是使用UI線程的Dispatcher調用Invoke
方法執行該委托,但是這個委托中使用了比較耗時的網絡請求,也就是說該請求如果在Invoke
方法中,就會占用UI線程的時間片去執行,即使放在了創建的線程中。
解決的方法就是將訪問控件的部分放入Invoke
方法匯總,其他的耗時請求就在線程的方法中執行。然后這個問題就被輕易地解決了。
這個問題產生的原因是寫代碼的師兄不明白如何創建線程,就去網上找到了一個創建線程的代碼,並且炒上去,然后發現這個代碼並不能訪問看似全局數據的控件,就又去網上找代碼,發現把方法放進this.Dispathcer.Invoke
中就可以訪問了,以為就萬事大吉了。從這個例子中收獲的教訓是如果不完全明白代碼的細節,那么盡可能先去了解,不然如果直接炒上去,那么后續完全可能產生意想不到的Bug,而且要花費更大的精力去弄明白這個用法,然后再去解決。