• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 合理运行后台任务
2
3## 简介
4
5设备返回主界面、锁屏、应用切换等操作会使应用退至后台。为了降低设备耗电速度、保障用户使用流畅度,系统会对退至后台的应用进行管控,包括进程挂起和进程终止。为了保障后台音乐播放、日历提醒等功能的正常使用,系统提供了受规范约束的后台任务,扩展应用在后台的运行时间。
6本文将介绍各类后台任务的基本概念和适用场景,并且通过对短时任务和长时任务两个场景的性能分析说明合理运行后台任务的必要性。
7
8## 短时任务
9
10应用退至后台一小段时间后,应用进程会被挂起,无法执行对应的任务。如果应用在后台仍需要执行耗时不长的任务,可以申请短时任务,扩展应用在后台的运行时间。
11
12短时任务适用于小文件下载、缓存、信息发送等实时性高、需要临时占用资源执行的任务。详细的开发指导可参考[短时任务](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/task-management/transient-task.md/)13
14### 场景示例
15
16下面代码在申请短时任务后执行了一个耗时计算任务。源代码可访问[短时任务示例程序](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/feature/backgroundTask/src/main/ets/view/TransientTaskView.ets)获取。
17
18```javascript
19import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
20import { BusinessError } from '@ohos.base';
21import util from '@ohos.util';
22import hiTraceMeter from '@ohos.hiTraceMeter';
23
24const totalTimes: number = 50000000; // 循环次数
25const calculateResult: string = 'Total time costed = %s ms.'; // 文本格式
26
27@Entry
28@Component
29struct Index {
30  @State message: string = 'Click button to calculate.';
31  private requestId: number = 0;
32
33  // 申请短时任务
34  requestSuspendDelay() {
35    try {
36      let delayInfo = backgroundTaskManager.requestSuspendDelay('compute', () => {
37        console.info('Request suspension delay will time out.');
38        // 任务即将超时,取消短时任务
39        this.cancelSuspendDelay();
40      })
41      this.requestId = delayInfo.requestId;
42    } catch (error) {
43      console.error(`requestSuspendDelay failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
44    }
45  }
46
47  // 取消短时任务
48  cancelSuspendDelay() {
49    backgroundTaskManager.cancelSuspendDelay(this.requestId);
50    console.info('Request suspension delay cancel.');
51  }
52
53  // 计算任务
54  computeTask(times: number): number {
55    let start: number = new Date().getTime();
56    let a: number = 1;
57    let b: number = 1;
58    let c: number = 1;
59    for (let i: number = 0; i < times; i++) {
60      a = a * Math.random() + b * Math.random() + c * Math.random();
61      b = a * Math.random() + b * Math.random() + c * Math.random();
62      c = a * Math.random() + b * Math.random() + c * Math.random();
63    }
64    let end: number = new Date().getTime();
65    return end - start;
66  }
67
68  // 点击回调
69  clickCallback = () => {
70    this.requestSuspendDelay();
71    hiTraceMeter.startTrace('computeTask', 0); // 开启性能打点
72    let timeCost = this.computeTask(totalTimes);
73    this.message = util.format(calculateResult, timeCost.toString());
74    hiTraceMeter.finishTrace('computeTask', 0); // 结束性能打点
75    this.cancelSuspendDelay();
76  }
77
78  build() {
79    Column() {
80      Row(){
81        Text(this.message)
82      }
83      Row() {
84        Button('开始计算')
85          .onClick(this.clickCallback)
86      }
87      .width('100%')
88      .justifyContent(FlexAlign.Center)
89    }
90    .width('100%')
91    .height('100%')
92    .justifyContent(FlexAlign.Center)
93  }
94}
95```
96
97使用 IDE 中的 Time Profiler 获取示例应用从开始计算任务并退到后台执行一分钟内的性能数据。获取到的数据如下图。
98
99图1 短时任务 Time Profiler 泳道图
100
101![](./figures/reasonable-running-backgroundTask-image1.png)
102
103- ArkTS Callstack:基于时间轴展示 CPU 占用率和状态的变化。
104- User Trace:基于时间轴展示当前时段内触发用户自定义打点任务的具体情况。H:computeTask 表示短时任务执行用时。
105- Native Callstack:基于时间轴展示 CPU 占用率变化和进程/线程的活动状态以及函数调用栈。
106
107从上图中可以看出,Native Callstack 泳道与 H:computeTask 相对应的时间段内应用进程处于活跃状态,CPU 占用率在较高范围内变化。任务取消后,应用仍然处于运行状态,但是进程的活跃程度和 CPU 占用率都明显下降,直到在几秒后系统将应用挂起,不再占用 CPU。
108
109分别框选任务执行阶段和任务取消后未被挂起阶段对应的 Native Callstack 如下图,查看应用主线程在两个阶段的平均 CPU 占用率和最高 CPU 占用率情况。
110
111图2 任务执行阶段的 CPU 占用率
112
113![](./figures/reasonable-running-backgroundTask-image2.png)
114
115图3 任务取消后未被挂起阶段的 CPU 占用率
116
117![](./figures/reasonable-running-backgroundTask-image3.png)
118
119可以看到应用主线程在任务执行阶段的平均 CPU 占用率为 12.6%,最高 CPU 占用率为 40.0%,在任务取消后未被挂起阶段的平均 CPU 占用率为 2.2%,最高 CPU 占用率为 28.6%。
120
121在后台运行短时任务,会占用系统 CPU,在后台执行过多的短时任务就有可能会导致前台的应用卡顿,因此建议非必要情况不使用短时任务,使用时也避免同时申请过多的短时任务。
122
123更多短时任务的使用限制和注意事项可以参考[短时任务约束与限制](../task-management/transient-task.md)。
124
125
126## 长时任务
127
128应用退至后台后,在后台需要长时间运行用户可感知的任务,如播放音乐、导航等。为防止应用进程被挂起,导致对应功能异常,可以申请长时任务,使应用在后台长时间运行。申请长时任务后,系统会做相应的校验,确保应用在执行相应的长时任务。详细的开发指导可参考[长时任务](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/task-management/continuous-task.md/)129
130长时任务支持的类型包括数据传输、音视频播放、录音、定位导航、蓝牙相关、多设备互联、WLAN 相关、音视频通话、计算任务。可以根据下表的场景举例选择相应的长时任务类型。
131
132| 描述                           | 场景举例                             |
133| ------------------------------ | ------------------------------------ |
134| 数据传输                       | 后台下载大文件,如浏览器后台下载等。 |
135| 音视频播放                     | 音乐类应用在后台播放音乐。           |
136| 录音                           | 录音机在后台录音。                   |
137| 定位导航                       | 导航类应用后台导航。                 |
138| 蓝牙相关                       | 通过蓝牙传输分享的文件。             |
139| 多设备互联                     | 分布式业务连接。                     |
140| WLAN 相关(仅对系统应用开放)  | 通过 WLAN 传输分享的文件。           |
141| 音视频通话(仅对系统应用开放) | 系统聊天类应用后台音频电话。         |
142| 计算任务(仅对特定设备开放)   | 杀毒软件                             |
143
144- 申请了数据传输的长时任务,系统仅会提升应用进程的优先级,降低系统终止应用进程的概率,但仍然会挂起对应的应用进程。对于上传下载对应的功能,需要调用系统[上传下载代理接口](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/apis/js-apis-request.md/)托管给系统执行,可以参考[文件上传下载性能提升指导](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/performance/improve-file-upload-and-download-performance.md/)145- 申请音视频播放长时任务必须使用[媒体会话服务](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/media/avsession-overview.md/),否则无法在后台播放。
146- 申请录音类型长时任务,需要有显著的用户提示,必须通过动态授权弹框来提供用户授权界面,请求用户授权麦克风权限。
147
148### 场景示例
149
150下面模拟一个后台定位的场景。应用订阅设备位置变化,每隔一秒获取位置信息,为了保证应用在退到后台后仍然可以使用定位服务,申请了定位类型的长时任务。源代码可访问[长时任务示例程序](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/feature/backgroundTask/src/main/ets/view/LongTermTaskView.ets)获取。
151
152首先需要在 [module.json5](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/product/phone/entry/src/main/module.json5) 配置文件中为需要使用长时任务的 EntryAbility 声明任务类型。
153
154```javascript
155{
156  "module": {
157    // ...
158    "abilities": [
159      {
160        "name": "EntryAbility",
161        // 后台模式类型
162        "backgroundModes": [
163          "location"
164        ]
165      }
166    ],
167  }
168}
169```
170
171需要使用到的相关权限如下:
172
173- ohos.permission.INTERNET
174- ohos.permission.LOCATION_IN_BACKGROUND
175- ohos.permission.APPROXIMATELY_LOCATION
176- ohos.permission.LOCATION
177- ohos.permission.KEEP_BACKGROUND_RUNNING
178
179权限申请方式参考[配置文件权限申明](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/security/accesstoken-guidelines.md/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%9D%83%E9%99%90%E5%A3%B0%E6%98%8E),在 [module.json5](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/product/phone/entry/src/main/module.json5) 中进行配置。其中部分权限申请以及打开使能通知开关需要用户手动确认。系统为申请的长时任务发布通知栏消息时,应用的使能通知开关必须处于开启状态,否则用户无法感知后台正在运行的长时任务。
180
181后台定位的实现代码如下:
182
183```javascript
184import wantAgent, { WantAgent } from '@ohos.app.ability.wantAgent';
185import common from '@ohos.app.ability.common';
186import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
187import { BusinessError } from '@ohos.base';
188import geolocation from '@ohos.geoLocationManager';
189import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
190import notificationManager from '@ohos.notificationManager';
191
192const TAG: string = 'BackgroundLocation';
193
194@Entry
195@Component
196export struct LongTermTaskView {
197  @State latitude: number = 0;
198  @State longitude: number = 0;
199
200  aboutToAppear() {
201    // 请求发送通知的许可
202    notificationManager.requestEnableNotification().then(() => {
203      console.info(`[EntryAbility] requestEnableNotification success`);
204      // 申请定位相关权限
205      let atManager = abilityAccessCtrl.createAtManager();
206      try {
207        atManager.requestPermissionsFromUser(getContext(this), ['ohos.permission.INTERNET',
208          'ohos.permission.LOCATION',
209          'ohos.permission.LOCATION_IN_BACKGROUND',
210          'ohos.permission.APPROXIMATELY_LOCATION'])
211          .then((data) => {
212            console.info(`[EntryAbility], data: ${JSON.stringify(data)}`);
213          })
214          .catch((err: BusinessError) => {
215            console.info(`[EntryAbility], err: ${JSON.stringify(err)}`);
216          })
217      } catch (err) {
218        console.info(`[EntryAbility], catch err->${JSON.stringify(err)}`);
219      }
220    }).catch((err: BusinessError) => {
221      console.error(`[EntryAbility] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
222    });
223  }
224
225  // 位置变化回调
226  locationChange = async (location: geolocation.Location) => {
227    console.info(TAG, `locationChange location =${JSON.stringify(location)}`);
228    this.latitude = location.latitude;
229    this.longitude = location.longitude;
230  }
231
232  // 获取定位
233  async getLocation() {
234    console.info(TAG, `enter getLocation`);
235    let requestInfo: geolocation.LocationRequest = {
236      priority: geolocation.LocationRequestPriority.FIRST_FIX, // 快速获取位置优先
237      scenario: geolocation.LocationRequestScenario.UNSET, // 未设置场景信息
238      timeInterval: 1, // 上报位置信息的时间间隔
239      distanceInterval: 0, // 上报位置信息的距离间隔
240      maxAccuracy: 100 // 精度信息
241    };
242    console.info(TAG, `on locationChange before`);
243    geolocation.on('locationChange', requestInfo, this.locationChange);
244    console.info(TAG, `on locationChange end`);
245  }
246
247  // 开始长时任务
248  startContinuousTask() {
249    let context: Context = getContext(this);
250    // 通知参数,指定点击长时任务通知后跳转的应用
251    let wantAgentInfo: wantAgent.WantAgentInfo = {
252      wants: [
253        {
254          bundleName: (context as common.UIAbilityContext).abilityInfo.bundleName,
255          abilityName: (context as common.UIAbilityContext).abilityInfo.name
256        }
257      ],
258      operationType: wantAgent.OperationType.START_ABILITY,
259      requestCode: 0,
260      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
261    };
262    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
263      backgroundTaskManager.startBackgroundRunning(context,
264        backgroundTaskManager.BackgroundMode.LOCATION, wantAgentObj).then(() => {
265        console.info(`Succeeded in operationing startBackgroundRunning.`);
266      }).catch((err: BusinessError) => {
267        console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
268      });
269    });
270  }
271
272  // 停止长时任务
273  stopContinuousTask() {
274    backgroundTaskManager.stopBackgroundRunning(getContext()).then(() => {
275      console.info(`Succeeded in operationing stopBackgroundRunning.`);
276    }).catch((err: BusinessError) => {
277      console.error(`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
278    });
279  }
280
281  build() {
282    Column() {
283      Column() {
284        Text(this.latitude.toString())
285        Text(this.longitude.toString())
286      }
287      .width('100%')
288
289      Column() {
290        Button('开启定位服务')
291          .onClick(() => {
292            this.startContinuousTask();
293            this.getLocation();
294          })
295        Button('关闭定位服务')
296          .onClick(async () => {
297            await geolocation.off('locationChange');
298            this.stopContinuousTask();
299          })
300          .margin({ top: 10 })
301      }
302      .width('100%')
303    }
304    .width('100%')
305    .height('100%')
306    .justifyContent(FlexAlign.Center)
307  }
308}
309```
310
311基于上述场景,使用功耗测试工具获取 30min 设备功耗,得到的数据如下表。
312
313| 测试任务             | 测试时长(s) | 归一电流(mA) | 最大电流(mA) | 归一耗电(mAH) |
314| -------------------- | ----------- | ------------ | ------------ | ------------- |
315| 后台存在定位长时任务 | 1836.4      | 15.52        | 995.72       | 7.92          |
316| 后台无任务           | 1839.9      | 0.85         | 404.03       | 0.44          |
317
318- 归一电流:电压处于 3.8V 时的电流平均值。
319- 归一耗电的计算方式为:归一电流\*测试时长/3600。3600 表示一小时的秒数。
320
321对比后台存在长时定位任务和不存在长时任务时的功耗数据,当后台存在定位任务持续运行时,设备在 30 分钟内的功耗明显增加。
322
323从功耗角度考虑,应用应该避免过多使用长时任务,针对必须使用长时任务的场景,也可以优化任务执行过程,减少设备功耗。以下是一些优化建议:
324
325- 对定位要求不太高的场景可以适当调整上报时间间隔和上报距离间隔,减少更新频率。
326
327- 尽可能的减少网络请求次数和减小网络请求时间间隔。
328
329- 数据传输中使用高效率的数据格式和解析方法,减少任务执行时间。
330
331更多长时任务的使用限制和注意事项可以参考[长时任务约束与限制](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/task-management/continuous-task.md/)332
333## 延迟任务
334
335应用退至后台后,如果需要执行实时性要求不高的任务,可以使用延迟任务。当应用满足设定条件(包括网络类型、充电类型、存储状态、电池状态、定时状态等)时,将任务添加到执行队列,系统会根据内存、功耗、设备温度、用户使用习惯等统一调度拉起应用。
336
337延迟任务适用于软件更新、信息收集、数据处理等场景。详细的开发指导可参考[延迟任务](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/task-management/work-scheduler.md/)338
339## 代理提醒
340
341应用退到后台或进程终止后,仍然有一些提醒用户的定时类任务,例如购物类应用抢购提醒等,为满足此类功能场景,系统提供了代理提醒(reminderAgentManager)的能力。当应用退至后台或进程终止后,系统会代理应用做相应的提醒。详细的开发指导可参考[代理提醒](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/task-management/agent-powered-reminder.md/)342
343当前支持的提醒类型包括:
344
345- 倒计时:基于倒计时的提醒功能,适用于短时的计时提醒场景,例如抢购倒计时。
346- 日历:基于日历的提醒功能,适用于较长时间的日程提醒场景,例如生日提醒。
347- 闹钟:基于时钟的提醒功能,适用于闹钟相关场景,例如起床闹钟。
348
349## 总结
350
351合理的选择和使用后台任务对于优化用户体验,减少性能消耗非常重要。以下表格对比总结了各类后台任务的概念、适用场景以及任务执行过程中的应用状态。
352
353| 任务类型 | 概念                                                                                     | 应用退至后台状态                                                                                                                                                    | 适用场景                                                                              |
354| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
355| 无后台   | 不执行任何任务,直接退到后台                                                             | 一小段时间后应用挂起(几秒内)                                                                                                                                      | 通用                                                                                  |
356| 短时任务 | 实时性要求高、耗时不长的任务                                                             | 在单次配额内,应用不会被挂起直到取消任务;单次配额超时不取消,应用进程会被终止                                                                                      | 小文件下载、缓存、信息发送等时效性高、需要临时占用资源执行的任务                      |
357| 长时任务 | 长时间运行在后台、用户可感知的任务                                                       | 应用不会被挂起直到取消任务,任务结束不取消应用进程会被终止                                                                                                          | 数据传输、音频播放、录音、定位导航、蓝牙、WLAN 相关、多设备互联、音视频通话、计算任务 |
358| 延迟任务 | 实时性要求不高、可延迟执行的任务,满足条件后放入执行队列,系统会根据内存、功耗等统一调度 | 应用退到后台时挂起,满足任务设定条件时由系统统一调度拉起应用,创建 Extension 进程执行任务;单次回调最长运行 2 分钟,如果超时不取消,系统会终止对应的 Extension 进程 | 软件更新、信息收集、数据处理等                                                        |
359| 代理提醒 | 系统代理应用做出相应提醒                                                                 | 应用挂起或进程终止,满足条件后系统会代理应用做相应的提醒                                                                                                            | 闹钟、倒计时、日历                                                                    |
360
361<!--no_check-->