轻轨迹鸿蒙开发实战2:接管 backgroundTaskManager,长时后台定位与锁屏不间断追踪

背景介绍

在开发外勤考勤或运动轨迹类应用时,开发者面临的最大挑战往往不是“如何获取经纬度”,而是“怎么让定位在后台持续工作”。

对于我们的防作弊产品“轻轨迹”来说,员工在开始作业后,必然会把手机揣进兜里并锁屏。如果应用退到后台的几分钟内,定位服务就被系统无情掐断,那么画出来的轨迹就会出现大面积的“高空折线”甚至是空白,这就完全丧失了考核的真实性。

在 HarmonyOS NEXT 中,系统为了极致省电和用户隐私,建立了一套严丝合缝的后台冻结与生命周期托管机制。任何处于后台的普通应用进程,都会在几分钟内被系统强行转入挂起(Suspended)状态,其各种底层监听和异步回调(包括定位、网络、计时器)都会被全面冷冻。

作为程序员,想要冲破这层防线,唯一的合规途径就是——接管系统的长时任务(Continuous Task)管理器。本文将揭示鸿蒙的后台生存法则,带你通过实战代码打通锁屏常驻的后台高精度定位生命线。
image.png

1. 鸿蒙后台生存法则:短时任务 vs 长时任务

在鸿蒙系统资源调度框架中,应用进入后台后的待遇可划分为三种生存模式:

  1. 挂起与冻结(Suspended):默认模式。当应用退后台后,系统不会立刻杀死它,但会快速暂缓或停止其所有 CPU 调度。
  2. 短时任务(Transient Task):如果应用退后台时有未完成的工作(如正在写本地数据库),可向系统申请最长几分钟的延迟挂起。这对我们持续几小时的外勤考核来说无济于事。
  3. 长时任务(Continuous Task):如果应用在后台需要持续提供用户可感知的服务(如导航、音频播放、位置追踪),可以向系统申请后台长时任务。申请成功后,系统会为应用建立一条持续保活的通道,并强制在状态栏常驻一条小图标或胶囊,让用户明确知晓。
    后台任务选择流程:
    image.png

对于“轻轨迹”而言,我们必须申请 BackgroundMode.LOCATION 长时任务。

避坑指南

长时任务并不是开发者在代码里“悄悄”调个 API 就能实现的。它必须同时满足两个硬性前置条件:

  1. 必须在 module.json5 配置文件中声明后台运行模式(backgroundModes)。
  2. 在后台运行期间,必须向用户展示一个持续更新的前台通知(通过 WantAgent 托管),使用户随时能够点击通知回到应用。任何试图隐藏通知或滥用其他模式(如伪装成音频播放来蹭保活)的行为,在应用上架审核时都会被直接驳回。
2. module.json5 配置与权限声明

在开启代码编写前,我们先要在 entry/src/main/module.json5 中宣告我们要接管后台位置能力:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        // 【核心机制】:配置后台运行模式,声明为位置持续定位模式
        "backgroundModes": [
          "location"
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      // 系统前台高精度定位权限
      {
        "name": "ohos.permission.LOCATION"
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION"
      },
      // 【避坑指南】:在后台持续接收定位,必须额外声明此权限,否则退后台即报权限缺失
      {
        "name": "ohos.permission.LOCATION_IN_BACKGROUND"
      },
      // 申请后台运行长时任务的必备权限
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
      }
    ]
  }
}
3. 封装后台保活工具类:WantAgent 托管常驻通知栏

长时任务的启动需要使用到 backgroundTaskManager,并且需要传入一个 WantAgent 对象。

WantAgent 是鸿蒙提供的一种“代理意图”。因为我们的长时任务通知栏是由系统系统级进程渲染的,当用户点击这个通知时,系统进程需要知道应该拉起哪一个应用、进入哪一个页面。这就需要我们将拉起 EntryAbility 的意图封装进 WantAgent 中,托管给系统。

我们来封装 BackgroundTaskHelper.ets 工具类:

import backgroundTaskManager from '@ohos.backgroundTaskManager';
import wantAgent, { WantAgent } from '@ohos.app.ability.wantAgent';

export class BackgroundTaskHelper {
  private static context: Context | null = null;

  // 在 EntryAbility 初始化时,将 ApplicationContext 注入进来
  public static setContext(ctx: Context) {
    BackgroundTaskHelper.context = ctx;
  }

