IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Windows 10 UWP开发:如何不让界面卡死

    汪宇杰发表于 2016-02-18 11:22:00
    love 0

    自从.NET 4.5开始,C#多了一对新的异步关键词 async 和 await,如果不了解的朋友可以简单的看下下面的示意图。

    简单的说,就是在通常情况下,用户在界面上进行的操作,比如点击一个按钮之后,如果进行大量的计算,或者读写文件、网络请求等耗时的操作,那么程序的界面就会卡住,在这段时间里,任何交互都不会响应,直到后台的代码执行完毕才会继续响应用户操作。

    这种现象和你的应用是不是UWP没有什么卵关系,WinForm,WPF,Windows Phone SL/RT应用都会有这个问题。

    这个问题的原因是因为这些耗时的操作和界面(UI线程)没有关系,但却在UI线程上执行了。解决办法就是另起一个线程,让它在UI线程之外去执行代码,这样就不会锁死UI。就像这图:

    在.NET 4.5以前,要进行一个异步操作写法有点繁琐,.NET 4.5以后。我们只要把一个方法的签名声明为async Task,就可以直接变成异步方法。然后被调用者await。在UI线程上await后台操作,是不会卡住界面的。当然,前提是这个方法体内本身有异步API。不清楚的朋友可以去MSDN看看async await的介绍:

    使用 Async 和 Await 的异步编程(C# 和 Visual Basic)

    好了,话题回到UWP应用,今天我无聊的时候写了个算24点解法的应用:

    问题是后台计算24点的解法需要几秒钟时间,用户点击“求解”按钮以后,界面就卡住,直到计算完毕。

    原先的代码如下:

    ...
    public ObservableCollection<string> ResultList
    {
        get { return _resultList; }
        set { _resultList = value; RaisePropertyChanged(); }
    }
    
    private void DoCalc()
    {
        ResultList.Clear();
    
        // ... 各种计算逻辑
    
        foreach (一个神器的循环)
        {
    	ResultList.Add($"{expression} = {value}");
        }
    }
    ...

    这里的蛋疼之处在于,DoCalc()方法里没有任何.NET已提供的async API,没有await的机会。如果是.NET自带的异步API,比如写文件和网络请求(.NET Http Client),就可以天生被await。

    .NET里处理这种情况,用的是Task.Run(),它可以接受一个委托,然后起一个新线程去执行这个委托。由于返回类型是Task,所以可以被await。

    public static Task Run(Action action);

    所以DoCalc的方法签名就可以改成异步的了:

    private async Task DoCalc()

    然后里面塞个Task.Run(),然后把计算逻辑包在委托里面

    private void DoCalc()
    {
        ResultList.Clear();
    
    	await Task.Run(()=>
    	{
    		// ... 各种计算逻辑
    
    		foreach (一个神器的循环)
    		{
    			ResultList.Add($"{expression} = {value}");
    		}
    	});
    }

    但是这样是会爆炸的!原因是ResultList还在主线程(UI线程)上,Task新起的线程会跨线程访问这个ResultList变量,就会爆炸。

    所以我们的思路是让这个计算逻辑带返回值,算完以后把结果扔回UI线程,再给ResultList赋值,而不是直接更改ResultList。

    所以我们要把Action委托改成Func委托:

    Func<List<string>> funcCalc = () =>
    {
        var tempList = new List<string>();
    
        ...
    
        foreach (...)
        {
            tempList.Add($"{expression} = {value}");
        }
    
        return tempList;
    };

    然后这时候用Task.Run()的一个重载方法去执行,就能得到返回值:

    public static Task<TResult> Run<TResult>(Func<TResult> function);

    然后再给UI线程上的ResultList赋值:

    var list = await Task.Run(funcCalc);
    ResultList = list.ToObservableCollection();
    

    为了让交互更加友好,可以在界面里加个Progress Ring显示忙操作的状态:

    <ProgressRing Height="50" Width="50" IsActive="{Binding IsBusy}" />

    最后完工的异步方法如下:

    private async Task DoCalc()
    {
        IsBusy = true;
        ResultList.Clear();
    
        Func<List<string>> funcCalc = () =>
        {
            var tempList = new List<string>();
    		...
            foreach (...)
            {
                tempList.Add($"{expression} = {value}");
            }
    
            return tempList;
        };
    
        var list = await Task.Run(funcCalc);
        ResultList = list.ToObservableCollection();
    
        IsBusy = false;
    }

    最后要说的是,这种方法只针对由于大量CPU计算、磁盘IO和网络请求等非界面的逻辑造成的卡死,如果你的XAML写的像翔一样的,那么很有可能界面卡死是因为数据绑定、长列表没做虚拟化等等,那就另当别论啦~

     



沪ICP备19023445号-2号
友情链接