十一、流体应用与异步模式

一个有吸引力的移动应用的关键属性之一是它的响应能力。 用户更希望应用不干扰用户的交互,而是能够以流畅的方式呈现和执行用户手势。 为了实现快速和流畅的应用规范,以及性能,异步执行模式可以发挥作用。 在开发 Xamarin 应用时,以及 ASP.NET Core 中,任务的框架和响应式模块都可以帮助分配执行线程,并创建一个平滑且不间断的执行流。

在本章中,我们将学习任务框架的真正组成部分以及与之相关的基本概念。 我们还将介绍与异步执行模型(包括可等待和可观察模型)相关的一些最重要的模式,然后将它们应用到应用的各个部分。 最后,我们还将了解本机异步执行模型。

下面几节将带你了解一些异步执行的关键实现场景:

  • 利用任务和待办事项
  • 异步执行模式
  • 本机异步执行

在本章结束时,您将能够将 TPL 和本地异步特性引入到您的移动和 web 应用中。 它们将用 Xamarin 和。net 5 编写,以帮助您创建响应更快、更敏捷的应用。

利用任务和待办事项

在本节中,我们将研究任务和异步执行模式的基础知识,并确定使它们成为任何现代应用的基本部分的主要因素。

用户体验(UX)是一个术语,用于描述 UI 组件的组成以及用户如何与它们交互。 换句话说,UX 不仅仅是应用是如何设计的,还包括用户对应用的印象。 在这种情况下,应用的响应性是定义应用质量的关键因素之一。

一般来说,一个简单的交互用例从用户交互开始。 这种交互可以是点击屏幕上的某个区域,在画布上的某个手势,或者在屏幕上可编辑字段中的实际用户输入。 一旦用户交互触发执行流,应用业务逻辑就负责更新 UI,以便通知用户输入的结果。

如您所见,在简单交互模型的异步版本中,应用开始执行指定的业务流,而不等待它完成。 同时,用户可以自由地与 UI 的其他部分进行交互。 一旦结果可用,就会通知应用的 UI 它完成了。

此交互模型定义并满足简单的执行场景,例如使用正则表达式验证电子邮件字段或显示一个展板来显示项目上所需的详细信息。 然而,随着交互模型和业务逻辑变得越来越复杂,并且出现了额外的依赖关系(如 web 服务),我们应该对用户评估应用正在进行的工作(例如,下载远程资源时的进度条)。 为此,我们可以扩展我们的交互模型,让它为用户提供持续的更新:

Figure 11.1 – Thread Pool Concept

图 11.1 -线程池概念

现在,UI 不断地从后台进程接收更新。 这些更新可以像加载器环的忙音信号一样简单,也可以像复杂的完成率组件的数据更新一样简单。 然而,这种模式提出了另一个问题,即应用 UI 如何处理来自后台处理的多个更新。 在回答这个问题之前,让我们仔细看看应用的 UI 基础结构和基于任务的执行。

任务执行

一个应用 UI,不管它是在哪个平台上实现的,总是遵循单线程模型。 即使底层平台或硬件支持多线程,运行时也要负责提供一个分派器来呈现 UI。 这有助于我们避免多个线程试图在同一时间更新屏幕的同一部分。

在这个单线程模型中,应用负责将后台处理留给子线程,并同步回 UI 线程。

。net 框架引入了基于任务的线程模型,也称为任务异步编程(点击)模型,在。net 4.0,自那以后,已成为异步执行规范 Xamarin 等,特别是在移动平台上。

简单地说,TAP 提供了对经典线程模型的抽象。 在这种方法中,开发人员和应用(隐式地)不直接负责处理线程的创建、执行和同步,而只是简单地创建异步工作块(即任务),从而允许底层运行时处理所有繁重的工作。 特别是考虑到。net Standard 是各种运行时(如。net Core 和 Mono)的完整抽象,这个抽象允许每个平台实现最适合平台的方式来处理多线程。 这就是为什么在跨平台模块中不能使用Thread类的主要原因之一。 取而代之的是特定于平台的框架模块(例如,Xamarin)。 iOS 和 Xamarin.Android)提供了对经典线程模型的访问。

可以使用Task类中提供的静态帮助器方法创建一个简单的异步块:

Task.Run(() =>
{
    // Run code here
})

在这个例子中,我们在任务中包装了一个同步代码块。 现在,声明方法可以返回创建的任务块。 或者,如果有其他异步块,它应该使用await关键字来执行该块,从而创建一个async方法:

public Task SimpleyAsyncChain()
{
    return Task.Run(...);
}
public async Task MyAsyncMethod()
{
    var result = await Task.Run(...);
    await OtherAsyncMethod(result);

    // example async method
    await Task.Delay(300);
}

本例中的两个实现都创建了一个可以在顶层等待的异步方法链。 异常处理也可以使用简单的try/catch块引入,这与使用同步代码没有什么不同:

