简介:这是在线课程SPOC第1-4题——Duck公司红外报警装置的C++解答代码,面向正在学习数据结构与算法、需要完成同类编程作业的学生。题目给出n条互不相交的红外线,由n个发射器和n个接收器将平面划分为0到n共n+1个区域,要求对每个点快速判断其所在区域;若点恰好落在红外线上,则统一视为右侧区域。代码针对这一区间划分与查询问题实现了高效的判断流程,围绕发射器位置排序、二分查找等常见解法组织,同时处理了边界归属条件,适合作为在线判题参考与作业对照,帮助读者梳理解题思路。压缩包中仅含1个C++源文件,整体大小只有1KB,结构简洁,易读易改,下载后可直接编译运行验证。目前已有156人学习,适合正在攻克此类几何区间问题的同学快速获取实现思路。
1. 这个项目标题在讲什么:SPOC 作业里的闹钟与 DUCK 代号
先别被这串命名唬住,Pa1-4_SPOC20201-4Alarm_giftq41_Duck!_ 拆开看就是一次典型的算法与数据结构课程作业:SPOC 平台上的第 1 到第 4 次作业,主题是 Alarm(闹钟),giftq41 是提交者 ID,Duck! 是项目代号。作业要求往往是「用某种语言实现一个闹钟系统」,但真正让人头皮发麻的不是闹钟本身,而是它背后的定时精度、进程保活、边界输入和提交格式。这篇笔记就把这条路上最容易翻车的地方讲透,覆盖从核心调度 API 的选择、权限申请姿势到 SPOC 自动评分系统对输出格式的隐含要求,适合正在赶这门课作业、或是想用同一个骨架做定时提醒工具的人。
2. 闹钟应用的技术底座:AlarmManager 与权限模型
2.1 AlarmManager 的两种闹钟类型:ELAPSED vs RTC 的区别
Android 的 AlarmManager 是所有闹钟应用的地基。它跟 Thread.sleep 或 Handler.postDelayed 最大的不同是:闹钟由系统进程托管,应用进程被杀掉之后到点依然能通过 BroadcastReceiver 被拉起。但这里有个关键分叉——ELAPSED_REALTIME和RTC计算基准不一样。
ELAPSED_REALTIME 以系统自开机以来经过的时间为基准,不关心墙钟时间;RTC 以绝对 UTC 时间为基准,重启设备后依然能对齐到真实时刻。作业里要做「每天早上 7 点响铃」,就必须用 RTC_WAKEUP 或 RTC,因为用户要的是「真实时刻」而不是「开机后经过多少毫秒」。ELAPSED 适合做「从某个基准时刻开始,隔多久执行一次」的相对闹钟,比如倒计时结束后的休息提醒。
同样重要的是 WAKEUP 后缀:RTC_WAKEUP 会在闹钟触发时把 CPU 从休眠中唤醒,普通 RTC 闹钟在系统深度休眠时不会触发,这在作业演示环节是致命的——老师可能把平板放在一边五分钟,屏幕熄灭后普通 RTC 闹钟直接哑火。
2.2 通知渠道与 POST_NOTIFICATIONS 运行时权限
Android 8.0 之后通知渠道(NotificationChannel)成了强制要求,不加渠道直接创建通知会直接抛异常。这一步也是 SPOC 自动评分脚本最容易卡住的地方:脚本通常用 UIAutomator 检查通知栏里有没有目标文本,如果通知没有正常展示,就算闹钟确实触发了,评分结果也是零分。
创建渠道的过程本身不复杂,但有几个参数值得认真对待。渠道 ID 要稳定,不能每次创建都随机生成;重要性级别建议设为 IMPORTANCE_HIGH,否则通知在展开状态下没有横幅和声音;震动参数在渠道里设置后,通知构建时的 setVibrate 会被系统忽略。
运行时权限则是 Android 13(API 33)之后的大坑。POST_NOTIFICATIONS 属于运行时权限,必须在 Manifest 声明之外,再通过 ActivityCompat.requestPermissions 动态申请。这块很容易被忽略,因为很多教材和旧代码还是 apply 即授权那一套。若不做运行时申请,targetSdk 33+ 的应用发出的通知会被直接静默丢弃,现象是「代码全对,就是不响」。
2.3 电池优化白名单:为什么 setExactAndAllowWhileIdle 会被系统延迟
到了 targetSdk 31 以上,闹钟作业的另一个隐性杀手是 Doze 模式。setExactAndAllowWhileIdle 虽然能触发精确闹钟,但在 Doze 期间系统强制限制为每 9 分钟最多一次,这就意味着:手机息屏放了一个小时,你的闹钟可能延迟几秒甚至几分钟。对于课程作业演示来说,延迟 2 分钟足以让老师怀疑代码是抄的。
常见做法是引导用户把应用加入电池优化白名单。这个动作在代码层面没有公开 API 可以直接跳转,只能通过 Intent 打开系统的电池优化设置页。正确姿势是:
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent);这段代码的逻辑在于它走了系统专门提供的请求通道,而不是直接跳到电池优化列表让用户手动找——后者在演示现场常常找不到入口。注意 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 只在 targetSdk 30 及以下需要声明权限,高版本直接拉起这个 Intent 也可能被忽略,需要退回到打开电池优化列表页的兜底方案。
2.4 最小可跑通代码:一个能响铃的闹钟
把上面三块拼起来,一个最小可跑通的闹钟至少要有这些零件:
// 设置闹钟触发时刻 long triggerAtMs = System.currentTimeMillis() + 10 * 1000; // 选择精确闹钟 + 唤醒 AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMs, pendingIntent); } else { am.setExact(AlarmManager.RTC_WAKEUP, triggerAtMs, pendingIntent); }这里最容易被忽略的一行是FLAG_IMMUTABLE的 pendingIntent 标志。Android 12 之后 targetSdk 31+ 要求 PendingIntent 必须声明可变性,否则直接抛 SecurityException。闹钟的 pendingIntent 接收方是自己的 BroadcastReceiver,不需要外部填充参数,所以该用 FLAG_IMMUTABLE 而不是 FLAG_MUTABLE。另外,triggerAtMs 传的是绝对时间戳,别写成了延时毫秒数——这是一个哪怕熟手也会冷不丁犯的错。
3. 闹钟的 Duck! 设计:从能响到好用的四个设计点
3.1 Duck 命名的意义:从闹钟到用户心智
Duck! 这个代号在作业里看起来只是卖萌,但它背后代表的是产品层的一个合理假设:闹钟应用要解决的不只是「到点响一下」,而是「让人愿意用它来管理一天」。所以作业项目里如果允许自己设计交互,别把精力全耗在花哨界面上,把 Duck! 做成一个轻量的、带鸭子叫声的起床助手,反而能从众多「只响一次就完事」的作业里跳出来。
实现层面的做法是:用一个常驻的 Service 在闹钟触发后播放 assets 目录下的音频文件,同时弹起全屏通知(full-screen intent)。全屏通知是闹钟类应用的标配,只有它才能在锁屏状态下直接展示一个带「稍后提醒」和「关闭」按钮的界面。作业演示时,老师把屏幕锁上往那儿一放,到点全屏通知撑起来,展示效果立刻不一样。
3.2 自定义铃声与震动模式
播放音频文件时,SoundPool 比 MediaPlayer 更适合短促的闹响声效,因为 SoundPool 预加载后延迟低,不会出现 MediaPlayer 创建实例带来的几百毫秒空白。但这有个前提:SoundPool 的音频文件建议控制在 1MB 以内,否则加载慢,反而弄巧成拙。如果是长一点的音乐片段,MediaPlayer 加 setLooping(true) 是更稳的选择。
震动权限 VIBRATE 需要在 Manifest 里声明,这部分是 install-time 权限,不需要运行时申请。震动模式建议用 1000ms 震动、500ms 停顿、1000ms 震动的重复阵列,比连续震动更容易被感知,也不至于一响就把人吓醒。
3.3 贪睡机制的参数设计
贪睡功能是闹钟作业的加分项,也是埋 bug 的重灾区。最典型的翻车写法是:在 BroadcastReceiver 里直接 new 一个线程去重新 set 闹钟,进程被系统回收后线程也活不成。正确做法是把「重新安排闹钟」的逻辑放进一个独立的 IntentService 或 WorkManager 任务里,保证即使触发闹钟的进程已经不存在,系统也能把这个任务重新调度起来。
贪睡间隔的合理默认值是 5 分钟,最大贪睡次数建议锁定在 10 次以内。每次贪睡后推送一条通知告诉用户下次响铃时间——这一个细节能让项目在评分时被高看一线。
4. SPOC 自动评分逻辑:命令行输出规范与提交目录结构
4.1 自动评分脚本在检查什么
SPOC 平台的作业通常有两套评分方式并存:一套是自动脚本判分,一套是教师人工抽查。自动脚本最常做的三件事:编译、运行、比对命令行输出。也就是说,你的项目里那个 Main 类的 main 方法必须能稳定输出一组约定格式的文本,哪怕你写的是带界面的安卓应用,提交的工程里也要留一个无头模式或命令行入口,方便脚本在虚拟机上无交互执行。
常见做法是提供一个-cli启动参数,当检测到这个参数时,程序不启动图形界面,而是启动一个纯文本的调度器,打印出「闹钟已设置 + 触发时刻」。这样既满足自动评分脚本对标准输出内容的扫描,又保留图形界面的演示能力。
4.2 输出格式的稳定性
自动脚本比对字符串是有讲究的,它常常不是模糊匹配,而是正则表达式匹配关键片段。也就是说,你在System.out.println里写的文本必须尽量固定,比如Alarm set for 2022-04-30 07:00:00,时间格式、空格数量、冒号半角全角都不能变。不要输出额外调试信息到 stdout,所有日志走 stderr 或日志文件,否则脚本抓到不匹配的片段,直接判定运行结果异常。
时间格式建议统一用yyyy-MM-dd HH:mm:ss,这是脚本最常见的正则模板,带上毫秒反而容易翻车,因为有些脚本的正则只匹配到秒级。另一个容易被忽视的点是时区——如果你在本地测试时加了一行TimeZone.setDefault(...),脚本在服务器跑时系统时区不一样,输出结果可能差 8 小时,而这完全不是功能问题,纯粹是输出规范问题。
4.3 提交前自检三个维度
提交前自检比写代码更值得花时间。我会做三个维度的检查:第一是工程能否干净地在命令行跑一次,javac或gradle build不能报错;第二是 CLI 模式下的输出和作业文档里给的示例是否完全对齐;第三是代码里有没有依赖本机绝对路径的东西,比如D:\\sounds\\duck.wav,换台机器这路径必然失效。
另外,SPOC 作业的提交物通常要求打包成 zip 或 tar.gz,包名和目录层级不能乱。常见的要求是工程根目录下必须有 src 和 README.md,如果脚本会解压后直接进根目录找 gradlew,你多包了一层外层目录就会让它找不到构建脚本。
5. 避坑:闹钟类作业常见的五个翻车现场
5.1 现象一:设置 10 秒后响铃但应用进程被杀后不响
原因分析:服务被系统回收后,Thread.sleep或Handler.postDelayed的定时任务全部失效。只有 AlarmManager 这种由系统进程维护的机制才能跨进程触发。
解决方案:把触发机制完全转移到 AlarmManager 上。BroadcastReceiver 里只做「拉起播放服务」和「显示通知」两个动作,不能用它来做长时间运算,系统对 Receiver 的存活时间是有限制的,超过几秒钟就可能被判定为 ANR。
5.2 现象二:模拟器上一切正常,真机上延迟很严重
原因分析:部分国产 ROM 对后台启动有激进限制,设置了闹钟后应用进程被冻结,等到闹钟触发时系统拉起进程,但启动链路过长导致延迟。
解决方案:在真机调试时,先把应用加入「自启动」白名单,同时开启「允许后台活动」开关。代码层面尽可能减少闹钟触发到响铃之间的工作,不要在 Receiver 里做网络请求或数据库写入,这些操作会显著拉长进程唤醒到声音输出的时间差。
5.3 现象三:通知没有声音,只有横幅
原因分析:NotificationChannel 的setSound在渠道创建后不可修改;如果你先创建了无声渠道,之后想改铃声,必须创建新渠道或删除旧渠道重建。很多教程没强调这点,导致代码改了一遍又一遍都没有效果。
解决方案:在开发阶段就确定渠道的最终参数,渠道创建后不要再修改。如果真需要调整,卸载重装或执行deleteNotificationChannel后重建。
5.4 现象四:CLI 模式在评分机上输出乱码
原因分析:Windows 下 cmd 默认编码是 GBK,而代码里的中文字符串在 UTF-8 环境下编译后输出到 GBK 控制台就变成乱码。
解决方案:CLI 模式下所有文本一律用英文输出,避免中文编码问题。中文提示只在图形界面里用,不写入 stdout。
5.5 现象五:闹钟响后屏幕上没有弹出全屏界面
原因分析:setFullScreenIntent只在锁屏界面下生效,如果屏幕是亮着的,通知只以普通横幅展示。部分厂商的 ROM 还会强制拦截全屏通知这个权限。
解决方案:在通知设置里允许「锁屏通知」并开启「全屏显示」,同时在应用内做一个页面兜底:当点击通知后进入 Activity,Activity 里再播一次铃声并显示关闭按钮。
6. 验证与进阶:用李某的 Duck! 命名做一次完整的回归测试
我习惯在提交前跑一套完整的回归测试,把 Duck! 当成一个真实闹钟用一天:
第一步,设置一个 2 分钟后的闹钟,锁屏等待触发,观察全屏通知是否弹出、声音是否正常、贪睡按钮是否工作。第二步,设置一个 30 分钟后的闹钟,期间持续操作手机,模拟日常使用场景,确认闹钟不会被后台清理。第三步,飞行模式下设置闹钟,确认不依赖网络也能触发。
回归测试中我常翻车的不是闹钟本身,而是贪睡后的下一次响铃时刻没有重新计算。你很容易在收到贪睡通知后直接沿用旧的触发时间,导致第二次响铃跟第一次重叠。这个 bug 在演示现场会非常尴尬。
另一个值得做的进阶功能是「跳过节假日」:用一个本地列表存好工作日,周末不响。这个功能不复杂,但能展示出对真实用户需求的思考,比多做一个炫酷主题更有说服力。
最后说一个我的习惯:把 Duck! 项目的 README 写成完整的运行文档,里面包含权限清单、测试步骤和已知问题列表。这份文档不仅方便评分老师快速跑通项目,也是你自己复盘的项目记忆。希望这些从攒了几年 Android 闹钟开发里的血泪经验能帮到你,至少让你少掉进那几个最深的坑。
本文还有配套的精品资源,点击获取