【HarmonyOS】鸿蒙应用使用后台任务之长时任务,解决屏幕录制音乐播放等操作不被挂起

一、前言

1.后台是什么? 了解后台任务和长时任务前,我们需要先明白鸿蒙的后台特性:所谓的后台,指的是设备返回主界面、锁屏、应用切换等操作会使应用退至后台这个状态。

2.鸿蒙系统为什么这么做? 当应用退至后台后,如果继续活动,可能会造成设备耗电快、用户界面卡顿等现象。鸿蒙系统为了降低设备耗电速度、保障用户使用流畅度,系统会对退至后台的应用进行管控,包括进程挂起和进程终止。

3.会有什么问题? 当系统将应用挂起后,应用进程无法使用软件资源(如公共事件、定时器等)和硬件资源(CPU、网络、GPS、蓝牙等)。

综上所述,所以才会有标题存在的问题和对应的解决方案。 当我们应用正在使用蓝牙扫描 或者 音乐播放 或者 屏幕录制等类似的操作时,只要应用退到了后台超过三秒,就会被系统挂起,强制暂停。影响我们的逻辑业务。 所以这种情况下,鸿蒙提供了后台任务来解决。

二、后台任务是什么

后台任务是鸿蒙系统提供给有在后台,做业务操作不想被挂起需求的应用,提供的一套解决方案。

根据应用业务类型不同,也分为不同的后台任务:

【HarmonyOS】鸿蒙应用使用后台任务之长时任务,解决屏幕录制音乐播放等操作不被挂起_进程

根据我们的常规使用场景,例如屏幕录制举例,就需要使用长时任务来解决应用被挂起的问题。

三、长时任务的使用:

1.首先我们需要根据自己的业务类型,选择对应的长时任务类型:

【HarmonyOS】鸿蒙应用使用后台任务之长时任务,解决屏幕录制音乐播放等操作不被挂起_长时任务_02

我们以屏幕录制举例,选择AUDIO_RECORDING

2.在module.json5配置后台任务权限和长时任务能力类型:

"abilities": [
      {
        "backgroundModes": ["audioRecording"],
		}
// 申请长时任务
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      },

3.在录屏开启前,调用开启长时任务,退出录屏后取消长时任务。【开启和取消的两个调用时机需要注意,相当于长时任务的生命周期,是包裹住整个后台业务的生命周期。】