public async Task<MyEntity> MyAsyncMethodWithExceptionHandling()
{
    MyEntity result = null;    
 try
 {
        result = await Task.Run(...);

 }
 catch(Exception ex)
 {
        // TODO: Log the exception
 }
    return result;
}

虽然任务可以顺序执行,这是在MyAsyncMethod方法中完成的,但如果异步块之间没有依赖关系,它们也可以并行执行,允许运行时尽可能多地利用多线程:

public async Task MyParallelAsyncMethod()
{
    var result = await Task.Run(...);
    await Task.WhenAll(OtherAsyncMethod(result), Task.Delay(300));
}

以 TAP 模型提供的为基础,让我们来看看以下用户故事:

“作为一个注册用户,我希望有一个专属于我的个人资料的视图,这样我就可以在应用中查看和验证我的公共信息。”

也许基于任务的方法最突出的应用是在应用需要与远程后端(例如,基于 rest 的 web 服务)交互时。 然而,任务是深度集成的,并且.NET Framework 实际上是处理多线程的方法。 例如,服务代理客户机的起点将是创建一个简单的 REST 客户机,该客户机将针对目标 API 端点执行各种 HTTP 方法。

在实现我们的 rest 客户端之前,我们需要定义客户端接口:

public interface IRestClient
{
    Task<TResult> GetAsync<TResult>(string resourceEndpoint, string id) 
    where TResult : class;
    Task<TEntity> PostAsync<TEntity>(string resourceEndpoint, TEntity 
    entity) where TEntity : class;
    Task<TEntity> PutAsync<TEntity>(string resourceEndpoint, string id, 
    TEntity entity) where TEntity : class;
    Task<TResult> DeleteAsync<TResult>(string resourceEndpoint, string 
    id) where TResult : class; }

我们可以使用更专门的方法来扩展这个接口,例如GetListAsync方法,它可以帮助序列化一个项目列表:

Task<IEnumerable<TResult>> GetListAsync<TResult>(string resourceEndpoint) where TResult : class;

现在,这些方法的实现可以使用简单的HttpClient方法来执行远程调用和某种类型的序列化/反序列化:

