HarmonyOS 媒体后台服务的完整开发

举报
Jack20 发表于 2026/06/21 11:51:41 2026/06/21
【摘要】 一、背景与动机你有没有想过,为什么听音乐的时候切到别的 App,音乐还能继续播放?为什么导航的时候锁屏了,语音播报还在继续?这些"后台运行"的能力,靠的就是媒体服务。在移动端开发中,后台服务是一个绕不开的话题。尤其是媒体类应用——音乐播放器、播客、有声书、导航语音……这些应用的核心体验就是"退到后台也能继续工作"。但后台服务又是最容易被系统杀掉的,如何在保证用户体验的同时让服务稳定运行,这...

一、背景与动机

你有没有想过,为什么听音乐的时候切到别的 App,音乐还能继续播放?为什么导航的时候锁屏了,语音播报还在继续?这些"后台运行"的能力,靠的就是媒体服务。

在移动端开发中,后台服务是一个绕不开的话题。尤其是媒体类应用——音乐播放器、播客、有声书、导航语音……这些应用的核心体验就是"退到后台也能继续工作"。但后台服务又是最容易被系统杀掉的,如何在保证用户体验的同时让服务稳定运行,这是一个需要深入理解系统机制才能解决的问题。

HarmonyOS 提供了 ServiceAbility 和 Background Task 机制来支持后台媒体服务。理解它们的生命周期、通信方式和保活策略,是开发高质量媒体应用的关键。


二、核心原理

2.1 媒体服务架构

flowchart TB
    classDef primary fill:#4A90D9,stroke:#2E6BA6,color:#fff,stroke-width:2px
    classDef warning fill:#E6A23C,stroke:#CF8C12,color:#fff,stroke-width:2px
    classDef error fill:#F56C6C,stroke:#DD3B3B,color:#fff,stroke-width:2px
    classDef info fill:#67C23A,stroke:#4AA82A,color:#fff,stroke-width:2px
    classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,stroke-width:2px

    A[UIAbility 前台界面]:::primary --> B[ServiceAbility 媒体服务]:::warning
    
    B --> C[媒体播放引擎]:::info
    B --> D[AVSession 媒体会话]:::purple
    B --> E[后台任务管理]:::error
    
    C --> F[音频播放]:::primary
    C --> G[视频播放]:::primary
    
    D --> H[系统媒体控制]:::info
    D --> I[锁屏控制]:::info
    D --> J[通知栏控制]:::info
    
    E --> K[长时任务申请]:::warning
    E --> L[后台保活]:::warning
    E --> M[资源管理]:::warning
    
    B -.->|CommonEvent| N[其他应用]:::purple
    B -.->|DataShare| O[媒体数据]:::purple

2.2 ServiceAbility 生命周期

ServiceAbility 的生命周期比 UIAbility 简单,但媒体场景下需要特别关注后台状态的处理:

flowchart TB
    classDef primary fill:#4A90D9,stroke:#2E6BA6,color:#fff,stroke-width:2px
    classDef warning fill:#E6A23C,stroke:#CF8C12,color:#fff,stroke-width:2px
    classDef error fill:#F56C6C,stroke:#DD3B3B,color:#fff,stroke-width:2px
    classDef info fill:#67C23A,stroke:#4AA82A,color:#fff,stroke-width:2px
    classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,stroke-width:2px

    A[创建 onCreate]:::primary --> B[连接/启动]:::warning
    B --> C[运行中]:::info
    C --> D{收到停止请求?}:::error
    D -->|| E[销毁 onDestroy]:::error
    D -->|| C
    
    C --> F{系统资源回收?}:::purple
    F -->|| G[冻结/回收]:::error
    G -->|恢复| C
    
    B --> H[onConnect 回调]:::info
    B --> I[onCommand 回调]:::info

2.3 核心概念

概念 说明
ServiceAbility 无界面的后台服务组件,用于执行长时间运行的任务
BackgroundTaskManager 后台任务管理器,用于申请长时任务和短时任务
ContinuousTask 长时任务,允许应用在后台持续运行(如音乐播放)
AVSession 媒体会话,让系统和其他应用能感知到媒体播放状态
CommonEvent 公共事件,用于跨组件/跨应用通信

三、代码实战

3.1 后台媒体服务基础实现

这是最核心的部分——一个完整的后台音乐播放服务,包含 AVSession 集成和后台任务申请。

// MediaPlaybackService.ets
// 后台媒体播放服务

