有个iOS 应用Alarmy,能够使用LocalNotification 实现持续不断闹钟响铃,如何实现该功能

各位大神好,请教一个iOS LocalNotification的技术问题,翻遍苹果的官方技术文档,问遍各大人工智能,找遍各大论坛,写程序做各种验证,没有答案,想请大神帮忙从逆向工程的角度给予帮助。万分感谢!

有个iOS 应用Alarmy,能够使用LocalNotification 实现持续不断闹钟响铃,我想实现这个功能。

对Alarmy 做了仔细研究,发现在不联网(飞行模式),关闭应用后台刷新,使用应用程序管理器关闭该应用的情况下,依然能够实现在闹钟时间持续不断发消息通知响铃。

通过Xcode Device Console 分析iPhone系统消息,发现在设定某个闹钟,比如上午8:32,会在上午8:32 Post 40个LocalNotification(第1个 08:32:00 SpringBoard Posting notification id: 163B-B3EC),在同一时间点发2个通知,过3秒再发2个通知,等到40个LocalNotification 发完后,正好1分钟。接着又Post 第1个Local Notification(08:33:00 SpringBoard Posting notification id: 163B-B3EC),然后Modify第1个Local Notification(08:33:00 SpringBoard Modify notification id: 163B-B3EC),这样又Post 完40 个LocalNotification后,正好1分钟。只要用户不响应通知,通知会一直持续发出。

iOS 的User Notifications有两个触发LocalNotification通知的类:
UNTimeIntervalNotificationTrigger基于时间间隔触发通知,而UNCalendarNotificationTrigger基于日历事件(具体日期和时间)触发通知。

经反复写程序验证,推测该程序应该使用了UNTimeIntervalNotificationTrigger,该类可以设置一个定时器通知,init(timeInterval: TimeInterval, repeats: Bool) 方法,如果repeats 设为true,timeInterval 最小为60,单位为秒。符合Alarmy 1分钟后重复发送第一条消息通知,经写程序验证,只要不响应该通知,会持续不断发送。在Xcode Device Console 中跟踪Activity 和Message,发现Alarmy 的Posting notification 特征,先40条Posting notification,然后是同一notification id 的Posting notification和Modify notification成对出现,共79条。写程序验证发现UNTimeIntervalNotificationTrigger会先Posting notification,然后是同一notification id 的Posting notification和Modify notification成对出现。和Alarmy 的特征相符。但这个类只能指定从运行时刻起的时间间隔,使用场景为定时器,无法指定具体通知时间。
UNCalendarNotificationTrigger 类可以指定具体的通知时间,init(dateMatching: DateComponents, repeats: Bool)方法,如果repeats 设为true,则在指定的时间会再次出发通知。而且一个iOS应用最多可以发64个UNCalendarNotificationTrigger,经写程序验证,如果通知多于64个,会保留最后64个通知,前面的通知会被忽略。UNCalendarNotificationTrigger 只有Posting notification Activity特征,没有Modify notification Activity 特征。

至此,分析结果已走向死胡同。使用UNTimeIntervalNotificationTrigger 无法指定通知触发时间,而UNCalendarNotificationTrigger 无法做到1分钟后永远重复。

我对逆向工程不怎么在行,想请各位大神帮忙分析Alarmy 是如何做到在指定通知触发事件,且永远重复通知。是否使用了iOS 未公开的类和方法?以上分析也许有错误,仅供参考,希望不会误导。感谢!

以下两条Device Console Activity 的Message:
08:33:00.270797+0800 SpringBoard Posting notification id: 163B-B3EC; section: droom.sleepIfUCanFree; thread: C428-6833; category: ; timestamp: 2024-05-02 上午12:33:00 +0000; interruption-level: active; relevance-score: 0.00; filter-criteria: (null); actions: [ minimal: 0 (0 text), default: 0 (0 text) ]; destinations: [ {(
BulletinDestinationCoverSheet,
BulletinDestinationBanner,
BulletinDestinationNotificationCenter,
BulletinDestinationLockScreen
)} ]
08:33:00.297116+0800 SpringBoard Modify notification id: 163B-B3EC; section: droom.sleepIfUCanFree; thread: C428-6833; category: ; timestamp: 2024-05-02 上午12:33:00 +0000; interruption-level: active; relevance-score: 0.00; filter-criteria: (null); actions: [ minimal: 0 (0 text), default: 0 (0 text) ]; destinations: [ {(
)} ]

通知分几种类型的,不一定是本地推送实现的。有一种通知权限特别高,比如系统自带的闹钟就是这种权限,它用的是紧急推送,无视勿扰、飞行、静音等情况都能推送并且携带声音。

您说得很对

Critical. Urgent information about health and safety that directly impacts the person and demands their immediate attention. Critical notifications are extremely rare and typically come from governmental and public agencies or apps that help people manage their health or home.


This interruption level requires an approved entitlement. 这种通知,是需要苹果授权,我在Alarmy 做过验证,在勿扰模式下,Alarmy 发送的消息通知是不会点亮屏幕、不会播放声音。另外在Xcode Device Console 的Message 中,可以查到Alarmy(Bundle
ID:droom.sleepIfUCanFree)的; interruption-level: active,因此可以确定,不是使用Critical 类型

排除他用私有方法的可能,有没有可能它创建的是多个通知?比如晚上10点响,重复设置2分钟。如果按照通知保活大概是6-8秒,那么通知数量就是:2 * 60 / 6,那么各个通知的时间就是依次累加,直至结束。

如果是在响铃期间关闭了闹钟,那取消后续的通知,可以通过拦截通知进行取消。 其次可能它用了同一个group identifier管理不同通知identifier,如果重复时间过长,可以通过多个group identifier相关联。

总之闹钟这类说难不难,简单来说就是本地推送+本地数据库的项目。

非常感谢您的帮助和指点!

我前面讲得不够详细。Alarmy 创建了40条本地通知,每条的UUID 不同,到闹钟设定的时间,会每隔3秒发送2条,这2条通知为一组会响铃3秒,20组后,20组*3秒=1分钟,就把这40条通知发完了。接着又会发出第一组2条通知(通过查证为40条通知的前2条,因为UUID 相同),直至一分钟结束发送完40条通知,如此循环往复,在用户响应通知前永不停止。

您的建议,在关闭闹钟,可以通过拦截通知进行取消。我可以在Xcode Device Console 中跟踪Alarmy 的Activity 和Message,看是否有取消的操作。

最后,你提到的同一个group identifier 是什么?每一个通知只有一个identifiier,可以自定义,但一般都用UUID,Alarmy 就是用的UUID。我一般是创建64个元素的数组来保存UUID,在调用UNNotificationRequest() 创建每条消息的时候当入参。

再次感谢!

:zipper_mouth_face: id一样就是一分组。怎么拦截的话,你看看UNUserNotificationCenter.current()这下面就知道了。