public async Task<TResult> GetAsync<TResult>(string resourceEndpoint, string id)
    where TResult : class 
{ 
    var request = new HttpRequestMessage(HttpMethod.Get, $"
    {resourceEndpoint}/{id}");
    var response = await _client.SendAsync(request); 
    if (response.IsSuccessStatusCode) 
    { 
        var content = await response.Content.ReadAsStringAsync(); 
        return JsonConvert.DeserializeObject<TResult>(content); 
    } 
    // TODO: Throw Exception? 
    return null; 
}

在这里,客户端成员字段是在RestClient的构造函数中初始化的,可能有一个基 URL 声明,以及额外的 HTTP 处理程序:

public RestClient(string baseUrl)
{
    // TODO: Insert the authorization handler?
    _client = new HttpClient();
    _client.BaseAddress = new Uri(baseUrl);
}

使用RestClient,我们可以创建另一个抽象级别来实现特定于 api 的方法调用,将数据转换对象转换为域实体:

public async Task<User> GetUser(string userId)
{
    User result = null;
    // Should we initialize the client here? Is UserApi client going to 
    //be singleton?
    var client = new RestClient(_configuration["serviceUrl"]);
    try
    {
        var dtoUser = await client.GetAsync<User>
        (_configuration["usersApi"], userId);
        result = User.FromDto(dtoUser);
    }
    catch (Exception ex)
    {  
        // TODO:  
    }
    return result;
}

这里,我们有一个异步方法链,它最终执行一个远程调用。 在此之上,我们现在必须将用户 API 检索调用连接到我们的视图模型,该模型应该立即加载相关的用户数据,以便它可以显示在目标视图上。 在这个用例中,触发业务流的用户交互可能是用户点击用户概要链接。 应用通过导航到目标视图进行响应,目标视图初始化视图模型。 视图模型依次为用户配置文件请求远程数据:

public async Task RetrieveUserProfile() 
{
    if (string.IsNullOrEmpty(NavigationParameter)) 
    { 
        // TODO: Error/Exception 
    } 
    var userId = NavigationParameter; 

    var userResult = await _usersApi.GetUser(userId);
    CurrentUser = userResult;
}

设置了CurrentUser属性后,视图将被通知并更新,以显示检索到的信息。

这个实现将在一个简单的异步链中工作,因为语言提供的async/await构造在编译过程中被转换为状态机。 这确保异步线程返回 UI 线程,以便视图模型的更新可以传播回 UI。

如果我们想确保用户数据分配是在 UI 线程上执行的,我们可以使用InvokeOnMainThread方法,指示运行时在主 UI 线程上执行异步代码块:

Device.BeginInvokeOnMainThread (() => {
    CurrentUser = userResult;
});

当我们处理多个同步上下文时,主线程的调用就成为异步链的重要组成部分。 但是什么是同步上下文,我们如何在多线程移动应用中管理它? 下一节将给出答案!

同步上下文

当使用 TAP 处理异步方法调用时,重要的是要理解asyncawait是 c#提供的语言结构,而实际的多线程执行是注入的编译器生成的代码,用于替换异步/await 块。 如果编译器生成的异步状态机是密切观察和分析async方法构建器,您会注意到,在任何异步等待电话,当前同步上下文捕获,后来,当异步操作完成后,再次使用执行继续行动。

在 Xamarin 的应用中,的同步上下文——类似于执行上下文——是指当前的线程异步阻止被称为,以及目标线程当前异步块应该屈服。 如果一个 ASP.NET Core 应用在放大镜下,同步上下文将引用当前的HttpRequest对象。 在某些情况下,线程池可能在同步上下文中发挥作用。

正如我们前面提到的,由于在执行开始时捕获的上下文可能用于主 UI 线程,因此,实际上,将在 UI 线程中使用UserProfile的前一个异步示例。 一旦检索操作完成,将在主线程上执行延续操作(即,将结果分配给视图模型)。 然而,将异步方法交还给 UI 可能会导致性能损失,如果没有正确处理等待链,甚至会导致死锁。 在灾难性的场景中,UI 线程可能最终等待异步块,而异步块又等待 UI 线程返回。 为了避免这种情况发生,强烈建议使用显式控制捕获的上下文并使用ConfigureAwait方法生成目标上下文。 此外,尤其是在本机移动应用中,您应该使用ConfigureAwait(false)将 UI 线程从任何长时间运行的任务同步中释放出来(也就是说,不要屈服于捕获的上下文)。 这确保异步方法不会合并回 UI 线程,并且异步组合在单独的线程池中处理。 例如,让我们在前面的例子中添加一个额外的异步方法到async链:

var userResult = await _usersApi.GetUser(userId).ConfigureAwait(false);
var additionalUserData = _usersApi.GetUserDetails(userId).ConfigureAwait(false);
CurrentUser = userResult;

与前面的示例不同,此方法中的最后一条语句(continuation 操作)将在与 UI 线程不同的线程上执行。 第一个异步调用不会返回给 UI 线程,因为ConfigureAwait创建了一个辅助同步上下文。 然后,这个辅助上下文将被用作第二个异步调用的捕获上下文,它将生成第二个异步调用。 最后,分配结果的语句将在这个次要上下文中执行。 这意味着如果没有BeginInvokeOnMainThreadhelper 的执行,UI 很可能不会被传入的数据更新。 然而,当然,我们必须谨慎使用多个同步上下文和主线程。 在创建响应式应用时,我们不希望这些事件驱动的异步任务对视图和视图模型造成破坏。 控制它们的最简单方法是使用其他可用的控制机制,比如锁、信号量和互斥锁。

单次执行保证

异步任务实现的另一个流行的领域是通过整个移动应用的视图模型公开的命令。 如果要执行的业务流程作为响应用户输入(例如,提交按钮执行更新调用用户配置文件)取决于异步代码块,然后命令应该以这样一种方式实现,您可以调用异步功能正常。

让我们用现有的视图模型来演示一下。 首先,我们需要实现我们的内部执行方法:

public async Task ExecuteUpdateUserProfile() 
{ 
    try 
    { 
        await _usersApi.UpdateUser(CurrentUser); 
    } 
    catch (Exception ex) 
    { 
        // TODO: 
    } 
}

现在,让我们声明我们的命令:

public ICommand UpdateUserCommand 
{ 
    get 
    { 
        if (_updateUserCommand == null) 
        {
            _updateUserCommand = new Command(async () => await 
            ExecuteUpdateUserProfile()); 
        } 

        return _updateUserCommand; 
    } 
}

此时,如果命令绑定到用户控件(如按钮),那么多次点击该按钮将导致同一命令的多次执行。 虽然这可能不会在业务流上造成任何问题(也就是说,用户将被多次更新当前数据),但它可能会导致性能下降和服务端不必要的资源消耗。

锁和监视器,以及我们在经典线程中熟悉的互斥锁实现,也可以使用SemaphoreSlim在基于任务的异步代码块中实现。 SemaphoreSlim的主要用途可以概括为对一个或多个异步块进行节流。

对于这个场景,我们可以初始化一个只有一个可用槽位的信号量:

private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);

在命令方法的执行块中,我们可以检查当前信号量是否有租约。 如果没有,我们在一个插槽上放置一个租约,并在命令执行完成后释放它:

public async Task ExecuteUpdateUserProfile() 
{
    if (Semaphore.CurrentCount == 0) 
    { 
        return; 
    } 
    await Semaphore.WaitAsync().ConfigureAwait(false); 
    try { ... } catch { ... } 

    Semaphore.Release(); 
}

这样,该命令不能同时执行多次,从而避免数据冲突。 这里需要注意的是,由于信号量计数是在命令执行后释放的,因此必须使用try/catch块来防止在错误发生后锁住信号量。

逻辑任务