import { ServiceExtensionAbility } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { avSession } from '@kit.AvSessionKit';
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Want } from '@kit.AbilityKit';

// 播放状态枚举
enum PlaybackState {
  IDLE = 'idle',
  PLAYING = 'playing',
  PAUSED = 'paused',
  STOPPED = 'stopped',
  ERROR = 'error'
}

// 服务内部状态
interface MediaServiceState {
  playbackState: PlaybackState;
  currentTrack: string;
  currentPosition: number;
  duration: number;
  volume: number;
}

export default class MediaPlaybackService extends ServiceExtensionAbility {
  // 媒体会话
  private session: avSession.AVSession | null = null;
  // 播放器
  private player: media.AVPlayer | null = null;
  // 服务状态
  private state: MediaServiceState = {
    playbackState: PlaybackState.IDLE,
    currentTrack: '',
    currentPosition: 0,
    duration: 0,
    volume: 50
  };
  // 后台任务 ID
  private backgroundTaskId: number = -1;
  // 长时任务是否已申请
  private isContinuousTaskRunning: boolean = false;

  // 服务创建
  onCreate() {
    console.info('[媒体服务] 服务创建');
    this.initAVSession();
    this.initPlayer();
  }

  // 初始化 AVSession
  async initAVSession() {
    try {
      this.session = await avSession.createAVSession(this.context, '后台音乐服务', 'audio');

      // 设置媒体元数据
      this.updateMetadata({
        title: '未知曲目',
        artist: '未知艺术家',
        album: '未知专辑',
        duration: 0
      });

      // 注册控制命令回调
      this.session.on('play', () => {
        console.info('[媒体服务] 收到播放命令');
        this.play();
      });

      this.session.on('pause', () => {
        console.info('[媒体服务] 收到暂停命令');
        this.pause();
      });

      this.session.on('stop', () => {
        console.info('[媒体服务] 收到停止命令');
        this.stop();
      });

      this.session.on('seek', (time: number) => {
        console.info(`[媒体服务] 收到跳转命令: ${time}ms`);
        this.seek(time);
      });

      this.session.on('setSpeed', (speed: number) => {
        console.info(`[媒体服务] 收到变速命令: ${speed}x`);
        this.setSpeed(speed);
      });

      // 激活会话
      await this.session.activate();
      console.info('[媒体服务] AVSession 初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] AVSession 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  // 初始化播放器
  async initPlayer() {
    try {
      this.player = await media.createAVPlayer();

      // 播放状态回调
      this.player.on('stateChange', (state: string) => {
        console.info(`[媒体服务] 播放器状态: ${state}`);
        this.handlePlayerStateChange(state);
      });

      // 播放进度回调
      this.player.on('timeUpdate', (time: number) => {
        this.state.currentPosition = time;
        this.updatePlaybackState();
      });

      // 错误回调
      this.player.on('error', (error: BusinessError) => {
        console.error(`[媒体服务] 播放错误: ${error.code} - ${error.message}`);
        this.state.playbackState = PlaybackState.ERROR;
        this.updatePlaybackState();
      });

      console.info('[媒体服务] 播放器初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 播放器初始化失败: ${err.code} - ${err.message}`);
    }
  }

  // 处理播放器状态变更
  private handlePlayerStateChange(state: string) {
    switch (state) {
      case 'playing':
        this.state.playbackState = PlaybackState.PLAYING;
        this.startContinuousTask();  // 开始播放时申请后台任务
        break;
      case 'paused':
        this.state.playbackState = PlaybackState.PAUSED;
        break;
      case 'stopped':
      case 'idle':
        this.state.playbackState = PlaybackState.STOPPED;
        this.stopContinuousTask();   // 停止播放时释放后台任务
        break;
      case 'error':
        this.state.playbackState = PlaybackState.ERROR;
        this.stopContinuousTask();
        break;
    }
    this.updatePlaybackState();
  }

  // 播放
  async play() {
    if (!this.player) return;
    try {
      if (this.state.playbackState === PlaybackState.PAUSED) {
        await this.player.play();
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 播放失败: ${err.code} - ${err.message}`);
    }
  }

  // 暂停
  async pause() {
    if (!this.player) return;
    try {
      await this.player.pause();
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 暂停失败: ${err.code} - ${err.message}`);
    }
  }

  // 停止
  async stop() {
    if (!this.player) return;
    try {
      await this.player.stop();
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 停止失败: ${err.code} - ${err.message}`);
    }
  }

  // 跳转
  async seek(timeMs: number) {
    if (!this.player) return;
    try {
      await this.player.seek(timeMs);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 跳转失败: ${err.code} - ${err.message}`);
    }
  }

  // 变速
  async setSpeed(speed: number) {
    if (!this.player) return;
    try {
      await this.player.setSpeed(speed);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 变速失败: ${err.code} - ${err.message}`);
    }
  }

  // 加载并播放指定曲目
  async loadAndPlay(mediaUri: string, title: string, artist: string) {
    if (!this.player) return;
    try {
      this.state.currentTrack = title;
      // 设置媒体源
      this.player.url = mediaUri;
      // 更新元数据
      this.updateMetadata({ title, artist, album: '', duration: 0 });
      console.info(`[媒体服务] 加载曲目: ${title}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 加载曲目失败: ${err.code} - ${err.message}`);
    }
  }

  // 更新媒体元数据
  private updateMetadata(info: { title: string; artist: string; album: string; duration: number }) {
    if (!this.session) return;
    const metadata: avSession.AVMetadata = {
      assetId: `track_${Date.now()}`,
      title: info.title,
      artist: info.artist,
      album: info.album,
      mediaType: 'AUDIO',
      duration: info.duration
    };
    this.session.setAVMetadata(metadata);
  }

  // 更新播放状态到 AVSession
  private updatePlaybackState() {
    if (!this.session) return;
    const playbackState: avSession.AVPlaybackState = {
      state: this.mapToAVPlaybackState(this.state.playbackState),
      speed: 1.0,
      position: {
        elapsedTime: this.state.currentPosition,
        updateTime: Date.now()
      },
      bufferedTime: 0
    };
    this.session.setAVPlaybackState(playbackState);
  }

  // 映射播放状态
  private mapToAVPlaybackState(state: PlaybackState): avSession.PlaybackState {
    switch (state) {
      case PlaybackState.PLAYING:
        return avSession.PlaybackState.PLAYBACK_STATE_PLAY;
      case PlaybackState.PAUSED:
        return avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
      case PlaybackState.STOPPED:
        return avSession.PlaybackState.PLAYBACK_STATE_STOP;
      case PlaybackState.ERROR:
        return avSession.PlaybackState.PLAYBACK_STATE_ERROR;
      default:
        return avSession.PlaybackState.PLAYBACK_STATE_IDLE;
    }
  }

  // 申请长时后台任务
  private async startContinuousTask() {
    if (this.isContinuousTaskRunning) return;

    try {
      // 申请长时任务
      this.backgroundTaskId = await backgroundTaskManager.requestEnableContinuousTask(
        this.context,
        backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
        '后台音乐播放'
      );
      this.isContinuousTaskRunning = true;
      console.info(`[媒体服务] 长时任务已申请, ID: ${this.backgroundTaskId}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 长时任务申请失败: ${err.code} - ${err.message}`);
    }
  }

  // 释放长时后台任务
  private async stopContinuousTask() {
    if (!this.isContinuousTaskRunning) return;

    try {
      backgroundTaskManager.cancelContinuousTask(this.context, this.backgroundTaskId);
      this.isContinuousTaskRunning = false;
      console.info('[媒体服务] 长时任务已释放');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[媒体服务] 长时任务释放失败: ${err.code} - ${err.message}`);
    }
  }

  // 处理启动请求
  onCommand(want: Want, startId: number) {
    const action = want.parameters?.['action'] as string;
    console.info(`[媒体服务] 收到命令: ${action}, startId: ${startId}`);

    switch (action) {
      case 'play':
        this.play();
        break;
      case 'pause':
        this.pause();
        break;
      case 'stop':
        this.stop();
        break;
      case 'loadAndPlay': {
        const uri = want.parameters?.['uri'] as string;
        const title = want.parameters?.['title'] as string;
        const artist = want.parameters?.['artist'] as string;
        if (uri) {
          this.loadAndPlay(uri, title || '未知', artist || '未知');
        }
        break;
      }
      default:
        console.warn(`[媒体服务] 未知命令: ${action}`);
    }
  }

  // 处理连接请求
  onConnect(want: Want) {
    console.info('[媒体服务] 客户端连接');
    // 返回远程对象用于 IPC 通信
    return new MediaServiceRemote(this);
  }

  // 服务销毁
  onDestroy() {
    console.info('[媒体服务] 服务销毁');
    // 释放资源
    this.stopContinuousTask();
    this.player?.release();
    this.session?.destroy();
    this.player = null;
    this.session = null;
  }
}

// IPC 远程对象,用于客户端与服务端通信
import { rpc } from '@kit.IPCKit';

class MediaServiceRemote extends rpc.RemoteObject {
  private service: MediaPlaybackService;

  constructor(service: MediaPlaybackService) {
    super('MediaServiceRemote');
    this.service = service;
  }

  // 处理客户端请求
  onRemoteRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, options: rpc.MessageOption): boolean {
    switch (code) {
      case 1: {  // 获取当前播放状态
        const state = this.service.state.playbackState;
        reply.writeString(state);
        return true;
      }
      case 2: {  // 获取当前曲目信息
        reply.writeString(this.service.state.currentTrack);
        return true;
      }
      case 3: {  // 获取播放进度
        reply.writeLong(this.service.state.currentPosition);
        return true;
      }
      default:
        console.warn(`[媒体服务IPC] 未知请求码: ${code}`);
        return false;
    }
  }
}

3.2 媒体服务生命周期管理

在媒体服务中,生命周期的管理尤为重要——你需要在正确的时机申请后台任务、释放资源、处理系统回收。

// MediaServiceLifecycle.ets
// 媒体服务生命周期管理器

import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { avSession } from '@kit.AvSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 生命周期状态
enum ServiceLifecycleState {
  CREATED = 'created',
  STARTED = 'started',
  CONNECTED = 'connected',
  BACKGROUND_RUNNING = 'background_running',
  SUSPENDED = 'suspended',
  DESTROYING = 'destroying'
}

export default class MediaServiceLifecycle extends ServiceExtensionAbility {
  // 生命周期状态
  private lifecycleState: ServiceLifecycleState = ServiceLifecycleState.CREATED;
  // AVSession 引用
  private session: avSession.AVSession | null = null;
  // 后台任务 ID
  private bgTaskId: number = -1;
  // 是否正在播放
  private isPlaying: boolean = false;
  // 活跃连接数
  private activeConnections: number = 0;
  // 空闲超时定时器
  private idleTimer: number = -1;
  // 空闲超时时间(5分钟)
  private readonly IDLE_TIMEOUT_MS = 5 * 60 * 1000;

