UWP开发入门(二十一)——保持Ui线程处于响应状态

Posted 楼上那个蜀黍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于UWP开发入门(二十一)——保持Ui线程处于响应状态的主要内容,如果未能解决你的问题,请参考以下文章

java多线程快速入门(二十一)

Jmeter(二十一) - 从入门到精通 - JMeter断言 - 上篇(详解教程)

Python从入门到精通(二十一)Python并发编程互斥锁的运用以及线程通信

Java基础学习笔记二十一 多线程

第二十一次ScrumMeeting会议

javaweb学习总结二十一(servlet开发入门servlet生命周期以及调用过程)