在检索的例子中,我们在BeginInvokeOnMainThread块中执行视图模型数据分配块。 虽然这实际上保证了视图模型更改将被传播到 UI 线程,但使用这种类型的执行,我们不能真正地说一旦等待执行的异步方法完成,以及何时更新了视图模型。 此外,UI 执行块可以使用另一个异步代码块(例如,在检索数据时显示一个弹出窗口)。 在这种情况下,我们可以利用一个任务完成源,这样我们就可以更严格地控制异步代码块何时真正完成:

public async Task RetrieveUserProfile() 
{
    // Removed for brevity 

    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    var userResult = await _usersApi.GetUser(userId);     
    Device.BeginInvokeOnMainThread(async () => {                 
        CurrentUser = userResult;
        await ShowPopupAsync(); // async method
        tcs.SetResult(0);
    });
    await tcs.Task.ConfigureAwait(false);
}

在本例中,我们使用了TaskCompletionSource,它表示异步状态机并接受结果或异常。 这个状态机只有在 UI 块的执行完成和RetrieveUserProfile方法最终完成时才会得到结果。

TaskCompletionSource在描述异步块方面的本地 UI 流时也很有用。 例如,用户从可用内容提供者中选择媒体文件的完整 UI 流程可以描述为异步块。 在这种情况下,完成源将在用户打开文件选择器对话框时初始化,并且在用户从所选内容源中选择某个文件时设置结果。 如果用户点击某个对话框上的取消按钮,该实现可以扩展为抛出异常。 这样,由多个屏幕和交互组成的用户流可以抽象为异步方法,这意味着它们可以被应用的视图或视图模型轻松使用。

命令模式

命令模式是无功移动应用的流量模式的衍生。 在 Android 世界,这种模式下以类似的方式实现这个名字模型视图的目的(本研究),其唯一目的是创建一个单向的数据流和减少【显示】的复杂性源于的双重性质Model-View-ViewMode(MVVM【病人】)。

在此模式中,每个视图都配备了多个命令,这些命令是自包含的执行块,引用了底层应用基础设施(类似于工作单元)。 在这种情况下,用户交互被路由到相应的命令,命令的结果通过广播传播到相关的控件(例如,使用BroadcastReceiver实现)。

. net 标准中的任务基础设施及其实现的运行时,例如. net Core,允许开发人员实现可等待上下文元素,这些元素可以很容易地表示命令,并可以使用Task语法等待。

要使类实例具有可等待性,它应该实现GetAwaiter方法,而该方法又由. net 任务基础结构使用。 在命令模式实现中,我们可以先创建一个用于依赖注入的基抽象类,并实现awaitable方法:

public abstract class BaseCommand
{
    protected BaseCommand(IConfiguration configurationInstance, IUserApi userApi)
    {
        ConfigurationService = configurationInstance;
        UserApi = userApi;
    }

    protected IConfiguration ConfigurationService { get; private set; }
    protected IUserApi UserApi { get; private set;}
 public virtual TaskAwaiter GetAwaiter()
 {
 return InternalExecute().GetAwaiter();
 }
 protected virtual async Task InternalExecute()
 {
 }
}

我们还可以扩展命令实现来实际返回一个结果:

public abstract class BaseCommand<TResult> : BaseCommand
{
    protected TResult Result { get; set; }
    public new TaskAwaiter<TResult> GetAwaiter()
    {
        return ProcessCommand().GetAwaiter();
    }
    protected override async Task InternalExecute()
    {
        Result = await ProcessCommand().ConfigureAwait(true);
        await base.InternalExecute().ConfigureAwait(true);
    }
    protected virtual async Task<TResult> ProcessCommand()
    {
        // To be implemented by the deriving classes
        return default(TResult);
    }
}

现在,一个实际命令的实现—例如,更新用户配置文件—看起来类似如下:

public class UpdateUserCommand : BaseServiceCommand<User>
{
    User _userDetails;
    public UpdateProfileCommand(IConfiguration configuration, IUsersApi 
    usersApi, User user):
        base(configuration, usersApi)
    {
        _userDetails = user;
    }

    protected async override Task<string> ProcessCommand()
    {
        try 
        { 
            Result = await _usersApi.UpdateUser(CurrentUser);
            return Result;
        } 
        catch (Exception ex) 
        { 
            // TODO: 
        } 
    }
}

最后,实现的命令可以初始化并像这样执行:

var result = await new UpdateUserCommand(configuration, usersApi, user);

这里,base 命令还可以利用服务定位器或某种类型的属性注入,这样就不需要将服务集与命令参数一起注入。 此外,可以利用消息传递服务广播它成功地为多个用户控件执行了命令。

创造生产者/消费者

线程安全集合是。net Core 中异步工具集的宝贵成员,就像它们在完整的。net 框架中一样。 通过使用阻塞集合,可以实现并发模型,为多线程上的异步任务提供公共基础。 毫无疑问,这些模型中最突出的是生产者/消费者模式实现。 在此范例中,在并行线程/任务上执行的方法将生成数据项,这些数据项将被另一个称为消费者的并行操作使用,直到达到一个边界限制或生产完成。 这两个方法将共享相同的阻塞集合,其中阻塞集合将充当两个异步块之间的代理。