  onCreate() {
    this.lifecycleState = ServiceLifecycleState.CREATED;
    console.info('[生命周期] onCreate');
    this.initSession();
  }

  // 初始化媒体会话
  private async initSession() {
    try {
      this.session = await avSession.createAVSession(this.context, '生命周期管理服务', 'audio');
      await this.session.activate();

      // 监听系统对会话的干预(如其他应用抢占音频焦点)
      this.session.on('interrupt', (interruptEvent: avSession.InterruptEvent) => {
        this.handleAudioInterrupt(interruptEvent);
      });

      console.info('[生命周期] 会话初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[生命周期] 会话初始化失败: ${err.code} - ${err.message}`);
    }
  }

  // 处理音频焦点中断
  private handleAudioInterrupt(event: avSession.InterruptEvent) {
    console.info(`[生命周期] 音频中断: type=${event.interruptType}, hint=${event.interruptHint}`);

    switch (event.interruptHint) {
      case avSession.InterruptHint.INTERRUPT_HINT_PAUSE:
        // 系统要求暂停(如来电)
        this.isPlaying = false;
        this.updateLifecycleState();
        break;
      case avSession.InterruptHint.INTERRUPT_HINT_RESUME:
        // 系统允许恢复
        this.isPlaying = true;
        this.updateLifecycleState();
        break;
      case avSession.InterruptHint.INTERRUPT_HINT_STOP:
        // 系统要求停止
        this.isPlaying = false;
        this.releaseBackgroundTask();
        this.updateLifecycleState();
        break;
    }
  }

  // 启动命令
  onCommand(want: Want, startId: number) {
    this.lifecycleState = ServiceLifecycleState.STARTED;
    console.info(`[生命周期] onCommand, startId: ${startId}`);

    const action = want.parameters?.['action'] as string;
    if (action === 'play') {
      this.isPlaying = true;
      this.requestBackgroundTask();
    } else if (action === 'stop') {
      this.isPlaying = false;
      this.releaseBackgroundTask();
    }

    this.updateLifecycleState();
    this.resetIdleTimer();
  }

  // 连接回调
  onConnect(want: Want) {
    this.activeConnections++;
    this.lifecycleState = ServiceLifecycleState.CONNECTED;
    console.info(`[生命周期] onConnect, 活跃连接: ${this.activeConnections}`);
    this.resetIdleTimer();
    return null;
  }

  // 断开连接回调
  onDisconnect(want: Want) {
    this.activeConnections = Math.max(0, this.activeConnections - 1);
    console.info(`[生命周期] onDisconnect, 活跃连接: ${this.activeConnections}`);

    // 没有活跃连接且未在播放,启动空闲计时
    if (this.activeConnections === 0 && !this.isPlaying) {
      this.startIdleTimer();
    }
  }

  // 申请后台任务
  private async requestBackgroundTask() {
    if (this.bgTaskId !== -1) return;

    try {
      this.bgTaskId = await backgroundTaskManager.requestEnableContinuousTask(
        this.context,
        backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
        '后台音频播放'
      );
      this.lifecycleState = ServiceLifecycleState.BACKGROUND_RUNNING;
      console.info(`[生命周期] 后台任务已申请: ${this.bgTaskId}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[生命周期] 后台任务申请失败: ${err.code} - ${err.message}`);
    }
  }

  // 释放后台任务
  private releaseBackgroundTask() {
    if (this.bgTaskId === -1) return;

    try {
      backgroundTaskManager.cancelContinuousTask(this.context, this.bgTaskId);
      this.bgTaskId = -1;
      console.info('[生命周期] 后台任务已释放');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[生命周期] 后台任务释放失败: ${err.code} - ${err.message}`);
    }
  }

  // 更新生命周期状态
  private updateLifecycleState() {
    if (this.isPlaying && this.bgTaskId !== -1) {
      this.lifecycleState = ServiceLifecycleState.BACKGROUND_RUNNING;
    } else if (this.activeConnections > 0) {
      this.lifecycleState = ServiceLifecycleState.CONNECTED;
    } else if (this.isPlaying) {
      this.lifecycleState = ServiceLifecycleState.STARTED;
    } else {
      this.lifecycleState = ServiceLifecycleState.SUSPENDED;
    }
    console.info(`[生命周期] 当前状态: ${this.lifecycleState}`);
  }

  // 重置空闲计时器
  private resetIdleTimer() {
    if (this.idleTimer !== -1) {
      clearTimeout(this.idleTimer);
      this.idleTimer = -1;
    }
  }

  // 启动空闲计时器
  private startIdleTimer() {
    this.resetIdleTimer();
    this.idleTimer = setTimeout(() => {
      console.info('[生命周期] 空闲超时,服务自停');
      this.selfStop();
    }, this.IDLE_TIMEOUT_MS) as unknown as number;
  }

  // 服务自停
  private selfStop() {
    // 释放所有资源
    this.releaseBackgroundTask();
    this.session?.destroy();
    this.session = null;
    // 调用系统接口停止自身
    this.context.terminateSelf();
  }

  // 服务销毁
  onDestroy() {
    this.lifecycleState = ServiceLifecycleState.DESTROYING;
    console.info('[生命周期] onDestroy');
    this.resetIdleTimer();
    this.releaseBackgroundTask();
    this.session?.destroy();
  }
}

3.3 媒体服务通信与保活

服务需要与 UI 层通信,同时还需要在系统资源紧张时尽量保活。下面展示 IPC 通信和保活策略。

// MediaServiceCommunication.ets
// 媒体服务通信与保活策略

import { common, Want, WantConstant } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { avSession } from '@kit.AvSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { rpc } from '@kit.IPCKit';

// ======== 第一部分:客户端通信封装 ========

// MediaServiceProxy.ets
// 客户端代理,封装与服务端的 IPC 通信

export class MediaServiceProxy {
  private remote: rpc.IRemoteObject | null = null;
  private connection: number = -1;
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 连接服务
  async connect(): Promise<boolean> {
    const want: Want = {
      bundleName: 'com.example.mediapp',
      abilityName: 'MediaPlaybackService'
    };

    return new Promise((resolve) => {
      this.connection = this.context.connectServiceExtensionAbility(want, {
        onConnect: (elementName, remote) => {
          this.remote = remote;
          console.info('[服务通信] 已连接到媒体服务');
          resolve(true);
        },
        onDisconnect: (elementName) => {
          this.remote = null;
          console.info('[服务通信] 已断开媒体服务');
        },
        onFailed: (elementName) => {
          this.remote = null;
          console.error('[服务通信] 连接媒体服务失败');
          resolve(false);
        }
      });
    });
  }

  // 断开连接
  disconnect() {
    if (this.connection !== -1) {
      this.context.disconnectServiceExtensionAbility(this.connection);
      this.connection = -1;
      this.remote = null;
    }
  }

  // 发送播放命令
  async sendPlayCommand() {
    await this.sendCommand(1, '');
  }

  // 发送暂停命令
  async sendPauseCommand() {
    await this.sendCommand(2, '');
  }

  // 发送停止命令
  async sendStopCommand() {
    await this.sendCommand(3, '');
  }

  // 获取播放状态
  async getPlaybackState(): Promise<string> {
    return await this.sendRequest(100, '') as string;
  }

  // 获取当前曲目
  async getCurrentTrack(): Promise<string> {
    return await this.sendRequest(101, '') as string;
  }

  // 获取播放进度
  async getCurrentPosition(): Promise<number> {
    const result = await this.sendRequest(102, '');
    return result as number;
  }

  // 通用命令发送
  private async sendCommand(code: number, data: string): Promise<void> {
    if (!this.remote) {
      console.warn('[服务通信] 未连接到服务');
      return;
    }

    const messageSequence = rpc.MessageSequence.create();
    const reply = rpc.MessageSequence.create();
    messageSequence.writeString(data);

    try {
      await this.remote.sendMessageRequest(code, messageSequence, reply, new rpc.MessageOption());
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[服务通信] 命令发送失败: ${err.code} - ${err.message}`);
    }
  }

