Files
digital-pilates/services/backgroundTaskManagerV2.ts
richarjiang d74046498d # 分析方案
## 变更内容总结
1. **iOS后台任务系统重构** - 修复后台任务无法自动运行的问题
2. **日志系统优化** - 改进日志记录机制,添加队列和批量写入
3. **文档新增** - 添加后台任务修复总结和测试指南文档
4. **应用启动优化** - 添加后台任务状态检查和恢复逻辑
5. **版本号更新** - Info.plist版本从1.0.23升级到1.0.24

## 提交信息类型判断
- **主要类型**: `fix` - 这是一个重要的bug修复,解决了iOS后台任务无法自动运行的核心问题
- **作用域**: `ios-background` - 专注于iOS后台任务功能
- **影响**: 这个修复对iOS用户的后台功能至关重要

## 提交信息

fix(ios-background): 修复iOS后台任务无法自动运行的问题

主要修复内容:
- 修复BackgroundTaskBridge任务调度逻辑,改用BGAppRefreshTaskRequest
- 添加任务完成后自动重新调度机制,确保任务持续执行
- 优化应用生命周期管理,移除重复的后台任务调度
- 在应用启动时添加后台任务状态检查和恢复功能
- 将默认任务间隔从30分钟优化为15分钟

次要改进:
- 重构日志系统,添加内存队列和批量写入机制,提升性能
- 添加写入锁和重试机制,防止日志数据丢失
- 新增详细的修复总结文档和测试指南

技术细节:
- 使用BGAppRefreshTaskRequest替代BGProcessingTaskRequest
- 实现任务过期自动重新调度
- 添加任务执行状态监控和恢复逻辑
- 优化错误处理和日志输出

影响范围: iOS后台任务调度、通知推送、应用状态管理
2025-11-04 19:14:53 +08:00