让我们用一个小的实现来说明这个模式:

  1. 我们将从创建阻塞集合开始,该集合将用于存储Auction项:

    BlockingCollection<Auction> auctions = new BlockingCollection(100);

  2. 我们现在可以使用后台任务将Auction项添加到阻塞集合中。 这里,GetNewAuction方法将检索/创建拍卖实例,并将它们向下推到管道中:

    Task.Run(() => { while(hasMoreAuctions) { auctions.Add(GetNewAuction); } auctions.CompleteAdding(); });

  3. 与生产者类似,我们可以启动一个单独的消费者线程,该线程将处理交付的项目:

    Task.Run(() => { while (!auctions.IsCompleted) { Process(auctions.Take()); } }

  4. 将这个实现更进一步,我们可以使用GetConsumingEnumerable方法来创建一个阻塞枚举对象:

    Task.Run(() => { foreach(var auction in auctions.GetConsumingEnumerable()) { Process(auction); } }

  5. 最后,通过使用Parallel.ForEach,我们可以添加更多的消费者,而不需要通过非琐碎的同步实现:

    Parallel.Foreach(auctions.GetConsumingEnumerable(), Process);

现在,生产者生成的数据将被多个消费者消费,直到拍卖集合发送IsCompleted信号,这将导致消费的枚举对象中断并继续执行代码的其余部分。 在这种设置中,每个消费者将接收不同的数据项。 但是,如果有多个消费者期望同一组数据执行不同的操作,那会怎样呢? 这种类型的设置可以通过一个可观察/观察者实现来实现。

使用可见数据流

在。net 4 中引入了IObserverIObservable接口,它们构成了可观察的和所谓的反应模式的基础。 IObservable最突出的实现是在 Rx Extensions NuGet 库中。 目前这是一个开源项目,由。net 基金会管理。

在我们进入反应性数据之前,让我们后退一步,尝试演示不同类型的数据流。 我们将从一些简单的同步代码开始这个演示,然后我们可以使用这些代码作为其他实现的基本需求。 在我们开始之前,您应该创建一个新的.NET 5 控制台项目,并修改Main方法,使其为async:

class Program
{
    static async Task Main(string[] args)
    {
    }
}

现在已经创建了控制台应用项目,让我们添加一个名为GetNumbers的新方法:

public static IEnumerable<int> GetNumbers()
{
    var count = 0;
    while (count < 10)
    {
        yield return count++;
    }
}

这个方法将创建一个同步数据管道,通过一个简单的for/each循环将其打印在屏幕上:

Console.WriteLine("Synchronous Data");
foreach (var item in GetNumbers())
{
    Console.WriteLine($"Sync: {item}");
}

这里发生的情况是,每次for/each循环从可枚举对象请求一个新项时,GetNumbers方法生成一个数字,直到这些数字达到 10。

现在,假设我们要异步地检索并将这些数据推送到管道中。 我们不能使用Task<IEnumerable<int>>,因为这意味着我们需要等待完整的数据集完成加载,并且我们可以枚举它。 然而,我们可以使用异步任务的“流”:

public static IEnumerable<Task<int>> GetAsyncNumbers()
{
    var count = 0;
    while (count < 10)
    {
        yield return Task.Run(async () =>
            {
                await Task.Delay(200);
                return count++;
            });
    }
}

这里,我们使用一个Task.Delay调用来模拟一个异步操作。 现在,让我们使用这个新的生成器来打印数据:

Console.WriteLine("Asynchronous Data");
foreach (var itemTask in GetAsyncNumbers())
{
    await itemTask
        .ContinueWith(_ => Console.WriteLine($"Async: {_.Result}"));
}

在这个阶段,这个实现的发展可以向多个不同的方向发展。 第一个方向是创建一个阻塞集合,并遍历可消费的 enumerable。 现在,让我们重写我们的数据源来生成一个阻塞集合:

private static BlockingCollection<int> StartGetNumbers()
{
    var blockingCollection = new BlockingCollection<int>();
    Task.Run(
        async () =>
            {
                var count = 0;
                while (count < 10)
                {
                    await Task.Delay(200);
                    blockingCollection.Add(count++);
                }
                blockingCollection.CompleteAdding();
            });
    return blockingCollection;
}

按照前面的示例,消费者看起来类似于下面的:

Console.WriteLine("ConsumerProducer");
foreach (var item in StartGetNumbers().GetConsumingEnumerable())
{
    Console.WriteLine($"Consumer: {item}");
}

我们可以采取的另一个方向是创建一个可枚举的async:

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    var count = 0;
    while (count < 10)
    {
        await Task.Delay(1000);
        yield return count++;
    }
}

现在,我们可以异步地消耗枚举对象:

Console.WriteLine("Async Stream");
await foreach (int number in GetNumbersAsync())
{
    Console.WriteLine($"Async Stream: {number}");
}

到目前为止,我们已经异步地使用了带有阻塞集合和异步枚举的数据源。 现在,让我们看看使用可观察对象会是什么样子。

可观察对象可以使用 Rx 扩展创建,它可以在 NuGet 包中同名的System.Reactive命名空间下找到。 如果我们用一个可观察对象重写之前的实现,它看起来会是这样的:

private static IObservable<int> GetNumbersObservable()
{
    return Observable.Create<int>(
        async _ =>
            {
                var count = 0;
                while (count < 20)
                {
                    await Task.Delay(200);
                    _.OnNext(count++);
                }
                _.OnCompleted();
            });
}

这个实现中的重要部分是OnNextOnCompleted方法,它们控制数据流。 这个实现将产生一个冷的观察对象,当第一个观察对象连接到它时,它将开始产生数据。 如果它是一个热门的可观察对象,数据源将推送新项,而不管当前的观察者订阅计数。 关于热可观察对象的另一个特性是,它们将表现为多播生产者,而对于冷可观察对象,我们要么重申事件,要么有一组相互竞争的消费者,这取决于可观察函数的设置。

现在我们已经创建了我们的可观察对象,让我们为它创建一个订阅:

Console.Write("Observables");
var observable = GetNumbersObservable();
var subscriber = observable
    .Subscribe(_ => Console.WriteLine($"Observer: {_}"));

在通知集合和使用System.Reactive.Linq实现时,可观察对象非常灵活。 这样,就可以引入过滤和数据转换来帮助您修改到达观察者的数据。 例如,使用前面的例子,我们可以引入一个只推送偶数的过滤器:

var evenSubscriber = observable
    .Where(_ => _ % 2 == 0)
    .Subscribe(_ => Console.WriteLine($"Even Observer: {_}")); 

Rx 项目中还有来自其他异步模式的各种附加控制方法和转换策略。 在这里,我们只是演示了一个简单的数据管道示例并对其进行了过滤。 此外,还有另一个 Xamarin 的开源项目,它建立在 Rx Extensions 提供的基础上。

在本节中,我们简要概述了. net 上可用的异步功能,以及如何在应用 UI 和域实现中利用它们。 在下一节中,我们将研究移动应用中针对视图及其关联的视图模型或控制器的更专门的执行模式。

异步执行模式

任务通常用于为异步块创建一个简单的顺序执行。 然而,在某些情况下,等待任务完成可能是不必要或不可能的。 我们可以列举几个不可能或不需要等待任务的场景:

  • 如果我们正在执行异步块(类似于 update user 命令),我们只需将该命令绑定到控件上,并以“扔了就忘了”的方式执行它。
  • 如果我们的异步块需要在构造函数中执行,我们将没有简单的方法来等待任务。
  • 如果异步代码需要作为事件处理程序的一部分执行。

关于常见的问题,这里可以列出多个例子,例如以下:

  • 方法声明不应该显示asyncvoid返回类型。
  • 方法不应该被强制与Wait方法或Result属性同步执行。
  • 依赖于异步块结果的方法; 应该避免竞争条件。

可以使用各种模式来规避这些不可期待的场景。 在下面几节中,我们将仔细研究如何初始化一个依赖于将要完成的异步流程的视图模型。 然后,我们将把我们的 TPL 知识应用于异步事件处理。 最后,我们将学习如何在命令中处理异步方法。

服务初始化模式

在前面描述的构造函数场景中,让我们假设视图模型的构造函数应该检索一定数量的数据。 这将被相同视图模型的方法或命令使用。 如果我们执行方法而不等待结果,就不能保证在执行命令时,async构造函数的执行已经完成。

让我们用一个抽象的例子来说明这一点:

public class MyViewModel
{
    public MyViewModel()
    {
        // Can't await the method;
        MyAsyncMethod();
    }

    private async Task MyAsyncMethod()
    {
        // Load data from service to the ViewModel
    }
    public async Task ExecuteMyCommand()
    {
        // Data from the MyAsyncMethod is required
    }
}

当视图模型初始化后立即调用ExecuteMyCommand方法时,很有可能会出现竞争条件和可能的 bug,从而在一段时间内无法进行开发。

在所谓的服务初始化模式中,为了验证MyAsyncMethod是否成功执行,我们可以将生成的任务分配给一个字段,并使用该字段等待之前启动的任务:

public class MyViewModel
{
    private Task _myAsyncMethodExecution = null;
    public MyViewModel()
    {
        _myAsyncMethodExecution = MyAsyncMethod();
    }
    // ...
    public async Task ExecuteMyCommand()
    {
        await _myAsyncMethodExecution;
        // Data from the MyAsyncMethod is required
    }
}

这样,就避免了异步竞争条件,并且命令的执行将需要确保任务引用已经完成。

异步事件处理

正如前面提到的,如果调用链要求异步执行,那么async链应该一直传播到调用层次结构的顶层。 偏离此设置可能会导致线程问题、竞态条件和可能的死锁。 然而,同样重要的是,方法不偏离asyncTask 声明,确保async堆栈和任何生成的结果和错误都被保留。

带有异步代码的事件处理程序就是一个很好的例子,在这种情况下,我们对方法的签名没有太多要说的。 例如,让我们看一下按钮点击处理程序,它应该执行一个awaitable方法:

public async void OnSubmitButtonTapped(object sender, EventArgs e)
{
    var result = await ExecuteMyCommand();
    // do additional work
}

一旦这个事件处理程序订阅了按钮所单击的事件,异步代码将被正确地执行,并且我们不会注意到它的任何问题。 然而,使用 void 作为返回类型的方法声明将绕过运行时的错误处理基础结构,在出现错误的情况下,无论异常源是什么,应用都将崩溃,而不会留下任何错误的痕迹。 我们还应该提到,与这种类型的声明相关的编译器警告将被添加到项目的技术债务中。

在这里,我们可以创建 TAP 到异步编程模型(APM)的转换,它可以将异步链转换为回调方法,并引入一个错误处理程序。 这样,就不需要用异步签名声明OnSubmitButtonTapped方法。 我们可以很容易地引入一个扩展方法,它将使用回调函数执行异步任务:

public static class TaskExtensions 
{
    public static async void WithCallback<TResult>(
        this Task<TResult> asyncMethod, 
        Action<TResult> onResult = null, 
        Action<Exception> onError = null) 
    { 
        try 
        { 
            var result = await asyncMethod; 
            onResult?.Invoke(result); 
        } 
        catch (Exception ex) 
        { 
            onError?.Invoke(ex); 
        } 
    } 
}

可以引入另一个扩展方法来转换任务而不返回任何数据:

public static async void WithCallback(
    this Task asyncMethod, 
    Action onResult = null, 
    Action<Exception> onError = null) 
{ 
    try 
    { 
        await asyncMethod; 
        onResult?.Invoke(); 
    } 
    catch (Exception ex) 
    { 
        onError?.Invoke(ex); 
    } 
} 

现在,我们的异步事件处理程序可以重写以利用扩展方法:

public void OnSubmitButtonTapped(object sender, EventArgs e)
{
    ExecuteMyCommand()
        .WithCallback((result) => { 
            //do additional work
        });
}

这样,我们将优雅地中断异步链,而不会危及任务的基础结构。

异步命令

在异步 UI 实现中,几乎不可能避免处理异步任务的命令声明和绑定。 这里的一般方法是创建一个async委托,并将其作为操作传递给命令。 然而,这种基于承诺的执行削弱了我们查看异步块的完整生命周期的能力。 这使得为这些块创建单元测试变得更加困难,并且避免了终端事件(例如导航到不同的视图或关闭应用)中断执行的情况。

让我们看看我们之前实现的UpdateUserCommand:

_updateUserCommand = new Command(async () => await ExecuteUpdateUserProfile()); 

在这里,该命令只负责初始化用户配置文件更新。 但是,一旦执行了命令,就绝对不能保证完成了ExecuteUpdateUserProfile方法的全部执行。

为了弥补异步执行监视或缺乏异步执行监视,我们可以实现一个异步命令,该命令在命令本身中跟随任务的执行。 让我们从声明异步命令接口开始:

public interface IAsyncCommand : ICommand 
{
    Task ExecuteAsync(object parameter); 
}

在这里,我们声明了主执行方法的异步版本,它将被实际的命令方法使用。 让我们实现AsyncCommand类:

public class AsyncCommand : IAsyncCommand 
{
    // ...
    public AsyncCommand(
        Func<object, Task> execute,
        Func<object, bool> canExecute = null,
        Action<Exception> onError = null)
    {
        // ...
    }
    // ...
}

该命令将是接收异步任务和错误回调函数。 然后async将使用异步委托,如下所示:

public async Task ExecuteAsync(object parameter) 
{
    if (CanExecute(parameter)) 
    { 
        try 
        {
            await _semaphore.WaitAsync();
            RaiseCanExecuteChanged(); 
            await _execute(parameter); 
        }
        finally 
        { 
            _semaphore.Release(); 
        } 
    } 

    RaiseCanExecuteChanged(); 
}

注意,我们现在已经成功地集成了以前在异步命令块中实现的一次性执行修复。 每次租用信号量时,我们将引发一个事件,从而将CanExecute更改事件传播到绑定的用户控件。

最后,实际的ICommand接口将通过使用回调转换的扩展方法来使用ExecuteAsync方法:

void ICommand.Execute(object parameter) 
{
    ExecuteAsync(parameter).WithCallback(null, _onError); 
}

现在,应用单元测试可以直接使用ExecuteAsync方法,而绑定仍然使用Execute方法。 我们甚至可以通过公开 task 类型的属性进一步扩展这个实现,就像我们在服务初始化模式中所做的那样,从而允许连续的方法检查方法完成情况。

本节主要讨论异步执行模式,这些模式不仅可以帮助我们实现 Xamarin 应用,还可以帮助我们实现 ASP。 净的 web 服务。 当然,这些模式利用。net Core 或 mono 运行时,当移动应用实际正在使用时也适用。 如果我们有一个场景需要后端进程在应用的后台存在,我们可能需要求助于使用目标平台的本地特性。

本机异步执行

除了。net Core 提供的异步基础设施外,Xamarin 目标平台还提供了一些后台执行过程,这些过程可以帮助那些正在实现模块的开发人员,这些模块可以在应用没有实际工作时工作。 反过来,各种业务流程与主应用 UI 分开执行,创建轻量级和响应性强的 UX。

Android 服务

在 Android 平台上,后台进程可以作为服务实现。 服务是执行模块,可以按需启动或按计划启动。 例如,一个已启动的服务可以带有一个意图来启动。 这将一直运行,直到请求终止(或自行终止)。 这里,重要的是要注意,一旦意图实现,启动服务的流程和服务本身之间没有直接通信。

为了实现一个简单的启动服务,你需要实现Service类,并装饰启动服务的ServiceAttribute属性,以便它可以包含在应用清单中:

[Service]
public class MyStartedService : Service
{ 
    public override IBinder OnBind(Intent intent)
    {
        return null;
    }
    public override StartCommandResult OnStartCommand(
        Intent intent, StartCommandFlags flags, int startId)
    {
        // DO Work, can reference common core modules
        return StartCommandResult.NotSticky;
    }
}

一旦创建了服务,您可以使用Intent启动服务,如下所示:

var intent = new Intent (this, typeof(MyStartedService));
StartService(intent);

您也可以使用AlarmManager定期发起服务。

服务实现的另一个选项是使用绑定服务。 与已启动的服务不同,它们通过使用活页夹保持开放的通信渠道。 绑定服务方法可以由初始化流程(如活动)调用。

iOS 背景

iOS 平台也提供了从远程服务器获取额外数据的后台机制,即使应用甚至设备处于非活动状态。 虽然它不像 Android 上的警报管理器那样可靠,但这些后台任务都经过了高度优化,以保存电池。 这些后台任务可以作为对某些系统事件的响应执行,比如地理位置更新或以特定的名义间隔执行。 我们在这里使用了名义上的这个词,因为后台任务执行的时间段是不确定的,并且可能根据后台任务的执行性能以及可用的系统资源随时间变化。

例如,为了执行后台获取,你需要在后台模式中启用后台获取:

Figure 11.2 –  iOS Modes

图 11.2 - iOS 后台模式

一旦Background``fetch被启用,我们就可以引入我们的取回机制,该机制将被定期执行。 这种获取机制通常会进行远程服务调用来更新要显示的数据,因此一旦应用进入前台,它就不需要重复这些刷新数据调用。 执行获取可以在AppDelegate中建立:

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init();
    LoadApplication(new App());

 UIApplication.SharedApplication 
.SetMinimumBackgroundFetchInterval(UIApplication.BackgroundFetchIntervalMinimum);
    return base.FinishedLaunching(app, options);
}