  // 通用请求发送(有返回值)
  private async sendRequest(code: number, data: string): Promise<string | number> {
    if (!this.remote) {
      console.warn('[服务通信] 未连接到服务');
      return '';
    }

    const messageSequence = rpc.MessageSequence.create();
    const reply = rpc.MessageSequence.create();
    messageSequence.writeString(data);

    try {
      await this.remote.sendMessageRequest(code, messageSequence, reply, new rpc.MessageOption());
      // 尝试读取字符串或数字
      try {
        return reply.readString();
      } catch {
        return reply.readLong();
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[服务通信] 请求发送失败: ${err.code} - ${err.message}`);
      return '';
    }
  }
}

// ======== 第二部分:保活策略管理器 ========

// KeepAliveManager.ets
// 媒体服务保活策略管理

export class KeepAliveManager {
  private context: common.UIAbilityContext;
  private bgTaskId: number = -1;
  private isContinuousTaskActive: boolean = false;
  // 心跳间隔(秒)
  private heartbeatInterval: number = 30;
  // 心跳定时器
  private heartbeatTimer: number = -1;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 申请长时后台任务(核心保活手段)
  async requestContinuousTask(reason: string): Promise<boolean> {
    if (this.isContinuousTaskActive) {
      console.info('[保活] 长时任务已在运行');
      return true;
    }

    try {
      this.bgTaskId = await backgroundTaskManager.requestEnableContinuousTask(
        this.context,
        backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
        reason
      );
      this.isContinuousTaskActive = true;
      console.info(`[保活] 长时任务申请成功: ${this.bgTaskId}`);
      this.startHeartbeat();
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[保活] 长时任务申请失败: ${err.code} - ${err.message}`);
      return false;
    }
  }

  // 释放长时后台任务
  releaseContinuousTask() {
    if (!this.isContinuousTaskActive) return;

    try {
      backgroundTaskManager.cancelContinuousTask(this.context, this.bgTaskId);
      this.isContinuousTaskActive = false;
      this.bgTaskId = -1;
      this.stopHeartbeat();
      console.info('[保活] 长时任务已释放');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[保活] 长时任务释放失败: ${err.code} - ${err.message}`);
    }
  }

  // 启动心跳(防止系统认为服务空闲而回收)
  private startHeartbeat() {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      this.performHeartbeat();
    }, this.heartbeatInterval * 1000) as unknown as number;
    console.info('[保活] 心跳已启动');
  }

  // 停止心跳
  private stopHeartbeat() {
    if (this.heartbeatTimer !== -1) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = -1;
    }
  }

  // 执行心跳
  private performHeartbeat() {
    // 更新 AVSession 状态,让系统知道服务仍然活跃
    // 这是保活的关键——让系统感知到服务仍在工作
    console.info('[保活] 心跳 tick');
  }

  // 检查后台任务状态
  isTaskRunning(): boolean {
    return this.isContinuousTaskActive;
  }
}

// ======== 第三部分:UI 集成页面 ========

@Entry
@Component
struct MediaServicePage {
  // 服务代理
  private serviceProxy: MediaServiceProxy | null = null;
  // 保活管理器
  private keepAlive: KeepAliveManager | null = null;
  // 连接状态
  @State isConnected: boolean = false;
  // 播放状态
  @State playbackState: string = '未连接';
  // 当前曲目
  @State currentTrack: string = '';
  // 播放进度
  @State currentPosition: number = 0;

  aboutToAppear() {
    const context = getContext(this) as common.UIAbilityContext;
    this.serviceProxy = new MediaServiceProxy(context);
    this.keepAlive = new KeepAliveManager(context);
  }

  aboutToDisappear() {
    this.serviceProxy?.disconnect();
  }

  // 连接服务
  async connectService() {
    if (!this.serviceProxy) return;
    const connected = await this.serviceProxy.connect();
    this.isConnected = connected;
    if (connected) {
      this.playbackState = '已连接';
      // 申请保活
      await this.keepAlive?.requestContinuousTask('后台音乐播放');
    }
  }

  build() {
    Column() {
      Text('媒体服务管理')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      // 连接状态
      Row() {
        Circle({ width: 10, height: 10 })
          .fill(this.isConnected ? '#67C23A' : '#909399')
        Text(this.isConnected ? '已连接' : '未连接')
          .fontSize(14)
          .margin({ left: 8 })
      }
      .margin({ bottom: 8 })

      // 播放状态
      Text(`播放状态: ${this.playbackState}`)
        .fontSize(14)
        .margin({ bottom: 16 })

      // 连接/断开按钮
      Button(this.isConnected ? '断开服务' : '连接服务')
        .width('100%')
        .height(44)
        .backgroundColor(this.isConnected ? '#F56C6C' : '#4A90D9')
        .fontColor(Color.White)
        .margin({ bottom: 12 })
        .onClick(() => {
          if (this.isConnected) {
            this.serviceProxy?.disconnect();
            this.keepAlive?.releaseContinuousTask();
            this.isConnected = false;
            this.playbackState = '已断开';
          } else {
            this.connectService();
          }
        })

      // 控制按钮
      Row() {
        Button('播放')
          .onClick(() => this.serviceProxy?.sendPlayCommand())
          .backgroundColor('#67C23A')
          .fontColor(Color.White)
          .layoutWeight(1)
          .enabled(this.isConnected)

        Button('暂停')
          .onClick(() => this.serviceProxy?.sendPauseCommand())
          .backgroundColor('#E6A23C')
          .fontColor(Color.White)
          .layoutWeight(1)
          .margin({ left: 8 })
          .enabled(this.isConnected)

        Button('停止')
          .onClick(() => this.serviceProxy?.sendStopCommand())
          .backgroundColor('#F56C6C')
          .fontColor(Color.White)
          .layoutWeight(1)
          .margin({ left: 8 })
          .enabled(this.isConnected)
      }
      .width('100%')
      .margin({ bottom: 16 })

      // 保活状态
      Row() {
        Text('保活状态:')
          .fontSize(14)
        Text(this.keepAlive?.isTaskRunning() ? '运行中' : '未启动')
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.keepAlive?.isTaskRunning() ? '#67C23A' : '#909399')
          .margin({ left: 4 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

四、踩坑与注意事项

4.1 后台任务申请常见问题

问题 原因 解决方案
申请被拒绝 未声明对应权限 在 module.json5 中声明 ohos.permission.KEEP_BACKGROUND_RUNNING
后台任务被回收 未满足长时任务条件 确保确实在执行媒体播放,系统会检查
通知栏不显示 未配置通知内容 长时任务必须在通知栏显示持续通知
任务 ID 无效 重复释放 释放前检查 ID 是否有效

4.2 ServiceAbility 生命周期陷阱

  1. onCommand 可能被多次调用:每次 startAbility 都会触发 onCommand,需要用 startId 区分
  2. onConnect/onDisconnect 配对:连接数管理很重要,避免资源泄漏
  3. onDestroy 不可靠:系统可能直接杀进程而不走 onDestroy,关键数据要提前持久化
  4. 不要在 onCreate 做耗时操作:会影响服务启动速度

4.3 IPC 通信注意事项

  • MessageSequence 必须匹配:写入和读取的顺序、类型必须一致
  • 大文件不要通过 IPC 传输:IPC 有数据大小限制,大文件应该通过 URI 或共享内存传递
  • 异步处理耗时请求onRemoteRequest 在主线程执行,耗时操作应该放到子线程
  • 连接断开要处理:客户端异常退出时,服务端的 onDisconnect 可能不会被调用

4.4 保活策略注意

  • 不要滥用保活:只在确实需要后台运行时申请,否则会被系统判定为恶意行为
  • AVSession 是最好的保活信号:活跃的 AVSession 比长时任务更能说服系统你的服务"值得保留"
  • 空闲自停是好习惯:长时间空闲的服务应该自行停止,而不是一直占用后台资源
  • 适配不同设备策略:不同厂商对后台任务的管控策略不同,需要做兼容测试

五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5 HarmonyOS 6
服务模型 ServiceExtensionAbility 新增 AgentAbility(轻量级后台服务)
后台任务 ContinuousTask 新增 SmartBackgroundTask(智能后台任务)
IPC 通信 rpc.RemoteObject 新增 TypedRemoteObject(类型安全 IPC)
保活策略 手动管理 系统智能保活(基于 AVSession 状态自动判断)

5.2 迁移指南

// HarmonyOS 5 写法
export default class MediaService extends ServiceExtensionAbility {
  onCreate() { /* ... */ }
  onCommand(want: Want, startId: number) { /* ... */ }
  onConnect(want: Want) { /* ... */ }
  onDestroy() { /* ... */ }
}

// HarmonyOS 6 写法(AgentAbility 轻量级服务)
import { AgentAbility } from '@kit.AbilityKit';

export default class MediaAgentService extends AgentAbility {
  // 生命周期更简洁
  onCreate() { /* ... */ }
  onDestroy() { /* ... */ }
  
  // 任务式执行,系统自动管理生命周期
  async onTaskRequest(task: AgentTask) {
    switch (task.action) {
      case 'play':
        await this.handlePlay(task.parameters);
        break;
      case 'pause':
        await this.handlePause();
        break;
    }
    // 任务完成后系统自动判断是否需要保活
  }
}

5.3 新特性:智能后台任务

HarmonyOS 6 引入了智能后台任务,系统会根据 AVSession 状态自动判断是否需要保活:

// HarmonyOS 6 新增:智能后台任务
import { smartBackgroundTask } from '@kit.BackgroundTasksKit';

// 注册智能后台任务(系统自动管理)
const taskId = await smartBackgroundTask.register({
  type: 'media_playback',
  // 系统根据 AVSession 状态自动判断是否保活
  autoManage: true,
  // 当系统决定回收时的回调
  onReclaim: () => {
    console.info('[智能后台] 系统请求回收资源');
    // 保存状态、暂停播放等
  },
  // 当系统允许恢复时的回调
  onRestore: () => {
    console.info('[智能后台] 系统允许恢复运行');
    // 恢复播放
  }
});

六、总结

mindmap
  root((媒体服务))
    后台媒体服务
      ServiceExtensionAbility
      后台任务申请
      通知栏展示
      权限声明
    ServiceAbility媒体
      AVSession 集成
      播放器管理
      元数据更新
      控制命令回调
    生命周期
      onCreate/onDestroy
      onCommand 多次调用
      onConnect/onDisconnect
      空闲自停策略
    服务通信
      IPC RemoteObject
      MessageSequence
      客户端代理封装
      大文件传输
    服务保活
      长时任务申请
      AVSession 保活信号
      心跳机制
      空闲自停
    HarmonyOS 6
      AgentAbility
      智能后台任务
      TypedRemoteObject
      自动保活

关键知识点回顾

  1. 后台服务是媒体应用的核心:音乐、播客等应用的生命线,必须稳定可靠
  2. AVSession 是灵魂:它不仅让系统感知到你的媒体播放,还是最好的保活信号
  3. 长时任务是保活手段:申请和释放必须配对,不能滥用
  4. IPC 通信要严谨:MessageSequence 的读写顺序必须匹配,大文件走 URI
  5. 生命周期管理要精细:空闲自停、资源释放、异常恢复,一个都不能少
  6. HarmonyOS 6 更智能:AgentAbility 和智能后台任务让开发更简单

一句话总结:媒体服务的本质是"让音乐在后台继续响",但让这个过程稳定、省电、不被系统杀掉,才是真正的技术挑战。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。