Xamrin 官方博客的文章Getting-started-with-async/await中文翻译

原文在此:https://blog.xamarin.com/getting-started-with-async-await/
英语非常烂,翻得不好请指出。
Async/await 翻译
  异步编程对于流行的移动开发是很好的手段。对于长时间运行的任务使用异步方法(比如说下载数据的时候)可以保持你的界面有响应。在不使用异步方法的时候或者不正确使用异步或等待的时候,会使你的应用程序ui停止响应,然后用户会无法输入,直到这个任务执行完成。这样会导致用户体验很差,然后导致app在应用商城的评论很差,所以不使用异步编程并不是一个好的方案。
  今天我们会看到使用异步和怎么利用它在ListView中预防出现不可控的行为和意外。
什么叫做async/await?
  在net4.5下分别引入了async和await关键字,使得你调用异步方法更方便并且让你的代码好读。这个async/await的语法糖的原理是使用了TPL(Task Parallel Library 任务并行库)。如果你想开始一个新的任务并且在ui线程的代码运行之前优先完成,你的代码看起来可能是这样的:

// 开始一个新的任务 (创建一个新的线程)
Task.Factory.StartNew (() => {
    // 在后台线程上做点什么, 允许UI保持响应
    DoSomething();
// 当后台工作完成, 继续运行这个代码块
}).ContinueWith (task => {
    DoSomethingOnTheUIThread();
// 以下的代码强制ContinueWith的代码在调用线程运行,一般是Main/UI线程
}, TaskScheduler.FromCurrentSynchronizationContext ());

这样不够完美,使用async/await,上面的会变成:

await DoSomething();
DoSomethingOnTheUIThread();

上述的代码被后台编译成和第一个例子相同的TPL代码,所以说,这仅仅只是语法糖但又如此甜蜜。

使用async/await的陷阱:
  在阅读关于使用async/await之后,你可以看见一句话“总是异步”但真正的意思是什么呢?简单地说,它的意思是任何一个异步方法的调用(即一个方法的签名有async关键字)在调用异步方法的时候都应该使用await关键字。当调用调用一个异步方法不使用await关键字的结果是抛出“运行时吞没”的异常,导致问题的原因变得很难追踪。使用await关键字的要求是:调用的异步方法的签名async关键字。举个例子:

async Task CallingMethod()
{
    var x = await MyMethodAsync();
}

这就有一个问题,如果你想使用await关键字调用一个异步方法的时候,可你又不能使用async修饰这个调用的方法,例如说调用的方法签名不能使用async关键字的方法,或者系统调用的构造方法。比如说GetView在安卓系统的ArrayApdapter、或者GetCell在ios系统的ITableViewDataSource。例如说:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    /*
    在方法签名有不兼容的返回类型,使用无法使用async关键字,
    因此无法使用await关键字
    */
}

正如你知道的,一个异步方法只能有void、Task或者Task这三种返回,并且返回void的情况仅在使得异步处理事件的时候才使用。在上述GetView方法的情况下,你需要返回一个安卓的View,因为OS调用它所以明显不能使用await关键字,所以不能改成返回Task,因此不能处理为返回一个Task。所以你不能在上述的方法中用async关键字修饰,所以你也就不能在调用上述的方法的时候使用await关键字。为了绕开这个,做一个可能可以实现的尝试,就像过去做的一样。从GetView调用一个中间(或者在无法如何不能改变签名的平台上)方法然后异步调用这个中间的方法。

public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod();
    //更多代码
}
 
async Task IntermediateMethod()
{
     await MyMethodAsync();
}

这个问题是:“IntermediateMethod现在是一个异步方法所以应该被等待就像MyMethodAsync方法一样需要被等待。”所以你没有实现什么东西,同样的IntermediateMethod现在也是异步应该被等待的。此外,GetView方法将继续运行所有代码之后才调用“IntermediateMethod()”,这可能不是很让人满意。如果后面的代码调用“IntermediateMethod()”取决于“IntermediateMethod()”的结果,那么它不是让人满意的。在这种情况下,你可能会试着异步调用它的"Wait()"方法(或者Result属性)。比如说:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod().Wait();
    // 更多代码
}