  // 开启长时后台任务保活
  public static async startContinuousTask(): Promise<void> {
    let ctx = BackgroundTaskHelper.context;
    if (!ctx) {
      console.error("BackgroundTaskHelper", "Context is null, cannot start continuous task");
      return;
    }

    try {
      // 1. 构建 WantAgentInfo,设定用户点击通知栏后的跳转意图
      const wantAgentInfo: wantAgent.WantAgentInfo = {
        wants: [
          {
            bundleName: "com.qingkouwei.trajectory", // 我们的应用包名
            abilityName: "EntryAbility"              // 跳转的目标 Ability
          }
        ],
        actionType: wantAgent.OperationType.START_ABILITY, // 触发动作:拉起页面
        requestCode: 0,
        wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] // 标志位:更新已存在的通知
      };

      // 2. 向系统申请构建代理 WantAgent 对象
      const wa: WantAgent = await wantAgent.getWantAgent(wantAgentInfo);
      
      // 3. 核心调用:申请后台位置任务,并绑定通知栏托管
      await backgroundTaskManager.startBackgroundRunning(ctx, 
        backgroundTaskManager.BackgroundMode.LOCATION, 
        wa);
        
      console.info("BackgroundTaskHelper", `Continuous task (LOCATION) started successfully.`);
    } catch (e) {
      // 避坑指南:如果配置没有声明 backgroundModes,此 API 将直接抛出错误
      console.error("BackgroundTaskHelper", `Failed to start continuous task. ErrorCode: ${e.code}, Message: ${e.message}`);
    }
  }

  // 结束作业时,主动释放长时后台占位
  public static async stopContinuousTask(): Promise<void> {
    let ctx = BackgroundTaskHelper.context;
    if (!ctx) return;
    try {
      await backgroundTaskManager.stopBackgroundRunning(ctx);
      console.info("BackgroundTaskHelper", "Continuous task stopped, process returned to normal state");
    } catch (e) {
      console.error("BackgroundTaskHelper", `Failed to stop continuous task: ${e.code} ${e.message}`);
    }
  }
}
4. 业务总控对接:联动开始/结束作业

有了这个辅助工具后,我们的业务总控单例 TrajectoryManager.ets 便可以在开启定位作业的同时,无缝拉起后台常驻盾牌:

import { LocationHelper } from './LocationHelper';
import { BackgroundTaskHelper } from './BackgroundTaskHelper';
import { IMUFloorEstimator } from './IMUFloorEstimator';

export class TrajectoryManager {
  private static instance: TrajectoryManager | null = null;
  private isTracking = false;

  public static getInstance(): TrajectoryManager {
    if (!TrajectoryManager.instance) {
      TrajectoryManager.instance = new TrajectoryManager();
    }
    return TrajectoryManager.instance;
  }

  // 开始带看作业
  public async startTracking() {
    if (this.isTracking) return;
    this.isTracking = true;

    // 1. 一键激活后台长时任务,拉起盾牌通知栏,防止进程被系统打入 Suspended
    await BackgroundTaskHelper.startContinuousTask();

    // 2. 开启 IMU 惯性传感器与计步估算
    IMUFloorEstimator.getInstance().start();

    // 3. 注册持续高精度定位回调
    LocationHelper.startLocationUpdates((location) => {
      this.handleLocationUpdate(location);
    });
  }

  // 结束带看作业
  public async stopTracking() {
    if (!this.isTracking) return;
    this.isTracking = false;

    // 停止定位与传感器
    LocationHelper.stopLocationUpdates();
    IMUFloorEstimator.getInstance().stop();

    // 释放后台常驻,归还系统能耗,通知栏小图标自动消失
    await BackgroundTaskHelper.stopContinuousTask();
  }
}
5. 总结与下期预告

通过调用系统的 backgroundTaskManager,我们的“轻轨迹”拿到了退居后台和锁屏状态下的“免挂起金牌”。当应用退到后台时,用户会清晰地看到状态栏常驻的绿色定位小胶囊,点击它便可一键安全回到轻轨迹主页。

后台保活的生命线被打通后,下一个挑战是:如何优雅、流畅地在地图上将这些经纬度坐标绘制出来?当多个轨迹点高频到达时,如何避免界面闪烁与卡死?

在下一篇文章中,我们将踏入地图渲染的核心领域:Map Kit 深度实践,轻轨迹实时路网绘制与起点终点自动捕获! 敬请期待。


轻口味
39.5k 声望5.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei