HarmonyOS 媒体后台服务的完整开发
【摘要】 一、背景与动机你有没有想过,为什么听音乐的时候切到别的 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 生命周期陷阱
- onCommand 可能被多次调用:每次
startAbility都会触发onCommand,需要用startId区分 - onConnect/onDisconnect 配对:连接数管理很重要,避免资源泄漏
- onDestroy 不可靠:系统可能直接杀进程而不走 onDestroy,关键数据要提前持久化
- 不要在 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
自动保活
关键知识点回顾:
- 后台服务是媒体应用的核心:音乐、播客等应用的生命线,必须稳定可靠
- AVSession 是灵魂:它不仅让系统感知到你的媒体播放,还是最好的保活信号
- 长时任务是保活手段:申请和释放必须配对,不能滥用
- IPC 通信要严谨:MessageSequence 的读写顺序必须匹配,大文件走 URI
- 生命周期管理要精细:空闲自停、资源释放、异常恢复,一个都不能少
- HarmonyOS 6 更智能:AgentAbility 和智能后台任务让开发更简单
一句话总结:媒体服务的本质是"让音乐在后台继续响",但让这个过程稳定、省电、不被系统杀掉,才是真正的技术挑战。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)