621 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NativeEventEmitter, NativeModules, Platform, type EmitterSubscription } from 'react-native';
import { listChallenges } from '@/services/challengesApi';
import { resyncFastingNotifications } from '@/services/fastingNotifications';
import { store } from '@/store';
import { selectActiveFastingPlan, selectActiveFastingSchedule } from '@/store/fastingSlice';
import { getWaterIntakeFromHealthKit } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
import dayjs from 'dayjs';
export const BACKGROUND_TASK_IDENTIFIER = 'com.anonymous.digitalpilates.task';
const DEFAULT_RESCHEDULE_INTERVAL_SECONDS = 60 * 15; // 15 minutes
const BACKGROUND_EVENT = 'BackgroundTaskBridge.execute';
const EXPIRATION_EVENT = 'BackgroundTaskBridge.expire';
const NativeBackgroundModule = NativeModules.BackgroundTaskBridge;
const isIosBackgroundModuleAvailable = Platform.OS === 'ios' && NativeBackgroundModule;
// 检查通知权限
async function checkNotificationPermissions(): Promise<boolean> {
try {
const Notifications = await import('expo-notifications');
const { status } = await Notifications.getPermissionsAsync();
return status === 'granted';
} catch (error) {
console.error('检查通知权限失败:', error);
return false;
}
}
// 执行喝水提醒后台任务
async function executeWaterReminderTask(): Promise<void> {
try {
console.log('执行喝水提醒后台任务...');
let state;
try {
state = store.getState();
} catch (error) {
console.log('无法获取 Redux state使用本地存储:', error);
const dailyGoal = await getWaterGoalFromStorage();
if (!dailyGoal || dailyGoal <= 0) {
console.log('没有设置喝水目标,跳过喝水提醒');
return;
}
await sendSimpleWaterReminder();
return;
}
const waterStats = state.water?.todayStats;
const userProfile = state.user?.profile;
let dailyGoal = waterStats?.dailyGoal ?? 0;
if (!dailyGoal || dailyGoal <= 0) {
dailyGoal = await getWaterGoalFromStorage();
}
if (!dailyGoal || dailyGoal <= 0) {
console.log('没有设置喝水目标,跳过喝水提醒');
return;
}
const currentHour = new Date().getHours();
const userName = userProfile?.name || '朋友';
const todayRange = {
startDate: dayjs().startOf('day').toDate().toISOString(),
endDate: dayjs().endOf('day').toDate().toISOString()
};
let totalAmount = waterStats?.totalAmount ?? 0;
let completionRate = waterStats?.completionRate ?? (dailyGoal > 0 ? (totalAmount / dailyGoal) * 100 : 0);
try {
const healthKitRecords = await getWaterIntakeFromHealthKit(todayRange);
if (Array.isArray(healthKitRecords) && healthKitRecords.length > 0) {
totalAmount = healthKitRecords.reduce((sum: number, record: unknown) => {
if (record && typeof record === 'object' && 'value' in record) {
const { value } = record as { value?: number | string };
const numericValue = Number(value ?? 0);
return Number.isFinite(numericValue) ? sum + numericValue : sum;
}
return sum;
}, 0);
completionRate = Math.min((totalAmount / dailyGoal) * 100, 100);
} else {
console.log('HealthKit 未返回今日饮水记录,使用应用内缓存数据');
}
} catch (healthKitError) {
console.error('从HealthKit获取饮水记录失败使用应用内缓存数据:', healthKitError);
}
const todayWaterStats = {
totalAmount,
dailyGoal,
completionRate: Number.isFinite(completionRate) ? completionRate : 0
};
const notificationSent = await WaterNotificationHelpers.checkWaterGoalAndNotify(
userName,
todayWaterStats,
currentHour
);
if (notificationSent) {
console.log('后台喝水提醒通知已发送');
await AsyncStorage.setItem('@last_background_water_check', Date.now().toString());
} else {
console.log('无需发送后台喝水提醒通知');
}
} catch (error) {
console.error('执行喝水提醒后台任务失败:', error);
}
}
async function executeChallengeReminderTask(): Promise<void> {
try {
console.log('执行挑战鼓励提醒后台任务...');
let userName = '朋友';
try {
const state = store.getState();
const normalizedUserName = state.user?.profile?.name?.trim();
userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友';
} catch (error) {
console.log('无法获取用户名,使用默认值:', error);
}
const challenges = await listChallenges();
const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress);
if (!joinedChallenges.length) {
console.log('没有加入的挑战或挑战没有进度,跳过挑战提醒');
return;
}
const todayKey = new Date().toISOString().slice(0, 10);
const eligibleChallenges = [];
for (const challenge of joinedChallenges) {
const progress = challenge.progress;
if (!progress || progress.checkedInToday) {
continue;
}
const storageKey = `@challenge_encouragement_sent:${challenge.id}`;
const lastSent = await AsyncStorage.getItem(storageKey);
if (lastSent === todayKey) {
continue;
}
eligibleChallenges.push(challenge);
}
if (eligibleChallenges.length > 0) {
const randomIndex = Math.floor(Math.random() * eligibleChallenges.length);
const selectedChallenge = eligibleChallenges[randomIndex];
try {
await ChallengeNotificationHelpers.sendEncouragementNotification({
userName,
challengeTitle: selectedChallenge.title,
challengeId: selectedChallenge.id,
});
const storageKey = `@challenge_encouragement_sent:${selectedChallenge.id}`;
await AsyncStorage.setItem(storageKey, todayKey);
console.log(`已随机选择并发送挑战鼓励通知: ${selectedChallenge.title}`);
} catch (notificationError) {
console.error('发送挑战鼓励通知失败:', notificationError);
}
} else {
console.log('没有符合条件的挑战需要发送鼓励通知');
}
console.log('挑战鼓励提醒后台任务完成');
} catch (error) {
console.error('执行挑战鼓励提醒后台任务失败:', error);
}
}
// 执行断食通知后台任务
async function executeFastingNotificationTask(): Promise<void> {
try {
console.log('执行断食通知后台任务...');
let state;
try {
state = store.getState();
} catch (error) {
console.log('无法获取 Redux state跳过断食通知任务:', error);
return;
}
const activeSchedule = selectActiveFastingSchedule(state);
const activePlan = selectActiveFastingPlan(state);
if (!activeSchedule || !activePlan) {
console.log('没有激活的断食计划,跳过断食通知任务');
return;
}
const end = dayjs(activeSchedule.endISO);
if (end.isBefore(dayjs())) {
console.log('断食计划已结束,跳过断食通知任务');
return;
}
console.log('正在同步断食通知...', {
planId: activePlan.id,
start: activeSchedule.startISO,
end: activeSchedule.endISO,
});
await resyncFastingNotifications({
schedule: activeSchedule,
plan: activePlan,
enabled: true,
});
console.log('断食通知后台同步完成');
} catch (error) {
console.error('执行断食通知后台任务失败:', error);
}
}
// 发送测试通知以验证后台任务执行
async function sendTestNotification(): Promise<void> {
try {
console.log('发送后台任务测试通知...');
const Notifications = await import('expo-notifications');
await Notifications.scheduleNotificationAsync({
content: {
title: '后台任务测试',
body: `后台任务正在执行中... 时间: ${new Date().toLocaleTimeString()}`,
data: {
type: 'background_task_test',
timestamp: Date.now()
}
},
trigger: null,
});
console.log('后台任务测试通知发送成功');
await AsyncStorage.setItem('@last_background_test_notification', Date.now().toString());
} catch (error) {
console.error('发送测试通知失败:', error);
}
}
async function sendSimpleWaterReminder(): Promise<void> {
try {
const userName = '朋友';
const Notifications = await import('expo-notifications');
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: '💧 该喝水啦!',
body: `${userName},记得补充水分,保持身体健康~`,
data: {
type: 'water_reminder',
url: '/statistics'
},
sound: 'default',
},
trigger: null,
});
console.log('简单喝水提醒已发送ID:', notificationId);
} catch (error) {
console.error('发送简单喝水提醒失败:', error);
}
}
async function executeBackgroundTasks(): Promise<void> {
console.log('开始执行后台任务...');
try {
const hasPermission = await checkNotificationPermissions();
if (!hasPermission) {
console.log('没有通知权限,跳过后台任务');
return;
}
try {
const state = store.getState();
if (!state) {
console.log('Redux store 未初始化,跳过后台任务');
return;
}
} catch (error) {
console.log('无法访问 Redux store跳过后台任务:', error);
return;
}
const testNotificationsEnabled = await AsyncStorage.getItem('@background_test_notifications_enabled') === 'true';
if (testNotificationsEnabled) {
await sendTestNotification();
}
await executeWaterReminderTask();
await executeChallengeReminderTask();
await executeFastingNotificationTask();
console.log('后台任务执行完成');
} catch (error) {
console.error('执行后台任务失败:', error);
throw error;
}
}
export class BackgroundTaskManagerV2 {
private static instance: BackgroundTaskManagerV2;
private isInitialized = false;
private executingPromise: Promise<void> | null = null;
private eventSubscription?: EmitterSubscription;
private expirationSubscription?: EmitterSubscription;
static getInstance(): BackgroundTaskManagerV2 {
if (!BackgroundTaskManagerV2.instance) {
BackgroundTaskManagerV2.instance = new BackgroundTaskManagerV2();
}
return BackgroundTaskManagerV2.instance;
}
async initialize(): Promise<void> {
if (this.isInitialized) {
logger.info('[BackgroundTaskManagerV2] 后台任务管理器已初始化,跳过重复初始化');
return;
}
if (!isIosBackgroundModuleAvailable) {
logger.warn('[BackgroundTaskManagerV2] iOS 原生后台模块不可用,跳过初始化');
this.isInitialized = false;
return;
}
const emitter = new NativeEventEmitter(NativeBackgroundModule);
this.eventSubscription = emitter.addListener(BACKGROUND_EVENT, (payload) => {
logger.info('[BackgroundTaskManagerV2] 收到后台任务事件', payload);
this.handleBackgroundExecution();
});
this.expirationSubscription = emitter.addListener(EXPIRATION_EVENT, (payload) => {
logger.warn('[BackgroundTaskManagerV2] 后台任务在完成前即将过期', payload);
// 处理任务过期情况,确保重新调度
this.handleTaskExpiration();
});
try {
// 检查后台刷新状态
const status = await this.getStatus();
logger.info('[BackgroundTaskManagerV2] 后台刷新状态:', status);
if (status === 'denied' || status === 'restricted') {
logger.warn('[BackgroundTaskManagerV2] 后台刷新被限制或拒绝,后台任务可能无法正常工作');
// 不抛出错误,但标记为未完全初始化
this.isInitialized = false;
return;
}
await NativeBackgroundModule.configure({
identifier: BACKGROUND_TASK_IDENTIFIER,
taskType: 'refresh',
requiresNetworkConnectivity: false,
requiresExternalPower: false,
defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS,
});
this.isInitialized = true;
logger.info('[BackgroundTaskManagerV2] 已初始化并注册 iOS 后台任务');
// 立即调度一次后台任务
await this.scheduleNextTask();
// 检查待处理的任务请求
const pendingRequests = await this.getPendingRequests();
logger.info('[BackgroundTaskManagerV2] 当前待处理的任务请求数量:', pendingRequests.length);
} catch (error: any) {
// BGTaskSchedulerErrorDomain 错误码 1 表示后台任务功能不可用
// 这在模拟器上是正常的,因为模拟器不完全支持后台任务
const errorMessage = error?.message || String(error);
const isBGTaskUnavailable = errorMessage.includes('BGTaskSchedulerErrorDomain') &&
(errorMessage.includes('错误1') || errorMessage.includes('code 1'));
if (isBGTaskUnavailable) {
logger.warn('[BackgroundTaskManagerV2] 后台任务功能在当前环境不可用(模拟器限制),将在真机上正常工作');
this.removeListeners();
this.isInitialized = false;
// 不抛出错误,因为这是预期行为
return;
}
// 其他错误情况,尝试恢复
logger.error('[BackgroundTaskManagerV2] 初始化失败,尝试恢复', error);
try {
// 尝试重新初始化一次
await this.attemptRecovery();
} catch (recoveryError) {
logger.error('[BackgroundTaskManagerV2] 恢复失败,放弃初始化', recoveryError);
this.removeListeners();
throw error;
}
}
}
async scheduleNextTask(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
return;
}
try {
await NativeBackgroundModule.schedule({
delay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS
});
logger.info('[BackgroundTaskManagerV2] 已调度下一次后台任务');
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 调度后台任务失败', error);
}
}
private async handleBackgroundExecution(): Promise<void> {
if (this.executingPromise) {
logger.info('[BackgroundTaskManagerV2] 已有后台任务在执行,忽略重复触发');
return;
}
const startTime = Date.now();
logger.info('[BackgroundTaskManagerV2] 开始执行后台任务');
this.executingPromise = executeBackgroundTasks()
.then(async () => {
const executionTime = Date.now() - startTime;
logger.info(`[BackgroundTaskManagerV2] 后台任务执行成功,耗时: ${executionTime}ms`);
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.complete(true, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
logger.info('[BackgroundTaskManagerV2] 已标记后台任务成功完成并重新调度');
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 标记后台任务成功完成失败', error);
// 即使标记失败,也尝试手动重新调度
await this.scheduleNextTask();
}
}
})
.catch(async (error) => {
const executionTime = Date.now() - startTime;
logger.error(`[BackgroundTaskManagerV2] 后台任务执行失败,耗时: ${executionTime}ms`, error);
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.complete(false, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
logger.info('[BackgroundTaskManagerV2] 已标记后台任务失败并重新调度');
} catch (completionError) {
logger.error('[BackgroundTaskManagerV2] 标记后台任务失败状态时出错', completionError);
// 即使标记失败,也尝试手动重新调度
await this.scheduleNextTask();
}
}
})
.finally(() => {
this.executingPromise = null;
});
await this.executingPromise;
}
private async handleTaskExpiration(): Promise<void> {
logger.warn('[BackgroundTaskManagerV2] 处理后台任务过期');
// 任务过期时,确保重新调度下一次任务
try {
await this.scheduleNextTask();
logger.info('[BackgroundTaskManagerV2] 已为过期的任务重新调度');
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 为过期任务重新调度失败', error);
}
}
private async attemptRecovery(): Promise<void> {
logger.info('[BackgroundTaskManagerV2] 尝试恢复后台任务功能');
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 2000));
// 取消所有现有任务
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.cancelAll();
logger.info('[BackgroundTaskManagerV2] 已取消所有现有后台任务');
} catch (error) {
logger.warn('[BackgroundTaskManagerV2] 取消现有任务失败', error);
}
}
// 重新配置
await NativeBackgroundModule.configure({
identifier: BACKGROUND_TASK_IDENTIFIER,
taskType: 'refresh',
requiresNetworkConnectivity: false,
requiresExternalPower: false,
defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS,
});
logger.info('[BackgroundTaskManagerV2] 后台任务功能恢复成功');
}
async stop(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
return;
}
try {
await NativeBackgroundModule.cancelAll();
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 停止后台任务失败', error);
} finally {
this.removeListeners();
this.isInitialized = false;
}
}
private removeListeners(): void {
this.eventSubscription?.remove();
this.expirationSubscription?.remove();
this.eventSubscription = undefined;
this.expirationSubscription = undefined;
}
async getStatus(): Promise<string> {
if (!isIosBackgroundModuleAvailable) {
return Platform.OS;
}
try {
const status = await NativeBackgroundModule.backgroundRefreshStatus();
return status;
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 获取后台任务状态失败', error);
return 'unknown';
}
}
async checkStatus(): Promise<string> {
const status = await this.getStatus();
switch (status) {
case 'available':
return '可用';
case 'restricted':
return '受限制';
case 'denied':
return '被拒绝';
default:
return '未知';
}
}
async triggerTaskForTesting(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
await executeBackgroundTasks();
return;
}
try {
await NativeBackgroundModule.simulateLaunch();
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 模拟后台任务触发失败', error);
throw error;
}
}
async testBackgroundTask(): Promise<void> {
await this.triggerTaskForTesting();
}
async getLastBackgroundCheckTime(): Promise<number | null> {
try {
const lastCheck = await AsyncStorage.getItem('@last_background_water_check');
return lastCheck ? parseInt(lastCheck, 10) : null;
} catch (error) {
console.error('获取最后后台检查时间失败:', error);
return null;
}
}
async getPendingRequests(): Promise<any[]> {
if (!isIosBackgroundModuleAvailable) {
return [];
}
try {
const requests = await NativeBackgroundModule.getPendingRequests();
return Array.isArray(requests) ? requests : [];
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 获取待处理的后台任务请求失败', error);
return [];
}
}
}
export type BackgroundTaskEvent = {
taskId: string;
timestamp: number;
success: boolean;
error?: string;
};
export const BackgroundTaskManager = BackgroundTaskManagerV2;