原文在此: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
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永远都不会填充它。可能还是一卡一卡的。
一般来说,你应该避免使用“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上没有多余的卡顿!。
在行动中看它:
如果你想看到上面的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:在这里跟异步毫无关系,系统自带方法)
}
}
}