后台同步重构:移除不合规长时任务,改为分层调度
背景:用户反馈"后台常驻其实没实现,过一段时间常驻通知就没了"。调研确认这是 系统强制管控、不是 bug:DATA_TRANSFER 长时任务有一致性校验,**超 10 分钟未更新 传输进度即被强制取消**。日历同步是"间歇跑、平时无进度",不符合该语义 → 必被取消, 且频繁触发校验失败可能被列入管控名单。另:workScheduler 请求 20 分钟低于系统 "应用活跃分组"下限(活跃≥2h),被静默压制 → 名义 20 分钟实际从未生效。 改为分层调度(目标:用户要看时数据一定最新,而非追求进程常驻): 1. **移除 dataTransfer 长时任务**:BackgroundSyncService 不再申请; module.json5 去掉 backgroundModes 与 ohos.permission.KEEP_BACKGROUND_RUNNING 2. **延迟任务周期 20 分钟 → 2 小时**(对齐系统下限,避免被压制);保留 isRepeat/isPersisted 3. **新增 BackgroundSyncRunner**:后台同步唯一执行体(抽取自 SyncWorkAbility), 供延迟任务/网络恢复/回到前台三处共用,避免逻辑漂移 4. **新增 NetworkSyncWatcher**:net.connection 监听,**从无网恢复到有网时立即补同步** (带 20s 去抖;仅在 netLost→netAvailable 时触发,单纯切网不重复同步) 5. **回到前台补同步**:UIAbility.onForeground 立即同步 + 取消短时任务归还配额 6. **退后台短时任务**:onBackground 用 requestSuspendDelay 兜住退后台那次同步; 回到前台 cancelSuspendDelay(配额仅 10 分钟/日,必须归还) 7. **设置页开关语义调整**:"后台持续同步(常驻通知)" → "后台自动同步", 文案如实说明"系统最短约 2 小时一次 + 回前台/网络恢复立即补同步"; 开关现在真正控制延迟任务的注册/取消 8. **官网文档同步**:sf/uh 改写后台同步说明;upa **移除 KEEP_BACKGROUND_RUNNING 权限行** (权限表必须与实际声明一致);已重建 404 并部署线上 注:applyEfficiencyResources 在当前 SDK 不可用,未采用。
This commit is contained in:
@@ -1,115 +1,152 @@
|
||||
// entry/src/main/ets/common/BackgroundSyncService.ets
|
||||
// 后台同步管理:
|
||||
// 1) 长时任务(DATA_TRANSFER):进程不被挂起,前台定时器在后台继续跑(需用户在设置中开启)
|
||||
// 2) 延迟任务(workScheduler):App 被杀/重启后由系统按条件拉起兜底同步(最小周期 20 分钟)
|
||||
// ⭐ 后台同步调度(2026-10-10 重构,依据《定时同步方案调研.md》)
|
||||
//
|
||||
// 为什么不再用长时任务(重要):
|
||||
// 旧版申请 `dataTransfer` 长时任务想让进程常驻,但系统对 DATA_TRANSFER 有**一致性校验**——
|
||||
// "超过 10 分钟未更新传输进度 → 任务被强制取消"。日历同步是"间歇跑、平时无进度",
|
||||
// 本质不符合"持续传输中"的语义 → 必被取消(常驻通知消失就是这个原因),
|
||||
// 且频繁触发校验失败可能被系统列入管控名单。故**彻底移除**。
|
||||
//
|
||||
// 现在的分层调度(每层各司其职):
|
||||
// ① 前台:Index 里的 setInterval 按用户设定间隔同步(最准,已在前台实现)
|
||||
// ② 退后台瞬间:**短时任务**(requestSuspendDelay)兜住"退后台后那一次"
|
||||
// ③ 长期后台:**延迟任务**(workScheduler,≥2 小时,对齐系统活跃分组下限)
|
||||
// ④ 网络恢复 / 回到前台:各补一次(性价比最高,见 NetworkSyncWatcher / Index.onForeground)
|
||||
//
|
||||
// ⚠️ 关键约束(踩过的坑,勿改):
|
||||
// - workScheduler 的 repeatCycleTime **不得低于 2 小时**:系统按"应用活跃分组"压制
|
||||
// (活跃≥2h / 经常使用≥4h / 常用≥24h / 极少使用≥48h / 受限·从未使用禁止调度),
|
||||
// 请求更短不报错但会被压制,形同虚设。
|
||||
// - WorkSchedulerExtensionAbility 单次回调**上限 2 分钟**,超时进程被杀。
|
||||
// - 短时任务配额:单日 10 分钟、单次最长 3 分钟(低电量 1 分钟)→ 只做短收尾,用完必须 cancel。
|
||||
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
|
||||
import { wantAgent, common } from '@kit.AbilityKit';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { workScheduler } from '@kit.BackgroundTasksKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { AppSettings } from './AppSettings';
|
||||
import { LogUtil } from './LogUtil';
|
||||
|
||||
const WORK_ID: number = 1001;
|
||||
/** 延迟任务周期:2 小时(系统"活跃分组"下限,不可再短) */
|
||||
const DEFERRED_CYCLE_MS: number = 2 * 60 * 60 * 1000;
|
||||
|
||||
export class BackgroundSyncService {
|
||||
/** 申请长时任务(DATA_TRANSFER):申请成功后进程在后台不被挂起,失败抛错由调用方提示用户 */
|
||||
static async startContinuousTask(context: common.UIAbilityContext): Promise<void> {
|
||||
try {
|
||||
const info: wantAgent.WantAgentInfo = {
|
||||
wants: [{
|
||||
bundleName: context.abilityInfo.bundleName,
|
||||
abilityName: 'EntryAbility'
|
||||
}],
|
||||
operationType: wantAgent.OperationType.START_ABILITY,
|
||||
requestCode: 0
|
||||
};
|
||||
const agent = await wantAgent.getWantAgent(info);
|
||||
await backgroundTaskManager.startBackgroundRunning(
|
||||
context, backgroundTaskManager.BackgroundMode.DATA_TRANSFER, agent);
|
||||
LogUtil.write('长时任务已申请:后台持续同步开启');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`长时任务申请失败: ${e.code} - ${e.message}`);
|
||||
throw new Error(`长时任务申请失败(错误码 ${e.code})`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止长时任务 */
|
||||
static async stopContinuousTask(context: common.UIAbilityContext): Promise<void> {
|
||||
try {
|
||||
await backgroundTaskManager.stopBackgroundRunning(context);
|
||||
LogUtil.write('长时任务已停止');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`长时任务停止失败: ${e.code} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
/** 当前短时任务的 requestId(0 = 无);退后台申请、回前台取消 */
|
||||
private static transientId: number = 0;
|
||||
|
||||
/**
|
||||
* 注册延迟任务兜底同步:无论长时任务开关与否都注册。
|
||||
* 条件:有任意网络即执行;周期重复;重启后自动恢复。
|
||||
* 注:repeatCycleTime 各版本下限不同(20 分钟 ~ 2 小时),先用 20 分钟,
|
||||
* 被系统拒绝(9700003 条件无效)时降级为 2 小时再试。
|
||||
* 注册延迟任务兜底同步:周期 2 小时、重启后自动恢复(isPersisted)。
|
||||
* 条件:有任意网络即执行。
|
||||
* ⚠️ 周期**不得低于 2 小时**(系统按"应用活跃分组"压制:活跃≥2h / 经常使用≥4h / 常用≥24h /
|
||||
* 极少使用≥48h / 受限·从未使用禁止调度)。请求更短不报错但会被压制,形同虚设。
|
||||
*/
|
||||
static scheduleDeferredSync(context: common.UIAbilityContext): void {
|
||||
const build = (cycleMs: number): workScheduler.WorkInfo => {
|
||||
const info: workScheduler.WorkInfo = {
|
||||
workId: WORK_ID,
|
||||
bundleName: context.abilityInfo.bundleName,
|
||||
abilityName: 'SyncWorkAbility',
|
||||
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY,
|
||||
isRepeat: true,
|
||||
isPersisted: true,
|
||||
repeatCycleTime: cycleMs
|
||||
};
|
||||
return info;
|
||||
const info: workScheduler.WorkInfo = {
|
||||
workId: WORK_ID,
|
||||
bundleName: context.abilityInfo.bundleName,
|
||||
abilityName: 'SyncWorkAbility',
|
||||
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY,
|
||||
isRepeat: true,
|
||||
isPersisted: true,
|
||||
repeatCycleTime: DEFERRED_CYCLE_MS
|
||||
};
|
||||
try {
|
||||
workScheduler.startWork(build(20 * 60 * 1000));
|
||||
LogUtil.write('延迟任务已注册:20 分钟兜底同步');
|
||||
workScheduler.startWork(info);
|
||||
LogUtil.write('延迟任务已注册:2 小时兜底同步(系统活跃分组下限)');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
if (e.code === 9700003) {
|
||||
try {
|
||||
workScheduler.startWork(build(2 * 60 * 60 * 1000));
|
||||
LogUtil.write('延迟任务降级注册:2 小时兜底同步');
|
||||
return;
|
||||
} catch (err2) {
|
||||
const e2 = err2 as BusinessError;
|
||||
LogUtil.write(`延迟任务注册失败(2h): ${e2.code} - ${e2.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogUtil.write(`延迟任务注册失败: ${e.code} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** App 启动时统一入口:注册延迟任务 + 按设置恢复长时任务(恢复失败静默,不影响启动) */
|
||||
static async initOnLaunch(context: common.UIAbilityContext): Promise<void> {
|
||||
BackgroundSyncService.scheduleDeferredSync(context);
|
||||
const enabled: boolean = await AppSettings.getBackgroundSync(context);
|
||||
if (enabled) {
|
||||
try {
|
||||
await BackgroundSyncService.startContinuousTask(context);
|
||||
} catch (err) {
|
||||
// 启动时恢复失败不阻塞 App:下次开关切换或重启再试
|
||||
}
|
||||
/** 取消延迟任务(用户关闭"后台自动同步"时调用) */
|
||||
static cancelDeferredSync(context: common.UIAbilityContext): void {
|
||||
const info: workScheduler.WorkInfo = {
|
||||
workId: WORK_ID,
|
||||
bundleName: context.abilityInfo.bundleName,
|
||||
abilityName: 'SyncWorkAbility',
|
||||
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY,
|
||||
isRepeat: true,
|
||||
isPersisted: true,
|
||||
repeatCycleTime: DEFERRED_CYCLE_MS
|
||||
};
|
||||
try {
|
||||
workScheduler.stopWork(info, true);
|
||||
LogUtil.write('延迟任务已取消(后台自动同步关闭)');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`延迟任务取消失败: ${e.code} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置页开关切换:失败抛错由页面提示并回滚 UI */
|
||||
static async setBackgroundSync(context: common.UIAbilityContext, enabled: boolean): Promise<void> {
|
||||
if (enabled) {
|
||||
// 先申请任务,成功才落盘设置(失败时设置页回滚开关)
|
||||
await BackgroundSyncService.startContinuousTask(context);
|
||||
await AppSettings.setBackgroundSync(context, true);
|
||||
} else {
|
||||
await AppSettings.setBackgroundSync(context, false);
|
||||
try {
|
||||
await BackgroundSyncService.stopContinuousTask(context);
|
||||
} catch (err) {
|
||||
// 停止失败(如任务本就不在运行)不影响设置保存
|
||||
}
|
||||
/**
|
||||
* ⭐ 退到后台时申请**短时任务**:让"刚退后台的这一次同步"能跑完。
|
||||
* 必须在**前台或退后台 5 秒内**调用,否则申请失败。
|
||||
* 单次最长 3 分钟(实际申请到的时长以返回值为准),单日总配额 10 分钟。
|
||||
*/
|
||||
static requestTransientSync(reason: string): void {
|
||||
if (BackgroundSyncService.transientId !== 0) {
|
||||
return; // 已有短时任务在跑,不重复申请(重复申请会浪费配额)
|
||||
}
|
||||
try {
|
||||
const info: backgroundTaskManager.DelaySuspendInfo =
|
||||
backgroundTaskManager.requestSuspendDelay(reason, (): void => {
|
||||
// 超时回调:配额用完,系统即将挂起;清状态,等下次前台/退后台再申请
|
||||
LogUtil.write('短时任务超时(配额用尽),后台同步将被挂起');
|
||||
BackgroundSyncService.transientId = 0;
|
||||
});
|
||||
BackgroundSyncService.transientId = info.requestId;
|
||||
LogUtil.write(`短时任务已申请:requestId=${info.requestId},可用 ${Math.round(info.actualDelayTime / 1000)} 秒`);
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`短时任务申请失败: ${e.code} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 回到前台时取消短时任务:不占用当日配额(很重要,配额只有 10 分钟) */
|
||||
static cancelTransientSync(): void {
|
||||
if (BackgroundSyncService.transientId === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
backgroundTaskManager.cancelSuspendDelay(BackgroundSyncService.transientId);
|
||||
LogUtil.write('短时任务已取消(回到前台,归还配额)');
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`短时任务取消失败: ${e.code} - ${e.message}`);
|
||||
}
|
||||
BackgroundSyncService.transientId = 0;
|
||||
}
|
||||
|
||||
/** 查询短时任务剩余时间(毫秒);无任务或无配额返回 0 */
|
||||
static async remainingTransientMs(): Promise<number> {
|
||||
if (BackgroundSyncService.transientId === 0) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return await backgroundTaskManager.getRemainingDelayTime(BackgroundSyncService.transientId);
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** App 启动时统一入口:按用户设置注册/不注册延迟任务兜底同步 */
|
||||
static async initOnLaunch(context: common.UIAbilityContext): Promise<void> {
|
||||
const enabled: boolean = await AppSettings.getBackgroundSync(context);
|
||||
if (enabled) {
|
||||
BackgroundSyncService.scheduleDeferredSync(context);
|
||||
} else {
|
||||
LogUtil.write('后台自动同步未开启:不注册延迟任务');
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置页开关切换:控制"后台自动同步"(延迟任务注册与否) */
|
||||
static async setBackgroundSync(context: common.UIAbilityContext, enabled: boolean): Promise<void> {
|
||||
await AppSettings.setBackgroundSync(context, enabled);
|
||||
if (enabled) {
|
||||
BackgroundSyncService.scheduleDeferredSync(context);
|
||||
} else {
|
||||
BackgroundSyncService.cancelDeferredSync(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user