开启长时任务

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit';
  /**
   * 开启长时任务
   */
  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      // 添加需要被拉起应用的bundleName和abilityName
      wants: [
        {
          bundleName: "com.test.basedemo",
          abilityName: "EntryAbility"
        }
      ],
      // 指定点击通知栏消息后的动作是拉起ability
      actionType: wantAgent.OperationType.START_ABILITY,
      // 使用者自定义的一个私有值
      requestCode: 0,
      // 点击通知后,动作执行属性
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    try {
      // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
      wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
          try {
            backgroundTaskManager.startBackgroundRunning(getContext(),
              backgroundTaskManager.BackgroundMode.AUDIO_RECORDING, wantAgentObj, (error: BusinessError)=>{
                if (error) {
                  console.error(this.TAG,`Operation startBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
                } else {
                  console.info(this.TAG,"Operation startBackgroundRunning succeeded");
                  promptAction.showToast({
                    message: "开启长时任务成功!"
                  });
                  // // 此处执行具体的长时任务逻辑,如录音,录制等。
                  // this.startRecording();
                }
              })
          } catch (error) {
            console.error(this.TAG,`Operation startBackgroundRunning failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
          }
      });
    } catch (error) {
      console.error(this.TAG, `Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }

关闭长时任务

/**
   * 暂停长时任务
   */
  stopContinuousTask() {
    backgroundTaskManager.stopBackgroundRunning(getContext()).then(() => {
      console.info(this.TAG, `Succeeded in operationing stopBackgroundRunning.`);
      promptAction.showToast({
        message: "取消长时任务!"
      });
    }).catch((err: BusinessError) => {
      console.error(this.TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
    });
  }

源码示例:

【HarmonyOS】鸿蒙应用使用后台任务之长时任务,解决屏幕录制音乐播放等操作不被挂起_屏幕录制_03

module.json5

"abilities": [
      {
        "backgroundModes": ["audioRecording"],
		}
// 申请长时任务
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      },
      // 申请麦克风
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      },

BackTaskTestPage.ets

import media from '@ohos.multimedia.media';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct BackTaskTestPage {

  private TAG: string = "BackTaskTestPage";

  // 录屏沙箱文件
  private mFile: fs.File | null = null;

  aboutToAppear(): void {
    // 为了录屏可以采集麦克风,需要申请麦克风权限
    const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    try {
      atManager.requestPermissionsFromUser(getContext(), ["ohos.permission.MICROPHONE"]).then((data) => {
        if (data.authResults[0] === 0) {

        } else {
          console.log(this.TAG, "user rejected")
        }
      }).catch((err: BusinessError) => {
        console.log(this.TAG, "BusinessError err: " + JSON.stringify(err))
      })
    } catch (err) {
      console.log(this.TAG, "catch err: " + JSON.stringify(err))
    }

    // 创建录制视频的沙箱文件地址
    let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
    let pathDir: string = context.filesDir; // /data/storage/el2/base/haps/entry/files
    let filePath: string = pathDir + '/testBG.mp4';
    // 若文件不存在,则创建文件。
    let fileTarget = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    console.info(this.TAG, "file done: " + JSON.stringify(fileTarget.fd));
    this.mFile = fileTarget;

  }

  /**
   * 开启长时任务
   */
  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      // 添加需要被拉起应用的bundleName和abilityName
      wants: [
        {
          bundleName: "com.test.basedemo",
          abilityName: "EntryAbility"
        }
      ],
      // 指定点击通知栏消息后的动作是拉起ability
      actionType: wantAgent.OperationType.START_ABILITY,
      // 使用者自定义的一个私有值
      requestCode: 0,
      // 点击通知后,动作执行属性
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    try {
      // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
      wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
          try {
            backgroundTaskManager.startBackgroundRunning(getContext(),
              backgroundTaskManager.BackgroundMode.AUDIO_RECORDING, wantAgentObj, (error: BusinessError)=>{
                if (error) {
                  console.error(this.TAG,`Operation startBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
                } else {
                  console.info(this.TAG,"Operation startBackgroundRunning succeeded");
                  promptAction.showToast({
                    message: "开启长时任务成功!"
                  });
                  // // 此处执行具体的长时任务逻辑,如录音,录制等。
                  // this.startRecording();
                }
              })
          } catch (error) {
            console.error(this.TAG,`Operation startBackgroundRunning failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
          }
      });
    } catch (error) {
      console.error(this.TAG, `Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }

  /**
   * 暂停长时任务
   */
  stopContinuousTask() {
    backgroundTaskManager.stopBackgroundRunning(getContext()).then(() => {
      console.info(this.TAG, `Succeeded in operationing stopBackgroundRunning.`);
      promptAction.showToast({
        message: "取消长时任务!"
      });
    }).catch((err: BusinessError) => {
      console.error(this.TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
    });
  }

  build() {
    Row() {
      Column() {
        Button() {
          Text('申请长时任务').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 通过按钮申请长时任务
          this.startContinuousTask();
        })

        Button() {
          Text('取消长时任务').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 通过按钮取消长时任务
          this.stopContinuousTask();
        })

        Button() {
          Text('开始录制').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(async () => {
          this.startRecording();
        })

        Button() {
          Text('暂停录制').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          this.stopRecording();
        })
      }
      .width('100%')
    }
    .height('100%')
  }

  private screenCapture?: media.AVScreenCaptureRecorder;

  // 调用startRecording方法可以开始一次录屏存文件的流程,结束录屏可以通过点击录屏胶囊停止按钮进行操作。
  public async startRecording() {
    this.screenCapture = await media.createAVScreenCaptureRecorder();
    console.info(this.TAG,"startRecording screenCapture on done" );
    try {
      let avCaptureConfig: media.AVScreenCaptureRecordConfig = {
        fd: this.mFile?.fd ?? 0, // 文件需要先有调用者创建,赋予写权限,将文件fd传给此参数
      }
      await this.screenCapture?.init(avCaptureConfig); // avCaptureConfig captureConfig
      console.info(this.TAG,"startRecording screenCapture init done" );
    } catch (err) {
      console.info(this.TAG,"startRecording init err: " + JSON.stringify(err) );
    }
    await this.screenCapture?.startRecording();
    console.info(this.TAG,"startRecording screenCapture startRecording" );
  }

  // 可以主动调用stopRecording方法来停止录屏。
  public async stopRecording() {
    if (this.screenCapture == undefined) {
      // Error
      return;
    }
    await this.screenCapture?.stopRecording();

    // 调用release()方法销毁实例,释放资源。
    await this.screenCapture?.release();

    // 最后需要关闭创建的录屏文件fd, fs.close(fd);
    fs.closeSync(this.mFile);
  }
}