UWP開發入門(二十一)——保持Ui線程處於響應狀態


  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來放到后台,這一步的優化貌似到這里就沒轍了。

  接下來的思路是采用asyncawait這對關鍵字來進行異步編程,首先我們要明確使用了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;
        }

  至此程序才算有了一個比較好的效果,有兩點可以總結一下:

  1. 通過Task.Run將非UI相關的操作運行在后台線程上,減少不必要的等待時間
  2. 通過將耗時操作拆分成Nawait返回的異步方法,可以使UI線程保持響應

  GitHub:https://github.com/manupstairs/UWPSamples/tree/master/UWPSamples/KeepUIResponsive


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM