异步编程是风靡一时的移动应用程序开发的很好的理由。使用异步方法对于长时间运行的任务,比如下载数据,有助于保持您的用户界面响应,而不是使用异步方法,或不当使用 async/await,可以使应用程序的UI停止响应用户输入,直到长时间运行的任务完成为止。这可能导致用户体验不佳,从而导致对应用程序商店的评论不好,这对商业永远都不好。

今天我们将异步使用一看,如何利用它来防止不期望的ListView中的行为和意外。

什么是async/await?

 async 和await 关键词介绍了.NET 4.5使调用异步方法容易使你的代码更易读的异步。async/await的是语法糖,采用TPL(任务并行库)的幕后。如果您想在任务完成前在.NET 4.5上启动一个新的任务并在UI线程上运行代码,那么您的代码将类似于此:

// Start a new task (this launches a new thread)
Task.Factory.StartNew (() => {
    // Do some work on a background thread, allowing the UI to remain responsive
    DoSomething();
// When the background work is done, continue with this code block
}).ContinueWith (task => {
    DoSomethingOnTheUIThread();
// the following forces the code in the ContinueWith block to be run on the
// calling thread, often the Main/UI thread.
}, TaskScheduler.FromCurrentSynchronizationContext ());

那不是很漂亮。使用async/await,上述代码变理为

await DoSomething();
DoSomethingOnTheUIThread();

上面的代码在第一个示例中与第三方代码一样在后台编译,所以我们注意到,这只是语法糖,它是多么的甜蜜!

使用Async: Pitfalls

阅读有关使用async/await,可能见过“异步的方式“抛四周,但这到底是什么意思呢?简单地说,这意味着任何一个异步方法调用的方法 (一个方法,在其签名async关键字) 应该使用await关键字调用异步方法时。在调用异步方法的没有使用时候 await关键词会得到一个异常结果,抛出一个运行时期的异常,这会导致很难追踪问题。调用asyncUsing the 关键词标记的方法必须使用await关键词,比如:

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

这带来一个问题,如果你想使用await关键字调用异步方法时,您不能使用async修饰符对调用的方法,例如,如果调用方法的签名不能使用异步关键字或构造函数或一个操作系统调用的方法,比如在安卓中的 GetView的 ArrayAdapter 或者IOS中UITableViewDataSource的GetCell,比如:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    // Can’t use await keyword in this method as you can’t use async keyword
    // in method signature due to incompatible return type.
}

正如你可能知道,一个异步方法返回 void, Task, 或者 Task,返回 void 仅仅用来标记async事件发生。在上面的GetView访求的例子中,需要返回Android View,,不能改变为返回Task,因为操作系统方法没有使用await标记,不能处理返回Task 。不能添加async 关键词到上面的方法中,而且调用上面的方法中也不能使用await关键词。

为了解决这个问题,一种可能,因为我已经在过去的,只需调用一个方法 GetView (或类似的方法在签名的不可改变的平台) 作为中间方法, 然后调用异步方法的中间体的方法:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod();
    // more code
}

async Task IntermediateMethod()
{
     await MyMethodAsync();
}

问题是IntermediateMethod现在是 async方法可以使用等待,在MyMethodAsync方法中调用时。所以,你在这里什么也没有得到,IntermediateMethod现在是同步并且可以等待。额外的, GetView 方法会继续运行在所有调用IntermediateMethod()的代码中。这也许是可取的,也可能不是可取的。  如果在调用IntermediateMethod()代码取决于该IntermediateMethod()结果,, 那是不可取的。在这种情况下,你可能想使用Wait()方法调用(或Result属性)的异步任务,例如: 

public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod().Wait();
    // more code
}

在异步方法中调用Wait()会导致调用线程暂停,直到异步方法完成。如果这是UI线程,作为将在这里的话,那么你的UI将挂在异步任务运行。这是不友好的,在ArrayAdapter中,ListView的数据行是个List。用户将无法与列表视图交互,直到所有行的数据被下载,并且滚动可能是生涩的和/或完全不响应的,这不是一个好的用户体验。 在异常任务中有一个Result属性可以使用。如果异常任务使用Task返回数据并且做为异步方法的返回类型,那么你可以使用。这也将导致调用线程等待异步任务的结果:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    view.Text = IntermediateMethod().Result;
    // more code
}

async Task IntermediateMethod()
{
     return await MyMethodAsync(); // MyMethodAsync also returns Task in this example
}

其实做以上可能导致UI挂完全和ListView不密集,这是一种非起动器。它也可能只是生涩:

JerkyListView

一般来说,你应该避免使用wait()和Result,特别是在UI线程上。在iOS和Android的样本项目挂钩,在这个博客的结束, 你将看到ViewControllerJerky 和MainActivityJerky分别看到这种行为。Those files are not set to compile in the sample projects.这些文件没有设置编译示例项目。

使用异步的方式

所以,如何让我的“异步的方式”,在这种情况下?

围绕上述问题的一个方法是恢复到旧的TPL在异步/等待的基础。你要直接使用TPL,但只有一次开始异步方法调用链(并启动一个新线程就)。在某个地方,TPL将再次被直接使用,因为您需要使用TPL来启动一个新线程。你不能只使用异步/等待关键词启动一个新线程,所以一些方法倒链将不得不与TPL推出新的线程(或其他机构)。异步方法,启动一个新线程将一个框架的方法,像.NET HttpClient异步方法很多,如果不是大多数,例。如果不使用异步框架方法,有些方法你倒链将要推出一个新的线程,并返回的任务或任务。

让我们在一个Android项目采用getview例开始(虽然相同的理念将为任何平台,即Xamarin.iOS,Xamarin。形式等)就说我有一个ListView,我想填充文本从网上下载动态(更可能会下载整个字符串列表第一和然后填充列表的行与已下载的内容,但我在这里演示的目的,行下载字符串行加有场合,你可能想这样做的话)。我当然不想阻塞UI线程等待多个下载;相反,我希望用户能够开始工作的ListView,左右滚动,并有文字出现在每个ListView细胞作为文本被下载。我还想确保,如果一个单元格滚动了视图,当它被重用时,它将取消加载正在下载中的文本,并开始为该行加载新文本。我们做这个物流和取消标记。代码中的注释应该解释做了什么。

public override View GetView(int position, View convertView, ViewGroup parent)
{
    // We will need a CancellationTokenSource so we can cancel the async call
    // if the view moves back on screen while text is already being loaded.
    // Without this, if a view is loading some text, but the view moves off and
    // back on screen, the new load may take less time than the old load and
    // then the old load will overwrite the new text load and the wrong data
    // will be displayed. So we will cancel any async task on a recycled view
    // before loading the new text.
    
    CancellationTokenSource cts;

    // re-use an existing view, if one is available
    View view = convertView; // re-use an existing view, if one is available
    
    // Otherwise create a new one
    if (view == null) {
        view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
    }
    else
    {
        // If view exists, cancel any pending async text loading for this view
        // by calling cts.Cancel();
        var wrapper = view.Tag.JavaCast();
        cts = wrapper.Data;

        // If cancellation has not already been requested, cancel the async task
        if (!cts.IsCancellationRequested)
        {
           cts.Cancel();
        }
    }

    TextView textView = view.FindViewById(Android.Resource.Id.Text1);
    textView.Text = "placeholder";

    // Create new CancellationTokenSource for this view's async call
    cts = new CancellationTokenSource();

    // Add it to the Tag property of the view wrapped in a Java.Lang.Object
    view.Tag = new Wrapper { Data = cts };

    // Get the cancellation token to pass into the async method
    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;
}

总之,上述方法检查这是重复使用的电池,如果是这样的话,我们取消现有的异步文本下载如果仍然不完整。然后它加载的占位符文本进入细胞,启动异步任务下载正确的文本行,并返回占位符文本马上出发,从而填充ListView。这使UI响应,并在启动任务执行从Web获取正确文本的工作时显示单元格中的内容。作为文本被下载,你会看到改变下载占位符文本之一(不一定是为了由于不同的下载时间)。我添加了一个随机延时的异步任务模拟这种行为,因为我做了这样一个简单的、快速的要求。

这里是GetTextAsync(...)的实现:

async Task GetTextAsync(int position, CancellationToken ct)
{
    // Check to see if task was cancelled; if so throw cancelled exception.
    // Good to check at several points, including just prior to returning the string.
    ct.ThrowIfCancellationRequested();
    
    // to simulate a task that takes variable amount of time
    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;
}

请注意,我可以装饰λ传入的任务。与run() async关键字,从而让我等待叫我的异步方法,从而实现“异步的方式。“没有生涩的ListView!

SmoothListView

在行动中看到它

如果你想看到上面的行动,Xamarin.iOS,Xamarin.Android和Xamarin.Forms,看看我的 GitHub 仓库。 The iOS version is very similar to the above, the only difference being in how I attach the iOS版本以上非常相似,唯一不同的是我如何把cancellationtokensource的细胞由于没有标签的属性有一个Android的观点。Xamarin。形式,然而,没有一个直接等同于getview或getcell,我知道,所以我对相同的行为发起一个异步任务从主应用程序类的构造函数为每一行的文字。

异步编码快乐