在 Windows 应用中处理后台任务

了解如何在应用中使用 Windows 运行时(WinRT)BackgroundTaskBuilder 类创建和注册后台任务。

注册后台任务

有关在通用 Windows 平台 (UWP) 应用中注册后台任务的完整示例,请参阅 BackgroundTask 示例

以下示例演示在重复的 15 分钟计时器上运行的 Win32 COM 任务的注册。

若要注册后台任务,必须先创建 BackgroundTaskBuilder 类的新实例。 BackgroundTaskBuilder 类用于在应用中创建和注册后台任务。 下面的代码示例演示如何创建 BackgroundTaskBuilder 类的新实例:

using System;
using Windows.ApplicationModel.Background;

public IBackgroundTaskRegistration RegisterBackgroundTaskWithSystem(IBackgroundTrigger trigger, Guid entryPointClsid, string taskName)
{
    BackgroundTaskBuilder builder = new BackgroundTaskBuilder();

    builder.SetTrigger(trigger);
    builder.SetTaskEntryPointClsid(entryPointClsid);

    BackgroundTaskRegistration registration;
    if (builder.Validate())
    {
        registration = builder.Register(taskName);
    }
    else
    {
        registration = null;
    }

    return registration;
}

RegisterBackgroundTaskWithSystem(new TimeTrigger(15, false), typeof(TimeTriggeredTask).GUID, typeof(TimeTriggeredTask).Name);

RegisterBackgroundTaskWithSystem 方法采用三个参数:

  • trigger:将启动后台任务的触发器。
  • entryPointClsid:后台任务入口点的类 ID。
  • taskName:后台任务的名称。

RegisterBackgroundTaskWithSystem 方法创建 BackgroundTaskBuilder 类的新实例,并为后台任务设置触发器和入口点类 ID。 然后,该方法向系统注册后台任务。

注意

这个类不是敏捷类,这意味着你需要考虑它的线程模型和封送行为。 有关详细信息,请参阅 线程和封送处理(C++/CX)在多线程环境(.NET)中使用 Windows 运行时对象。

在后台任务中管理现代待机模式

BackgroundTaskBuilder 和相关 API 已允许打包的桌面应用程序运行后台任务。 API 现在扩展了这些 API,使这些应用能够在新式待机中执行代码。 更新还会添加应用可以查询的属性,以确定系统是否会限制新式待机中应用程序的后台任务,以节省电池使用时间。 这使得应用程序能够在新式待机模式下接收 VoIP 呼叫或其他推送通知。

注意

本节中的“打包桌面应用程序”是指具有包标识的 Win32 应用程序(即桌面桥或稀疏签名的打包应用程序),并且以 main(或 wmain)函数作为其入口点。

以下示例演示应用开发人员如何使用 BackgroundTaskBuilder API 将最多一个任务注册到指定任务名称。 此示例还演示了如何检查并在应用程序的关键任务中选择在现代待机模式下启用任务登记。

// The following namespace is required for BackgroundTaskBuilder APIs. 
using Windows.ApplicationModel.Background; 

// The following namespace is required for API version checks. 
using Windows.Foundation.Metadata; 

// The following namespace is used for showing Toast Notifications. This 
// namespace requires the Microsoft.Toolkit.Uwp.Notifications NuGet package 
// version 7.0 or greater. 
using Microsoft.Toolkit.Uwp.Notifications; 

// Incoming calls are considered to be critical tasks to the operation of the app. 
const string IncomingCallTaskName = "IncomingCallTask"; 
const string NotificationTaskName = "NotificationTask"; 
const string PrefetchTaskName = "PrefetchTask"; 

public static bool IsAllowedInBackground(BackgroundAccessStatus status) { 
    return ((status != BackgroundAccessStatus.Denied) && 
            (status != BackgroundAccessStatus.DeniedBySystemPolicy) && 
            (status != BackgroundAccessStatus.DeniedByUser) && 
            (status != BackgroundAccessStatus.Unspecified)); 
} 

public async void RegisterTask(IBackgroundTrigger trigger, 
                               Guid entryPointClsid, 
                               string taskName, 
                               bool isRunInStandbyRequested) 
{ 
    var taskBuilder = new BackgroundTaskBuilder(); 
    taskBuilder.SetTrigger(trigger); 
    taskBuilder.SetTaskEntryPointClsid(entryPointClsid); 

    // Only the most critical background work should be allowed to proceed in 
    // modern standby. Additionally, some platforms may not support modern 
    // or running background tasks in modern standby at all. Only attempt to 
    // request modern standby execution if both are true. Requesting network 
    // is necessary when running in modern standby to handle push notifications. 
    if (IsRunInStandbyRequested && taskBuilder.IsRunningTaskInStandbySupported) 
    { 
        var accessStatus = BackgroundExecutionManager.GetAccessStatusForModernStandby(); 
        if (!IsAllowedInBackground(accessStatus) 
        { 
            await BackgroundExecutionManager.RequestAccessKindForModernStandby( 
                    BackgroundAccessRequestKind.AllowedSubjectToSystemPolicy, 
                    "This app wants to receive incoming notifications while your device is asleep"); 
        } 

        accessStatus = BackgroundExecutionManager.GetAccessStatusForModernStandby(); 

        if (IsAllowedInBackground(accessStatus) 
        { 
            taskBuilder.IsRunningTaskInStandbyRequested = true; 
            taskBuilder.IsNetworkRequested = true; 
        } 
    } 

    // Check that the registration is valid before attempting to register. 
    if (taskBuilder.IsRegistrationValid) 
    { 
        // If a task with the specified name already exists, it is unregistered 
        // before a new one is registered. Note this API may still fail from 
        // catastrophic failure (e.g., memory allocation failure). 
        taskBuilder.Register(taskName); 
    } 

    return; 
} 

RegisterTask(new PushNotificationTrigger(), "{INSERT-YOUR-GUID-HERE}", IncomingCallTaskName, true); 

检查后台任务是否已超出新式待机中的预算

以下示例代码演示应用开发人员如何使用 BackgroundWorkCost.WasApplicationThrottledInStandbyBackgroundWorkCost.ApplicationEnergyUseLevel 监视和响应其后台任务耗尽其应用预算。 应用开发人员可以通过减少新式待机中完成的优先级较低的工作来做出反应。 请注意,这依赖于上一示例中的代码。

public async void ReduceBackgroundCost() 
{ 
    BackgroundTaskRegistration callTask; 
    BackgroundTaskRegistration notificationTask; 
    BackgroundTaskRegistration prefetchTask; 

    // Nothing to do if the app was not or will not be throttled. 
    if (!BackgroundWorkCost.WasApplicationThrottledInStandby && 
        (BackgroundWorkCost.ApplicationEnergyUseLevel != StandbyEnergyUseLevel.OverBudget)) 
    { 
        return; 
    } 

    foreach (var task in BackgroundTaskRegistration.AllTasks) 
    { 
        switch (task.Value.Name) { 
        case IncomingCallTaskName: 
            callTask = task.Value; 
            break; 

        case NotificationTaskName: 
            notificationTask = task.Value; 
            break; 

        case PrefetchTaskName: 
            prefetchTask = task.Value; 
            break; 

        default: 
        } 
    } 

    if (callTask.WasTaskThrottledInStandby) 
    { 
        // Unset the throttle flag after acknowledging it so the app can 
        // react to the same task being throttled again in the future. 
        task.Value.WasTaskThrottledInStandby = false; 

        // Notify the user that the notification was missed. 
        new ToastContentBuilder() 
            .AddText("You missed a call") 
            .AddText(task.Value.Name) 
            .Show(); 

        // Because the incoming calls were not activated, demote less notifications 
        // tasks so the calls can be delivered promptly in the future. 
        RegisterTask(notificationTask.Value.Trigger, 
                     typeof(TimeTriggeredTask).GUID, 
                     notificationTask.Value.Name, 
                     false); 
    } 

    // Note that if incoming call tasks were throttled in some previous modern 
    // standby session, the application energy use was over budget for some period. 
    // Demote unimportant tasks like prefetch work to avoid calls and notifications 
    // from being throttled.
    if (callTask.WasTaskThrottledInStandby) ||
        (BackgroundWorkCost.ApplicationEnergyUseLevel == StandbyEnergyUseLevel.OverBudget))
    {
        RegisterTask(prefetchTask.Value.Trigger,
                     typeof(TimeTriggeredTask).GUID,
                     prefetchTask.Value.Name,
                     false);
    }

    return;
}

下面是 GitHub上的以下 C++WinRT/C# 示例代码的增量端到端更新。

该示例演示如何使用 BackgroundWorkCost.ApplicationEnergyUseTrend 监视后台任务如何耗尽其预算。 您还可以阻止最昂贵的后台任务在新式待机中运行,并在应用程序过快使用其预算时阻止后台任务在新式待机中运行。 此示例依赖于前面示例中的代码。

public async void ReduceBackgroundCostPreemptively() 
{ 
    BackgroundTaskRegistration mostExpensiveTask = null; 

    // We can't do anything preemptively since the trend isn't known. 
    if (!BackgroundWorkCost.IsApplicationEnergyUseTrendKnown) 
    { 
        return; 
    } 

    // The app is not trending towards being over budget, so this method can 
    // return early. 
    if ((BackgroundWorkCost.ApplicationEnergyUseTrend != EnergyUseTrend.OverBudget) && 
        (BackgroundWorkCost.ApplicationEnergyUseTrend != EnergyUseTrend.OverHalf)) 
    { 
        return; 
    } 

    // The application is going exceeding its budget very quickly. Demote the 
    // most expensive task that is not the call task before call tasks start being 
    // throttled. 
    if (BackgroundWorkCost.ApplicationEnergyUseTrend == EnergyUseTrend.OverBudget) 
    { 
        foreach (var task in BackgroundTaskRegistration.AllTasks) 
        { 
            if ((task.Value.Name != IncomingCallTaskName) && 
                ((mostExpensiveTask == null) || 
                 (mostExpensiveTask.ApplicationEnergyUseTrendContributionPercentage < 
                  task.Value.ApplicationEnergyUseTrendContributionPercentage))) 
            { 
                mostExpensiveTask = task.Value; 
            } 
        } 
    } 

    if (mostExpensiveTask != null) 
    { 
        RegisterTask(mostExpensiveTask.Trigger, 
                     typeof(TimeTriggeredTask).GUID, 
                     mostExpensiveTask.Name, 
                     false); 
    } 

    // The application is trending toward eventually exceeding its budget. Demote the 
    // least important prefetch task before calls and notifications are throttled. 
    foreach (var task in BackgroundTaskRegistration.AllTasks) 
    { 
        if (task.Value.Name == PrefetchTaskName) { 
            RegisterTask(task.Value.Trigger, 
                         typeof(TimeTriggeredTask).GUID, 
                         task.Value.Name, 
                         false); 
        } 
    } 

    return; 
} 

后台任务和网络连接

如果后台任务需要网络连接,请注意以下注意事项。

  • 使用 SocketActivityTrigger 在收到数据包时激活后台任务,并执行一次性任务。 执行任务后,后台任务应终止以节省电源。
  • 使用 ControlChannelTrigger 在收到数据包时激活后台任务,并且需要执行长期任务。
  • 将 InternetAvailable 条件(BackgroundTaskBuilder.AddCondition)添加到后台任务,以延迟触发后台任务,直到网络堆栈正在运行。 此条件可节省电源,因为在网络访问可用之前,后台任务不会执行。 此条件不提供实时激活。
  • 无论使用哪种触发器,在后台任务上设置 IsNetworkRequested,以确保后台任务运行时网络保持正常运行。 这会告知后台任务基础结构在执行任务时保持网络状态,即使设备已进入连接待机模式也是如此。 如果后台任务不使用 IsNetworkRequested,则当处于连接待机模式时,后台任务将无法访问网络。