异步方法调用“Wait()”导致调用线程停止,直到异步方法完成才会恢复。如果这是Ui线程,那么你的UI将在异步Task运行的时候挂起。这不是很好,尤其是在ArrayAdapter在为ListView的行提供数据的时候,用户将无法与ListView进行交互,直到所有的行的数据都已经完成下载,并且滚动是完全没有反应或者有卡顿的,这不是一个好的用户体验。还有一个可以调用异步任务的Result属性。如果你的异步任务是返回Task,则使用以下这种写法。这将导致调用的线程等待异步任务的结果。

public override View GetView(int position, View convertView, ViewGroup parent)
{
    view.Text = IntermediateMethod().Result;
    // 更多代码
}
 
async Task IntermediateMethod()
{
     return await MyMethodAsync(); 
     // 在这个例子中MyMethodAsync也返回Task。
}

事实上按上面代码那样做可能导致你的UI完全挂起并且那个不启动的ListView永远都不会填充它。可能还是一卡一卡的。

image.png

一般来说,你应该避免使用“Wait()”和"Result",特别是在UI线程上。在这个博客的末尾有一些Ios和安卓的项目,你可以分别看ViewControllerJerky和MainActivityJerky的这的实现,这些文件夹没有设置为在示例项目中进行编译。

使用异步的方式:
那么在这种情况下异步如何实现呢?解决上面的问题的方法是回到以前实现async/await的TPL。你要直接使用TPL,但只有一次启动异步方法调用链(并且马上创建一个新线程)。TPL在某处将直接使用,同样的你需要使用TPL启动一个新的线程。不能仅仅使用async/await关键字启动一个新的线程,所以对于一些方法的调用链不得不使用TPL启动新的线程(或另外一种方法),启动新线程的异步方法将会是一种框架方法,像許多(这种“多”不是绝大多的那种“多”)情況下的“.NET HttpClient”
異步方法。如果不使用异步框架方法,那么你的调用链中的某些方法不可不启动一个新的线程并且返回Task或者Task
  让我们开始一个例子使用GetView在安卓平台的项目(尽管相同思想可以适用在任何平台例如Xamarin.iOS, Xamarin.Forms),比如说我有一个istView,我想填充从网络动态下载的文本(一般会先下载整个字符串列表,然后用已经下载的内容填充列表,但是为了演示,我逐行下载这些文本。再说了,可能有些地方也会要这么做呢)。
  我当然不想让UI线程等待多次下载。反而,我希望ListView能够开始就让用户可以滚动使用,并且随着文本的下载,文本将在每个ListView的Item中显示。我还想保证,如果一个Item滚出View,那么当它被重用的时候,它会取消加载正在下载的文本,并且开始为该行添加新的文本。我们会用TPL和取消Token来实现这件事,代码的注释应该能说明正在做什么。

public override View GetView(int position, View convertView, ViewGroup parent)
{
   /* 我们需要一个CancellationTokenSource,如果在文本已经被加载的情况下
       View在屏幕上重新展示的时候,可以取消这个异步调用。没有这个的话, 
      如果一个View正在加载一些文本,但View在屏幕上移动并且返回,则新加载的
      数据可能比旧加载的数据所需的时间少,然后旧的数据就会覆盖新的数据,这样      
      就会显示错误的数据。所以在加载新文本之前,我们要在View重新出现时取消任何异步    
      任务。
    */
    CancellationTokenSource cts;
    View view = convertView; // 如果有View可用,则重复利用现有的View
    
    // 否则创建一个新的View
    if (view == null) {
        view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
    } else {
        //如果View存在, 调用cts.Cancel()取消此View在等待的异步加载文本任务
        var wrapper = view.Tag.JavaCast>();
        cts = wrapper.Data;
        // 如果请求尚未取消,则取消异步任务。
        if (!cts.IsCancellationRequested)
        {
           cts.Cancel();
        }
    }
    TextView textView = view.FindViewById(Android.Resource.Id.Text1);
    textView.Text = "placeholder";
    // 为此View的“异步调用”创建新的CancellationTokenSource
    cts = new CancellationTokenSource();
    // 将其添加到包含在Java.Lang.Object中的View的Tag属性中
    view.Tag = new Wrapper { Data = cts };
    // 获得取消的Token并且传入异步方法。
    var ct = cts.Token;
    Task.Run(async () => {
        try
        {
            textView.Text = await GetTextAsync(position, ct);
        } catch (System.OperationCanceledException ex) {
            Console.WriteLine($"Text load cancelled: {ex.Message}");
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
        }
    }, ct);
    return view;
}

简单来说,上述方法检查这是否是一个重用的Item,如果是,但是还不完整,我们将取消现有的异步文本下载任务。然后将占位符文本加载到Item中,启动异步任务来下载改行的正确文本,并且立刻返回具有占位符文本的View,进而填充ListView。这样会保持Ui的响应,并且在Item中显示某些内容,而启动的任务会从Web获得正确的文本。
  随着文本的下载,你会看到占位符逐一更改成下载的文本(由于下载的次数不同,不一定是按顺序排列的。)。因为我做了这样简单、快速的请求所以我给异步任务添加了一个随机延迟来模拟这个行为,以下是GetTextAsync方法的实现:

async Task GetTextAsync(int position, CancellationToken ct)
{
    // 检查任务是否被取消,如果被取消则抛出“取消”的异常。
    // 很好的检查几个点,包括在返回字符串之前
    ct.ThrowIfCancellationRequested();
    // 模拟一个任务需要的时间变量
    await Task.Delay(rand.Next(100,500));
    ct.ThrowIfCancellationRequested();
    if (client == null)
    {
        client = new HttpClient();
    }
    string response = await client.GetStringAsync("http://example.com");
    string stringToDisplayInList = response.Substring(41, 14) + " " + position.ToString();
    ct.ThrowIfCancellationRequested();
    return stringToDisplayInList;
}

请注意,我可以使用async关键字来修饰传入Task.Run()的Lambda,从而让我等待着我的异步方法的调用,从而实现“总是异步”,在ListView上没有多余的卡顿!。

image.png

在行动中看它:
如果你想看到上面的Xamarin.iOS、Xamarin.Android、Xamarin.Forms的实现,请查看我的Github repo。Ios版本和上面的例子非常相似,唯一的区别在于如何将CancellationTokenSource附加到Item,因为其没有Tag属性。然而,Xamarin.Forms并没有直接等同于我所知道的GetView或GetCell,所以我通过App的构造函数启动异步任务来模拟相同的行为来获取每一行的文本。
  异步编程是快乐的!

译者附送:
AsyncAllTheWayXamForms实现,AsyncAllTheWayXamForms.cs

using System;
using Xamarin.Forms;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using UIKit;

namespace AsyncAllTheWayXamForms
{
    public class App : Application
    {
        var items = new ObservableCollection();
        HttpClient client { get; set;}
        Random rand { get; set;}
        var indexes = new List();
        public App()
        {
            rand = new Random(DateTime.Now.Millisecond);
            // 在项目列表占位符文本和数字1-50填充索引。
            for (int i = 0; i < 50; i++)
            {
                items.Add("Placeholder");
                indexes.Add(i);
            }
            // 随机赋值索引(这样列表会以随机的排序加载)
            for (int i = 0; i < 49; i++)
            {
                int swapindex = rand.Next(0, 49);
                int hold = indexes[i];
                indexes[i] = indexes[swapindex];
                indexes[swapindex] = hold;
            }
            // 这个app的根页面
            var content = new ContentPage
            {
                Title = "AsyncAllTheWayXamForms",
                Content = new ListView
                {
                    VerticalOptions = LayoutOptions.FillAndExpand,
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    ItemsSource = items
                }
            };

            MainPage = new NavigationPage(content);

            Task.Run(async () => 
            {
                if (client == null)
                {
                    client = new HttpClient();
                }
                for (int i = 0; i < 50; i++)
                {
                    string text = await GetItemAsync(i);
                    items.RemoveAt(indexes[i]);
                    items.Insert(indexes[i], text);
                }
            });
        }

        public async Task GetItemAsync(int i)
        {
            string response = 
                          await client.GetStringAsync("http://example.com");
            string stringToDisplayInList = response.Substring(41, 14) +
                           " " + indexes[i].ToString();
            return stringToDisplayInList;
        }

        protected override void OnStart()
        {
            // 当app启动的时候调用(ps:在这里跟异步毫无关系,系统自带方法)
        }

        protected override void OnSleep()
        {
            // 当app挂起的时候调用(ps:在这里跟异步毫无关系,系统自带方法)
        }

        protected override void OnResume()
        {
            // 当app从挂起恢复的时候调用(ps:在这里跟异步毫无关系,系统自带方法)
        }
    }
}

你可能感兴趣的:(Xamrin 官方博客的文章Getting-started-with-async/await中文翻译)