鸿蒙开发系列 · 中级篇
前言
你的天气元服务已经能显示当前天气了,但有个明显的问题:用户每次想看天气都得主动打开应用。如果早上出门前台面卡片显示的还是昨天的数据,体验就大打折扣。反过来,如果用户关注的球队刚刚进球,他希望在锁屏上立刻看到推送——而不是等到下次打开App才知道。
推送和后台任务是现代移动应用的标配能力,但HarmonyOS的实现方式与Android/iOS有显著差异。Android的FCM、厂商推送各自为政,后台服务常常被系统杀死;iOS的APNs相对统一,但后台执行能力受限。HarmonyOS NEXT的做法是:统一推送通道 + 严格后台管控 + 智能任务调度。
本文将带你打通这两条链路:从服务端推送一条消息到设备,以及让应用在后台可靠地完成定时任务。
一、Push Kit 统一推送服务
1.1 Push Kit 架构与优势
HarmonyOS NEXT的推送服务名为 Push Kit,它是一个系统级、全设备统一的推送通道。
┌──────────────┐ HTTPS ┌──────────────┐ 系统级通道 ┌──────────────┐
│ 开发者服务器 │ ─────────────→ │ Push Kit │ ─────────────────→ │ 用户设备 │
│ (你的后端) │ (REST API) │ 推送平台 │ (不依赖App存活) │ (通知中心/横幅)│
└──────────────┘ └──────────────┘ └──────────────┘
与Android推送相比,Push Kit的核心优势:
1.2 消息类型:通知消息 vs 透传消息
Push Kit支持两种消息:
通知消息的优势是即使应用被杀死也能正常展示;透传消息的优势是可以携带自定义数据,由应用决定如何响应。
1.3 客户端接入实战
步骤1:配置module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.RECEIVE_NOTIFICATION",
"reason": "$string:perm_push_reason"
}
]
}
}
步骤2:获取Push Token
Push Token是设备的唯一标识,服务端需要用它来指定推送目标。
// entry/src/main/ets/utils/PushManager.ets
import { push } from '@kit.PushKit';
import { promptAction } from '@kit.ArkUI';
export class PushManager {
// 获取Push Token
static async getToken(): Promise<string> {
try {
const token = await push.getToken();
console.info(`Push Token: ${token}`);
return token;
} catch (err) {
console.error('获取Push Token失败', err);
throw err;
}
}
// 删除Push Token(如用户退出登录)
static async deleteToken(): Promise<void> {
try {
await push.deleteToken();
console.info('Push Token已删除');
} catch (err) {
console.error('删除Push Token失败', err);
}
}
// 注册推送回调(透传消息)
static registerPushCallback(onMessage: (data: string) => void): void {
push.on('message', (message) => {
console.info('收到透传消息:', JSON.stringify(message));
onMessage(message.data);
});
}
// 注销推送回调
static unregisterPushCallback(): void {
push.off('message');
}
}
步骤3:在EntryAbility中初始化
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PushManager } from '../utils/PushManager';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 应用启动时获取Token并上报服务端
PushManager.getToken().then(token => {
this.reportTokenToServer(token);
});
// 注册透传消息监听
PushManager.registerPushCallback((data) => {
this.handlePushData(data);
});
}
private async reportTokenToServer(token: string) {
// 将token上报到你的服务器
// 服务器后续用此token向该设备推送消息
console.info('上报Token到服务器:', token);
}
private handlePushData(data: string) {
try {
const payload = JSON.parse(data);
// 根据payload.type处理不同类型消息
switch (payload.type) {
case 'weather_update':
this.refreshWeatherWidget(payload.data);
break;
case 'news_alert':
this.showNewsNotification(payload.data);
break;
}
} catch (e) {
console.error('解析推送数据失败', e);
}
}
private refreshWeatherWidget(data: unknown) {
// 刷新天气卡片
console.info('刷新天气数据:', data);
}
private showNewsNotification(data: unknown) {
// 展示新闻通知
console.info('展示新闻:', data);
}
onDestroy(): void {
PushManager.unregisterPushCallback();
}
}
1.4 服务端推送示例
服务端通过HTTPS调用Push Kit REST API发送消息:
# 获取访问令牌(OAuth 2.0)
curl -X POST "https://oauth-login.cloud.huawei.com/oauth2/v3/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
# 发送推送消息
curl -X POST "https://push-api.cloud.huawei.com/v1/YOUR_APP_ID/messages:send" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"token": "DEVICE_PUSH_TOKEN",
"notification": {
"title": "天气提醒",
"body": "北京下午有雨,记得带伞"
},
"android": {
"notification": {
"click_action": {
"type": 1,
"intent": "#Intent;action=ohos.want.action.view;component=your.bundle.name/EntryAbility;end"
}
}
}
}
}'
实际项目中,建议使用华为提供的服务端SDK简化调用。
二、后台任务管理:Work Scheduler
2.1 后台执行的三种方式
HarmonyOS NEXT严格限制了应用的后台行为,提供了三种标准化的后台执行方式:
对于大多数场景,Work Scheduler是首选。它由系统统一调度,在满足条件时批量执行多个应用的任务,降低整体功耗。
2.2 Work Scheduler 实战
场景:每天早上8点自动刷新天气数据,但只在Wi-Fi环境下执行以节省流量。
// entry/src/main/ets/utils/WorkSchedulerManager.ets
import { workScheduler } from '@kit.BackgroundTasksKit';
export class WorkSchedulerManager {
// 注册定时天气刷新任务
static registerWeatherRefreshTask(): void {
const workInfo: workScheduler.WorkInfo = {
workId: 1001, // 任务唯一ID
bundleName: 'com.example.weatherapp',
abilityName: 'WorkAbility',
networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI, // 仅在Wi-Fi下执行
chargerType: workScheduler.ChargingType.CHARGING_PLUGGED_ANY, // 充电时也可执行
batteryLevel: 20, // 电量不低于20%
storageRequest: workScheduler.StorageRequest.STORAGE_LEVEL_OKAY, // 存储空间充足
repeatCycleTime: 24 * 60 * 60 * 1000, // 每24小时执行一次
repeatCount: workScheduler.REPEAT_INFINITE // 无限重复
};
try {
workScheduler.startWork(workInfo);
console.info('天气刷新任务已注册');
} catch (err) {
console.error('注册任务失败', err);
}
}
// 取消任务
static cancelWeatherRefreshTask(): void {
const workInfo: workScheduler.WorkInfo = {
workId: 1001,
bundleName: 'com.example.weatherapp',
abilityName: 'WorkAbility'
};
try {
workScheduler.stopWork(workInfo);
console.info('天气刷新任务已取消');
} catch (err) {
console.error('取消任务失败', err);
}
}
// 查询所有已注册任务
static async getAllWorks(): Promise<workScheduler.WorkInfo[]> {
try {
return await workScheduler.getAllWorks();
} catch (err) {
console.error('查询任务失败', err);
return [];
}
}
}
WorkAbility 实现:
// entry/src/main/ets/workability/WorkAbility.ets
import { WorkSchedulerExtensionAbility } from '@kit.BackgroundTasksKit';
import { http } from '@kit.NetworkKit';
export default class WorkAbility extends WorkSchedulerExtensionAbility {
// 任务触发时执行
async onWorkStart(workInfo: workScheduler.WorkInfo): Promise<void> {
console.info(`任务 ${workInfo.workId} 开始执行`);
try {
// 调用天气API获取最新数据
const weatherData = await this.fetchWeatherData();
// 更新本地缓存
await this.updateLocalCache(weatherData);
// 更新桌面卡片
await this.updateWidget(weatherData);
console.info('天气数据刷新完成');
} catch (err) {
console.error('天气刷新失败', err);
}
}
private async fetchWeatherData(): Promise<unknown> {
// 实际项目中调用真实天气API
return {
city: '北京',
temp: 24,
condition: '晴',
updateTime: new Date().toISOString()
};
}
private async updateLocalCache(data: unknown): Promise<void> {
// 写入Preferences或RDB
console.info('更新本地缓存:', data);
}
private async updateWidget(data: unknown): Promise<void> {
// 调用FormBindingData更新卡片
console.info('更新桌面卡片:', data);
}
onWorkStop(workInfo: workScheduler.WorkInfo): void {
console.info(`任务 ${workInfo.workId} 已停止`);
}
}
2.3 Work Scheduler 的约束与最佳实践
最佳实践:
任务幂等设计:Work Scheduler不保证任务只执行一次,网络波动可能导致重复触发
// 每次执行前检查时间戳,避免重复刷新 async onWorkStart(workInfo: workScheduler.WorkInfo): Promise<void> { const lastUpdate = await this.getLastUpdateTime(); if (Date.now() - lastUpdate < 23 * 60 * 60 * 1000) { console.info('距离上次更新不足23小时,跳过'); return; } // 执行刷新... }失败重试策略:Work Scheduler本身不提供重试,需要自行实现
private async fetchWithRetry(url: string, maxRetries: number = 3): Promise<Response> { for (let i = 0; i < maxRetries; i++) { try { return await http.createHttp().request(url); } catch (err) { if (i === maxRetries - 1) throw err; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); // 指数退避 } } throw new Error('请求失败'); }批量操作:在2分钟窗口内尽量完成更多工作
async onWorkStart(): Promise<void> { // 同时执行多个独立的刷新任务 await Promise.all([ this.refreshWeather(), this.refreshNews(), this.cleanOldCache() ]); }
三、推送 + 后台任务的组合拳
3.1 场景设计:智能天气服务
将Push Kit和Work Scheduler组合,可以构建一个体验优秀的天气服务:
场景1:定时刷新(Work Scheduler)
├── 每天早上8点,Wi-Fi环境下自动刷新天气
├── 更新本地缓存 + 桌面卡片
└── 无需用户干预,卡片始终展示最新数据
场景2:恶劣天气预警(Push Kit)
├── 服务端监测到暴雨/大风/高温预警
├── 立即推送通知消息到设备
├── 用户点击通知进入详情页
└── 同时携带透传数据,静默更新卡片
场景3:用户手动刷新(前台交互)
├── 用户打开应用,下拉刷新
├── 前台请求最新数据
├── 更新UI + 缓存 + 卡片
└── 重置Work Scheduler的定时器
3.2 代码整合
// entry/src/main/ets/utils/WeatherService.ets
import { PushManager } from './PushManager';
import { WorkSchedulerManager } from './WorkSchedulerManager';
export class WeatherService {
// 初始化所有天气相关服务
static async initialize(): Promise<void> {
// 1. 获取并上报Push Token
const token = await PushManager.getToken();
await this.reportToken(token);
// 2. 注册透传消息监听
PushManager.registerPushCallback((data) => {
this.handleServerPush(data);
});
// 3. 注册定时刷新任务
WorkSchedulerManager.registerWeatherRefreshTask();
console.info('天气服务初始化完成');
}
// 用户手动刷新
static async manualRefresh(): Promise<void> {
const data = await this.fetchWeatherFromServer();
await this.updateAll(data);
}
// 服务端推送处理
private static async handleServerPush(data: string): Promise<void> {
const payload = JSON.parse(data);
if (payload.type === 'weather_alert') {
await this.updateAll(payload.data);
}
}
// 定时任务触发刷新
static async scheduledRefresh(): Promise<void> {
const data = await this.fetchWeatherFromServer();
await this.updateAll(data);
}
private static async updateAll(data: unknown): Promise<void> {
await Promise.all([
this.updateLocalCache(data),
this.updateWidget(data)
]);
}
private static async reportToken(token: string): Promise<void> {
// 上报服务端
}
private static async fetchWeatherFromServer(): Promise<unknown> {
// 网络请求
return {};
}
private static async updateLocalCache(data: unknown): Promise<void> {
// 更新缓存
}
private static async updateWidget(data: unknown): Promise<void> {
// 更新卡片
}
}
四、常见问题排查
五、小结与下篇预告
本文打通了两条关键链路:
Push Kit推送:系统级统一推送通道,支持通知消息和透传消息,覆盖手机、平板、车机等全设备
Work Scheduler后台任务:延迟/定时任务的标准化调度,在系统约束下实现可靠的后台执行
组合策略:推送负责即时触达,后台任务负责定时维护,两者结合覆盖绝大多数场景
HarmonyOS对后台的严格管控,本质上是一种"优雅约束"——它迫使开发者放弃"后台常驻"的坏习惯,转而用系统提供的标准化机制解决问题。结果是功耗更低、体验更一致。
接下来,我们将进入更具体的领域能力开发。音视频是现代应用的高频场景,但相机预览、录音、播放、编解码的复杂度也很高。第14篇将深入鸿蒙音视频开发实战,从AVPlayer音频播放到Camera Kit相机采集,再到媒体会话管理和后台播放,帮你构建完整的音视频技术栈。
参考阅读
本系列第11篇:《元服务开发入门:从概念到第一个原子化服务》
本系列第12篇:《安全机制与权限管理:应用签名、ACL与隐私合规》