现在,iOS 运行时将定期调用PerformFetch方法,所以我们可以在这里注入我们的检索代码:

public override void PerformFetch(
    UIApplication application, Action<UIBackgroundFetchResult> 
    completionHandler)
{
    // TODO: Perform fetch
    // Return the correct status according to the fetch execution
    completionHandler(UIBackgroundFetchResult.NewData);
}

返回的结果状态很重要,因为运行时利用该结果来优化获取间隔。 结果状态可以是以下三种状态之一:

  • UIBackgroundFetchResult.NewData:当新内容被获取,应用被更新时调用。
  • UIBackgroundFetchResult.NoData:当获取新内容完成,但没有可用内容时调用。
  • UIBackgroundFetchResult.Failed:用于错误处理; 当获取无法完成时调用。

除了后台获取,NSUrlSession与后台传输基础设施结合,可以提供后台检索机制,这些机制可以合并到后台获取操作中。 通过这种方式,应用内容可以保持最新,即使它处于活动状态。

正如我们在这里所演示的,Android 和 iOS 平台都提供了自己的异步流机制,并且这些特性都可以在 Xamarin 平台上使用。 根据用例的不同,开发人员可以自由选择是使用特定的 TPL 模式实现还是本地子过程来处理异步执行。

总结

简而言之,移动应用不应该被设计为在用户交互层上执行长时间运行的任务,而应该使用异步机制来执行这些工作流。 在本例中,UI 只负责通知用户后台执行状态。 在过去,后台任务是通过经典的. net 线程模型来处理的,而现在,TAP 模型提供了一组丰富的功能,它将开发人员从创建、管理和同步线程和线程池的负担中解放出来。 在本章中,我们已经看到了可以帮助我们创建后台任务的各种模式。 然后这些结果将返回给 UI 线程,以便异步流程结果可以传播到 UI。 我们还讨论了同步机制和任务的不同策略,从而避免了死锁和竞争条件。 此外,我们还研究了 iOS 和 Android 的原生后台程序。

总的来说,异步任务和后台技术主要用于一个共同目标:在应用域中保持数据的最新。 在下一章中,我们将仔细研究有效管理应用数据的不同技术。