原文在此:https://blog.xamarin.com/getting-started-with-async-await/
英語非常爛,翻得不好請指出。
Async/await 翻譯
??異步編程對于流行的移動開發(fā)是很好的手段。對于長時間運行的任務(wù)使用異步方法(比如說下載數(shù)據(jù)的時候)可以保持你的界面有響應(yīng)。在不使用異步方法的時候或者不正確使用異步或等待的時候,會使你的應(yīng)用程序ui停止響應(yīng),然后用戶會無法輸入,直到這個任務(wù)執(zhí)行完成。這樣會導(dǎo)致用戶體驗很差,然后導(dǎo)致app在應(yīng)用商城的評論很差,所以不使用異步編程并不是一個好的方案。
??今天我們會看到使用異步和怎么利用它在ListView中預(yù)防出現(xiàn)不可控的行為和意外。
什么叫做async/await?
??在net4.5下分別引入了async和await關(guān)鍵字,使得你調(diào)用異步方法更方便并且讓你的代碼好讀。這個async/await的語法糖的原理是使用了TPL(Task Parallel Library 任務(wù)并行庫)。如果你想開始一個新的任務(wù)并且在ui線程的代碼運行之前優(yōu)先完成,你的代碼看起來可能是這樣的:
// 開始一個新的任務(wù) (創(chuàng)建一個新的線程)
Task.Factory.StartNew (() => {
// 在后臺線程上做點什么, 允許UI保持響應(yīng)
DoSomething();
// 當后臺工作完成, 繼續(xù)運行這個代碼塊
}).ContinueWith (task => {
DoSomethingOnTheUIThread();
// 以下的代碼強制ContinueWith的代碼在調(diào)用線程運行,一般是Main/UI線程
}, TaskScheduler.FromCurrentSynchronizationContext ());
這樣不夠完美,使用async/await,上面的會變成:
await DoSomething();
DoSomethingOnTheUIThread();
上述的代碼被后臺編譯成和第一個例子相同的TPL代碼,所以說,這僅僅只是語法糖但又如此甜蜜。
使用async/await的陷阱:
??在閱讀關(guān)于使用async/await之后,你可以看見一句話“總是異步”但真正的意思是什么呢?簡單地說,它的意思是任何一個異步方法的調(diào)用(即一個方法的簽名有async關(guān)鍵字)在調(diào)用異步方法的時候都應(yīng)該使用await關(guān)鍵字。當調(diào)用調(diào)用一個異步方法不使用await關(guān)鍵字的結(jié)果是拋出“運行時吞沒”的異常,導(dǎo)致問題的原因變得很難追蹤。使用await關(guān)鍵字的要求是:調(diào)用的異步方法的簽名async關(guān)鍵字。舉個例子:
async Task CallingMethod()
{
var x = await MyMethodAsync();
}
這就有一個問題,如果你想使用await關(guān)鍵字調(diào)用一個異步方法的時候,可你又不能使用async修飾這個調(diào)用的方法,例如說調(diào)用的方法簽名不能使用async關(guān)鍵字的方法,或者系統(tǒng)調(diào)用的構(gòu)造方法。比如說GetView在安卓系統(tǒng)的ArrayApdapter、或者GetCell在ios系統(tǒng)的ITableViewDataSource。例如說:
public override View GetView(int position, View convertView, ViewGroup parent)
{
/*
在方法簽名有不兼容的返回類型,使用無法使用async關(guān)鍵字,
因此無法使用await關(guān)鍵字
*/
}
正如你知道的,一個異步方法只能有void、Task或者Task<T>這三種返回,并且返回void的情況僅在使得異步處理事件的時候才使用。在上述GetView方法的情況下,你需要返回一個安卓的View,因為OS調(diào)用它所以明顯不能使用await關(guān)鍵字,所以不能改成返回Task<View>,因此不能處理為返回一個Task<T>。所以你不能在上述的方法中用async關(guān)鍵字修飾,所以你也就不能在調(diào)用上述的方法的時候使用await關(guān)鍵字。為了繞開這個,做一個可能可以實現(xiàn)的嘗試,就像過去做的一樣。從GetView調(diào)用一個中間(或者在無法如何不能改變簽名的平臺上)方法然后異步調(diào)用這個中間的方法。
public override View GetView(int position, View convertView, ViewGroup parent)
{
IntermediateMethod();
//更多代碼
}
async Task IntermediateMethod()
{
await MyMethodAsync();
}
這個問題是:“IntermediateMethod現(xiàn)在是一個異步方法所以應(yīng)該被等待就像MyMethodAsync方法一樣需要被等待。”所以你沒有實現(xiàn)什么東西,同樣的IntermediateMethod現(xiàn)在也是異步應(yīng)該被等待的。此外,GetView方法將繼續(xù)運行所有代碼之后才調(diào)用“IntermediateMethod()”,這可能不是很讓人滿意。如果后面的代碼調(diào)用“IntermediateMethod()”取決于“IntermediateMethod()”的結(jié)果,那么它不是讓人滿意的。在這種情況下,你可能會試著異步調(diào)用它的"Wait()"方法(或者Result屬性)。比如說:
public override View GetView(int position, View convertView, ViewGroup parent)
{
IntermediateMethod().Wait();
// 更多代碼
}
異步方法調(diào)用“Wait()”導(dǎo)致調(diào)用線程停止,直到異步方法完成才會恢復(fù)。如果這是Ui線程,那么你的UI將在異步Task運行的時候掛起。這不是很好,尤其是在ArrayAdapter在為ListView的行提供數(shù)據(jù)的時候,用戶將無法與ListView進行交互,直到所有的行的數(shù)據(jù)都已經(jīng)完成下載,并且滾動是完全沒有反應(yīng)或者有卡頓的,這不是一個好的用戶體驗。還有一個可以調(diào)用異步任務(wù)的Result屬性。如果你的異步任務(wù)是返回Task<T>,則使用以下這種寫法。這將導(dǎo)致調(diào)用的線程等待異步任務(wù)的結(jié)果。
public override View GetView(int position, View convertView, ViewGroup parent)
{
view.Text = IntermediateMethod().Result;
// 更多代碼
}
async Task<string> IntermediateMethod()
{
return await MyMethodAsync();
// 在這個例子中MyMethodAsync也返回Task<string>。
}
事實上按上面代碼那樣做可能導(dǎo)致你的UI完全掛起并且那個不啟動的ListView永遠都不會填充它。可能還是一卡一卡的。
一般來說,你應(yīng)該避免使用“Wait()”和"Result",特別是在UI線程上。在這個博客的末尾有一些Ios和安卓的項目,你可以分別看ViewControllerJerky和MainActivityJerky的這的實現(xiàn),這些文件夾沒有設(shè)置為在示例項目中進行編譯。
使用異步的方式:
那么在這種情況下異步如何實現(xiàn)呢?解決上面的問題的方法是回到以前實現(xiàn)async/await的TPL。你要直接使用TPL,但只有一次啟動異步方法調(diào)用鏈(并且馬上創(chuàng)建一個新線程)。TPL在某處將直接使用,同樣的你需要使用TPL啟動一個新的線程。不能僅僅使用async/await關(guān)鍵字啟動一個新的線程,所以對于一些方法的調(diào)用鏈不得不使用TPL啟動新的線程(或另外一種方法),啟動新線程的異步方法將會是一種框架方法,像許多(這種“多”不是絕大多的那種“多”)情況下的“.NET HttpClient”
異步方法。如果不使用異步框架方法,那么你的調(diào)用鏈中的某些方法不可不啟動一個新的線程并且返回Task或者Task<T>。
??讓我們開始一個例子使用GetView在安卓平臺的項目(盡管相同思想可以適用在任何平臺例如Xamarin.iOS, Xamarin.Forms),比如說我有一個istView,我想填充從網(wǎng)絡(luò)動態(tài)下載的文本(一般會先下載整個字符串列表,然后用已經(jīng)下載的內(nèi)容填充列表,但是為了演示,我逐行下載這些文本。再說了,可能有些地方也會要這么做呢)。
??我當然不想讓UI線程等待多次下載。反而,我希望ListView能夠開始就讓用戶可以滾動使用,并且隨著文本的下載,文本將在每個ListView的Item中顯示。我還想保證,如果一個Item滾出View,那么當它被重用的時候,它會取消加載正在下載的文本,并且開始為該行添加新的文本。我們會用TPL和取消Token來實現(xiàn)這件事,代碼的注釋應(yīng)該能說明正在做什么。
public override View GetView(int position, View convertView, ViewGroup parent)
{
/* 我們需要一個CancellationTokenSource,如果在文本已經(jīng)被加載的情況下
View在屏幕上重新展示的時候,可以取消這個異步調(diào)用。沒有這個的話,
如果一個View正在加載一些文本,但View在屏幕上移動并且返回,則新加載的
數(shù)據(jù)可能比舊加載的數(shù)據(jù)所需的時間少,然后舊的數(shù)據(jù)就會覆蓋新的數(shù)據(jù),這樣
就會顯示錯誤的數(shù)據(jù)。所以在加載新文本之前,我們要在View重新出現(xiàn)時取消任何異步
任務(wù)。
*/
CancellationTokenSource cts;
View view = convertView; // 如果有View可用,則重復(fù)利用現(xiàn)有的View
// 否則創(chuàng)建一個新的View
if (view == null) {
view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
} else {
//如果View存在, 調(diào)用cts.Cancel()取消此View在等待的異步加載文本任務(wù)
var wrapper = view.Tag.JavaCast<Wrapper<CancellationTokenSource>>();
cts = wrapper.Data;
// 如果請求尚未取消,則取消異步任務(wù)。
if (!cts.IsCancellationRequested)
{
cts.Cancel();
}
}
TextView textView = view.FindViewById<TextView>(Android.Resource.Id.Text1);
textView.Text = "placeholder";
// 為此View的“異步調(diào)用”創(chuàng)建新的CancellationTokenSource
cts = new CancellationTokenSource();
// 將其添加到包含在Java.Lang.Object中的View的Tag屬性中
view.Tag = new Wrapper<CancellationTokenSource> { 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,如果是,但是還不完整,我們將取消現(xiàn)有的異步文本下載任務(wù)。然后將占位符文本加載到Item中,啟動異步任務(wù)來下載改行的正確文本,并且立刻返回具有占位符文本的View,進而填充ListView。這樣會保持Ui的響應(yīng),并且在Item中顯示某些內(nèi)容,而啟動的任務(wù)會從Web獲得正確的文本。
??隨著文本的下載,你會看到占位符逐一更改成下載的文本(由于下載的次數(shù)不同,不一定是按順序排列的。)。因為我做了這樣簡單、快速的請求所以我給異步任務(wù)添加了一個隨機延遲來模擬這個行為,以下是GetTextAsync方法的實現(xiàn):
async Task<string> GetTextAsync(int position, CancellationToken ct)
{
// 檢查任務(wù)是否被取消,如果被取消則拋出“取消”的異常。
// 很好的檢查幾個點,包括在返回字符串之前
ct.ThrowIfCancellationRequested();
// 模擬一個任務(wù)需要的時間變量
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關(guān)鍵字來修飾傳入Task.Run()的Lambda,從而讓我等待著我的異步方法的調(diào)用,從而實現(xiàn)“總是異步”,在ListView上沒有多余的卡頓!。
在行動中看它:
如果你想看到上面的Xamarin.iOS、Xamarin.Android、Xamarin.Forms的實現(xiàn),請查看我的Github repo。Ios版本和上面的例子非常相似,唯一的區(qū)別在于如何將CancellationTokenSource附加到Item,因為其沒有Tag屬性。然而,Xamarin.Forms并沒有直接等同于我所知道的GetView或GetCell,所以我通過App的構(gòu)造函數(shù)啟動異步任務(wù)來模擬相同的行為來獲取每一行的文本。
??異步編程是快樂的!
譯者附送:
AsyncAllTheWayXamForms實現(xiàn),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<string>();
HttpClient client { get; set;}
Random rand { get; set;}
var indexes = new List<int>();
public App()
{
rand = new Random(DateTime.Now.Millisecond);
// 在項目列表占位符文本和數(shù)字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<string> 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啟動的時候調(diào)用(ps:在這里跟異步毫無關(guān)系,系統(tǒng)自帶方法)
}
protected override void OnSleep()
{
// 當app掛起的時候調(diào)用(ps:在這里跟異步毫無關(guān)系,系統(tǒng)自帶方法)
}
protected override void OnResume()
{
// 當app從掛起恢復(fù)的時候調(diào)用(ps:在這里跟異步毫無關(guān)系,系統(tǒng)自帶方法)
}
}
}