Expo/React Native 技术问询:如何在应用后台状态下播放定时提醒音(闹钟应用场景)
我之前在Expo托管项目里做过类似的闹钟应用,正好碰到过同样的问题!下面这个基于Expo官方工具的方案应该能解决你的需求,完全不需要eject:
核心解决方案:Expo Task Manager + Notifications + Audio 组合
Expo默认会暂停后台JS线程,但原生级别的定时任务和通知不受此限制,我们可以通过这三个官方库实现可靠的后台闹钟提醒:
1. 先配置项目依赖与权限
首先安装所需库(Expo项目无需额外原生配置):
expo install expo-task-manager expo-notifications expo-audio
然后在app.json中添加权限和资源配置:
{ "expo": { "ios": { "infoPlist": { "UIBackgroundModes": ["audio", "remote-notification"] } }, "android": { "permissions": ["POST_NOTIFICATIONS", "WAKE_LOCK"], "useNextNotificationsApi": true }, "plugins": [ [ "expo-notifications", { "icon": "./assets/notification-icon.png", "color": "#ffffff", "sounds": ["./assets/alarm-sound.wav"] } ] ] } }
2. 注册后台任务处理闹钟触发
创建一个原生级别的后台任务,确保到达时间后即使应用在后台/未运行,也能播放提醒音:
import * as TaskManager from 'expo-task-manager'; import * as Notifications from 'expo-notifications'; import * as Audio from 'expo-audio'; // 定义唯一的任务标识 const ALARM_TASK = 'ALARM_TRIGGER_TASK'; // 编写后台任务逻辑 TaskManager.defineTask(ALARM_TASK, async ({ data, error }) => { if (error) { console.error('闹钟任务执行失败:', error); return; } // 配置音频模式,确保后台也能播放 await Audio.setAudioModeAsync({ staysActiveInBackground: true, playsInSilentModeIOS: true, shouldDuckAndroid: false, }); // 播放提醒音(循环直到用户关闭) const sound = new Audio.Sound(); try { await sound.loadAsync(require('./assets/alarm-sound.wav')); await sound.playAsync(); sound.setIsLoopingAsync(true); } catch (err) { console.error('播放提醒音失败:', err); } }); // 初始化任务与权限 async function initAlarmService() { // 请求通知权限 const notificationPerm = await Notifications.getPermissionsAsync(); if (!notificationPerm.granted) { await Notifications.requestPermissionsAsync(); } // 注册后台任务 await TaskManager.registerTaskAsync(ALARM_TASK); } // 在App组件初始化时调用 useEffect(() => { initAlarmService(); }, []);
3. 调度定时闹钟
替换你原来的setTimeout逻辑,用原生定时触发任务和通知:
async function scheduleAlarm(ringsInMilliseconds) { // 1. 调度系统通知(确保用户能看到提醒,同时触发声音) await Notifications.scheduleNotificationAsync({ content: { title: '闹钟提醒', body: '该起床啦!', sound: 'alarm-sound.wav', // 对应app.json配置的声音文件 data: { type: 'alarm' }, }, trigger: { seconds: ringsInMilliseconds / 1000, repeats: false, // 重复闹钟可设为true }, }); // 2. 调度后台任务(双重保障,防止通知被用户忽略) await TaskManager.scheduleTaskAsync(ALARM_TASK, { delay: ringsInMilliseconds, period: null, }); }
4. 处理通知点击跳转页面
当用户点击通知时,自动打开闹钟响铃页面:
useEffect(() => { const notificationListener = Notifications.addNotificationResponseReceivedListener(response => { const data = response.notification.request.content.data; if (data.type === 'alarm') { navigation.push('AlarmRings'); } }); return () => notificationListener.remove(); }, [navigation]);
5. 在AlarmRings页面控制声音
响铃页面负责停止提醒音:
import { useRef } from 'react'; import { View, Text, Button } from 'react-native'; import * as Audio from 'expo-audio'; function AlarmRings({ navigation }) { const soundRef = useRef(null); useEffect(() => { // 检查是否已有声音在播放,统一控制 async function checkAndControlSound() { const { playing } = await Audio.getAudioModeAsync(); if (playing) { // 获取当前播放的声音实例(或直接初始化新的) soundRef.current = new Audio.Sound(); await soundRef.current.loadAsync(require('./assets/alarm-sound.wav')); } } checkAndControlSound(); // 组件卸载时停止声音 return () => { if (soundRef.current) { soundRef.current.stopAsync(); soundRef.current.unloadAsync(); } }; }, []); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 24, marginBottom: 20 }}>闹钟响了!</Text> <Button title="关闭闹钟" onPress={() => navigation.goBack()} /> </View> ); }
关键注意事项
- iOS限制:后台任务执行时间有限,但系统通知的声音是系统级别的,即使应用未运行也会播放;我们的后台任务用于保障声音循环播放。
- Android厂商限制:部分国产手机有严格的后台杀进程策略,可能需要引导用户将应用加入后台白名单。
- 不要用setTimeout:JS线程在后台会被暂停,只有原生级别的任务才能可靠触发。
内容的提问来源于stack exchange,提问作者janaabf
相关产品推荐
相关产品推荐

