GUI的程序有時候會因為等待一個耗時操作完成,導致界面卡死。本篇我們就UWP開發中可能遇到的情況,來討論如何優化處理。
假設當前存在點擊按鈕跳轉頁面的操作,通過按鈕打開的新頁面,在初始化過程中存在一些耗時的操作。
public void OnNavigatedTo(object obj) { var watch = new Stopwatch(); Debug.WriteLine("---------------Start"); watch.Start(); //假設耗時1秒 DoBusyWork(); //耗時1秒 int count = GetPersonCount(); //假設每創建一個Person耗時500毫秒 PersonList = CreatePersonList(count); watch.Stop(); Debug.WriteLine(watch.ElapsedMilliseconds); Debug.WriteLine("----------------Stop"); Notify = "頁面初始化已完成!計時:" + watch.ElapsedMilliseconds + "毫秒"; }
可以注意到以上方法都是順序同步執行完成的,在點擊跳轉按鈕后,會有一個明顯的卡死且非常尷尬的等待過程。GetPersonCount方法返回100這個數字的話,StopWatch記錄的用時會是大約7秒,在這7秒之后才會打開跳轉的頁面,這是一個無法忍受的時間。
優化的初步思路是將無需等待完成的操作放到非UI線程去做。這里發現DoBusyWork這個方法是可以剝離開的。
Task.Run(()=> { DoBusyWork(); });
寫完之后發現雖然減少了1秒,但是意義不大,還是很卡。而PersonList的賦值操作必須在UI線程執行,不能夠用Task來放到后台,這一步的優化貌似到這里就沒轍了。
接下來的思路是采用async和await這對關鍵字來進行異步編程,首先我們要明確使用了await的語句仍然是會阻塞並等待完成,才可以執行下一句的。不同的是程序會在await的時候yeid return一次,以使得UI線程保持響應。但錯誤或者不合適的使用await往往會導致意想不到的結果,甚至比同步執行更差的性能。我們先看第一版的異步程序:
public async void OnNavigatedTo(object obj) { var watch = new Stopwatch(); Debug.WriteLine("---------------Start"); watch.Start(); //不必要的等待,耗時1秒 await DoBusyWorkAsync(); //耗時1秒,返回數字100 int count = await GetPersonCountAsync(); //依然會造成長時間阻塞的Get方法 PersonList = await CreatePersonListAsync(count); watch.Stop(); Debug.WriteLine(watch.ElapsedMilliseconds); Debug.WriteLine("----------------Stop"); Notify = "頁面初始化已完成!計時:" + watch.ElapsedMilliseconds + "毫秒"; }
運行發現,Navigate到第二個頁面很快(這是await的功勞),但是等到PersonList完全加載出來,仍然耗時7秒。這里的第一個錯誤是不必要的await DoBusyWorkAsync這個方法,應該果斷去除await關鍵字,雖然Visual Studio會給出warning:“由於此調用不會等待,因此在此調用完成之前將會繼續執行當前方法。請考慮將 "await" 運算符應用於調用結果”。但仔細想想會發現我們的本意就是不等待該方法。如果想去掉該提示,可以考慮將DoBusyWorkAsync方法的返回值由Task改為void。
private async void DoBusyWorkAsync() { await Task.Delay(1000); }
改為void之后在捕獲異常時可能會沒有堆棧信息,考慮到這里是個簡單方法,就不用顧慮了。
CreatePersonListAsync方法依賴於GetPersonCountAsync的返回值,這種情況下沒有太好的優化方案。只能說GetPersonCountAsync的這一秒你值得等待。
至於CreatePersonListAsync方法本身的耗時達到了5秒,成為了性能瓶頸,對該方法進行分析:
private async Task<ObservableCollection<Person>> CreatePersonListAsync(int count) { var list = new ObservableCollection<Person>(); for (int i = 0; i < count; i++) { var person = await Person.CreatePresonAsync(i, i.ToString()); list.Add(person); } return list; }
可以看到阻塞發生在for循環的內部,每次 await Person.CreatePresonAsync都有500毫秒的等待發生。而實際上每個create preson的操作是獨立的,並不需要等待前一次的完成。代碼修改如下:
private ObservableCollection<Person> CreatePersonListWithContinue(int count) { var list = new ObservableCollection<Person>(); for (int i = 0; i < count; i++) { Person.CreatePresonAsync(i, i.ToString()).ContinueWith(_ => list.Add(_.Result),TaskScheduler.FromCurrentSynchronizationContext()); } return list; }
修改后運行效果還挺不錯的,首先頁面間的跳轉不再卡頓,同時PersonList的加載時間也有了明顯的縮短,在“頁面初始化已完成”這句話出現后很短的時間內,列表便加載完畢,不過仔細觀察發現元素的順序是錯亂的。

這是因為for循環里CreatePersonAsync的操作相當於並發進行,添加到List里的順序自然是不固定的。我們可以在插入前進行排序來修正。
private ObservableCollection<Person> CreatePersonListWithContinue(int count) { var list = new ObservableCollection<Person>(); for (int i = 0; i < count; i++) { Person.CreatePresonAsync(i, i.ToString()).ContinueWith(_ => { var person = _.Result; int index = list.Count(p => p.Age < person.Age); list.Insert(index, person); },TaskScheduler.FromCurrentSynchronizationContext()); } return list; }
至此程序才算有了一個比較好的效果,有兩點可以總結一下:
- 通過Task.Run將非UI相關的操作運行在后台線程上,減少不必要的等待時間
- 通過將耗時操作拆分成N個await返回的異步方法,可以使UI線程保持響應
GitHub:https://github.com/manupstairs/UWPSamples/tree/master/UWPSamples/KeepUIResponsive
