4

注意:在编辑的更下方,有一些简单的代码会产生问题,而没有我的原始程序的全部复杂性。

我正在尝试为越狱的 iOS 编写一个闹钟应用程序。我有一个 UI 设置为一个独立的应用程序来安排警报,然后将警报信息保存到磁盘。保存文件由始终运行的启动守护程序读取,该守护程序处理实际调度警报。

我正在安排警报(编辑:在守护程序中)(NSDate *fireDate之前计算过):

NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:fireDate
                                                interval:0
                                                  target:self
                                                selector:@selector(soundAlarm:)
                                                userInfo:alarm
                                                 repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:singleTimer
                             forMode:NSRunLoopCommonModes];
[self.timers addObject:singleTimer];
[singleTimer release];

编辑:上面的代码在一个名为 的方法中运行,该方法createTimersreloadData. reloadData从共享的保存文件中读取有关计时器的信息,并在AMMQRDaemonManager的 init 函数中调用它,以及每当管理器收到notify_postUI 应用程序已更新保存文件的通知时(使用 )。

soundAlarm:方法(编辑:也在守护进程中)是:

- (void)soundAlarm:(NSTimer *)theTimer {
    NSLog(@"qralarmdaemon: sounding alarm");

    extern CFStringRef kCFUserNotificationAlertTopMostKey;

    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionaryAddValue(dict, kCFUserNotificationAlertTopMostKey, kCFBooleanTrue);
    CFDictionaryAddValue(dict, kCFUserNotificationAlertHeaderKey, CFSTR("Title"));
    CFDictionaryAddValue(dict,kCFUserNotificationDefaultButtonTitleKey, CFSTR("OK"));

    SInt32 err = 0;
    CFUserNotificationRef notif = CFUserNotificationCreate(NULL,
              0, kCFUserNotificationPlainAlertLevel, &err, dict);

    CFOptionFlags response;
    if((err) || (CFUserNotificationReceiveResponse(notif, 0, &response))) {
        // do stuff
    } else if((response & 0x3) == kCFUserNotificationDefaultResponse) {
        // do stuff
    }
    CFRelease(dict);
    CFRelease(notif);

    // Do some other stuff
}

这很好用,并显示手机是否解锁或锁定的警报。但是,如果手机被锁定足够长的时间进入深度睡眠,那么计时器将无法触发。

我不需要它来打开屏幕(尽管那会很好),因为除了显示警报之外,我还会播放声音,但我确实需要触发计时器,以便我知道何时启动声音。

有任何想法吗?


编辑:这是main守护进程的功能。

int main(int argc, char **argv, char **envp) {

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSLog(@"qralarmdaemon: launched");

    AMMQRDaemonManager *manager = [[AMMQRDaemonManager alloc] init];

    NSTimer *keepRunningTimer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture]
                                                         interval:1000
                                                           target:manager
                                                         selector:@selector(keepRunning:)
                                                         userInfo:nil
                                                          repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:keepRunningTimer
                                 forMode:NSRunLoopCommonModes];

    // Execute run loop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop run];

    [manager release];

    NSLog(@"qralarmdaemon: exiting");

    [pool release];

    return 0;
}

(不包括注册来自主应用程序的通知以了解何时读取保存文件等的代码,但我认为这不相关)。


编辑(再次):我在运行循环中添加了一个计时器,该计时器在[NSDate distantFuture]. 这似乎可以延长计时器(在手机锁定后 1 分 45 秒安排的计时器关闭,并唤醒手机)但不是无限期地(在手机锁定后 7 分 30 秒安排的计时器没有关闭)。


编辑:我构建了以下玩具示例来说明问题,而不必担心与我的代码的其他部分的交互。

我编译了这段代码,通过 SSH 连接并运行它,然后锁定了我的手机。如果我将 更改dateByAddingTimeInterval:480dateByAddingTimeInterval:30,我会得到以下输出:

2013-03-31 12:21:25.555 daemontimertest[6160:707] daemon-timer-test: launched
2013-03-31 12:21:56.265 daemontimertest[6160:707] daemon-timer-test: timer fired

但是当它设置为 480 时,我等了 8 多分钟,只看到第一行:

2013-03-31 12:08:09.331 daemontimertest[6049:707] daemon-timer-test: launched

main.m

#import "MyClass.h"

int main(int argc, char **argv, char **envp) {

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSLog(@"daemon-timer-test: launched");

    MyClass *obj = [[MyClass alloc] init];

    NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:[[NSDate date] dateByAddingTimeInterval:480]
                                                    interval:0
                                                      target:obj
                                                    selector:@selector(fireTimer:)
                                                    userInfo:nil
                                                     repeats:NO];

    [[NSRunLoop currentRunLoop] addTimer:singleTimer
                                 forMode:NSRunLoopCommonModes];

    // Execute run loop
    [[NSRunLoop currentRunLoop] run];

    [pool release];

    return 0;
}

MyClass.m

#import "MyClass.h"

@implementation MyClass

- (void)fireTimer:(NSTimer *)theTimer {
    NSLog(@"daemon-timer-test: timer fired");
}

@end

编辑(3/31/13 5:50 EDT):我在玩具应用程序代码中添加了以下代码,以纳入 Nate 关于使用 GCDdispatch_after功能的建议,但它似乎受到相同的时间限制。作为附加说明,主 UI 应用/Applications程序安装在/usr/bin.

    double delayInSeconds = 10.0;
    NSLog(@"daemon-timer-test: delay is %f",delayInSeconds);
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"daemon-timer-test: time has passed.");
    });

编辑(3/31 5:54 PM):另一个快速说明。在系统日志似乎进入深度睡眠之前,以下几行(不连续)出现在系统日志中,并且在我唤醒手机之前没有更多消息。我选择了看起来可能相关的那些;最后一条消息是深度睡眠前发送到 syslog 的最后一条消息。

Mar 31 17:34:23 Andrew-MacKie-Masons-iPhone lockdownd[50]: 002c1000 -[hostWatcher handleSleepNotification:service:messageArgument:]: <hostWatcher: 0x1cd59890> [CC535EDB-0413-4E5E-A844-4DA035E7217C 169.254.2.141:54757] [fd=13]: kIOMessageCanSystemSleep
Mar 31 17:34:23 Andrew-MacKie-Masons-iPhone lockdownd[50]: 002c1000 -[hostWatcher handleSleepNotification:service:messageArgument:]: <hostWatcher: 0x1cd59890> [CC535EDB-0413-4E5E-A844-4DA035E7217C 169.254.2.141:54757] [fd=13]: kIOMessageSystemWillSleep
...
Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone lockdownd[50]: 00343000 __63-[hostWatcher handleSleepNotification:service:messageArgument:]_block_invoke_0: Allowing Sleep
Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone powerd[42]: PM scheduled RTC wake event: WakeImmediate inDelta=645.40
Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone powerd[42]: Idle Sleep Sleep: Using BATT (Charge:76%)
...
Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone kernel[0]: en0::stopOutputQueues
...
Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone kernel[0]: pmu wake events: menu
4

2 回答 2

9

简答

是的,这是可能的(我已经做到了)。

我尝试了几种不同的方法,但我无法让我的守护进程/NSTimer以您描述的方式失败。但是,我还没有看到定义您的应用程序的所有文件/代码,所以我至少还关心一件事。

保持守护进程活着

如果您查看 Apple 文档中的NSRunLoop run

如果没有输入源或计时器附加到运行循环,则此方法立即退出;否则,它通过重复调用 runMode:beforeDate: 在 NSDefaultRunLoopMode 中运行接收器。换句话说,此方法有效地启动了一个无限循环,该循环处理来自运行循环的输入源和计时器的数据。

从运行循环中手动删除所有已知的输入源和计时器并不能保证运行循环将退出。OS X 可以根据需要安装和删除额外的输入源,以处理针对接收者线程的请求。因此,这些来源可能会阻止运行循环退出。

在您为守护main程序显示的代码中,您不会(直接)创建任何计时器。当然,我不知道你在做什么[[AMMQRDaemonManager alloc] init],所以也许我错了。然后你使用:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];

启动运行循环。问题是,如果此时没有计时器,我不确定你的守护进程是否会继续存在。如果您查看上面的第二段,它还表明它可能还活着,所以也许这就是为什么我在尝试使用您的代码时没有看到我的守护进程死亡的原因。

你的评论说你看到守护进程活着,警报应该响起。但是,我想知道您的守护进程是否确实死了,然后重新启动。也许您还可以向我们展示您用于启动守护程序的 .plist 文件(位于 中/System/Library/LaunchDaemons)。

一个快速的实验,可能是自动启动你的守护进程。只需从文件夹中卸载 plist 文件LaunchDaemons,并确保终止该进程。然后,从命令行手动启动它,通过 ssh 进入手机:

$ /Applications/MyApp.app/MyDaemon

然后,观察命令行。你会看到它是否死了,因为它实际上并没有被运行,所以如果它死launchd了就不会重新启动。

解决方案?

如果事实证明您确实遇到了它死亡的问题,那么我会尝试添加一个计时器,该计时器总是在您的守护进程执行时启动。如果您查看我的其他示例Chris Alvares 的守护程序教程,它会显示这一点。在 daemonmain()中,您设置一个NSTimer来触发一个run:方法。在该run:方法中,您可以使用while循环和sleep()调用。或者只是安排计时器以较慢的间隔重复。

我也不确定你的整个应用程序是如何工作的。它只是用于安排 ( NSTimer) 警报的工具吗?如果是这样,则可能在任何时候都没有设置警报。也许另一种解决方案,而不是使用UIApplicationnotify_post()与守护进程通信一个新的计时器,您可以将守护进程配置为简单地监视一个数据文件。只要有新的UIApplication计时器,就会写出数据文件。然后,iOS 可以唤醒你的守护进程来安排NSTimer.

无论如何,这可能是与您的原始问题不同的问题,但它也可能是构建闹钟守护程序的更有效方法,因为如果没有活跃的警报,它实际上并不需要运行。

如果这些想法不能帮助您解决问题,请发布更多信息([AMMQRDaemonManager init]可能会有所帮助)。

更新

还有两个建议:

  • 确保您的应用程序(守护程序和 UI)安装在/Applications. 这是越狱应用程序的正常位置,但我只是想确保您没有将其安装在沙盒区域。

  • 尝试用GCD 块替换您的NSTimer实现(对于警报,您可以保留main()守护进程keepalive计时器):

   // you have used notify_post() to tell the daemon to schedule a new alarm:
   double delayInSeconds = 1000.0;
   dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
   dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
      // put timer expiration code here
   });

更新二

我还注意到,在您原来的alarm:回调中,您使用CFUserNotificationReceiveResponse()的是无限超时。这意味着如果用户不关闭弹出窗口,计时器回调将不会完成,我相信这意味着后续安排的计时器回调不会触发。可能,您应该将所有CFUserNotification代码放入它自己的方法(例如showPopup)中,然后让您的计时器回调如下:

- (void)soundAlarm:(NSTimer *)theTimer {
   dispatch_async(dispatch_get_main_queue(), ^(void) {
       [self showPopup];
   });
}

然后是主程序(在您放在 Dropbox 上的代码中)。我建议将您的计时器(您直接从中调用main())更改为重复计时器,间隔相对较小,而不是使用带有distantFuture. 如果你愿意,你什么都做不了。这只是一个心跳。

主.m:

NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:[NSDate date]
                                                interval:5*60   // 5 minutes
                                                  target:obj
                                                selector:@selector(heartbeat:)
                                                userInfo:nil
                                                 repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:singleTimer
                             forMode:NSRunLoopCommonModes];

我的班级.m:

- (void)heartbeat:(NSTimer *)theTimer {
   NSLog(@"daemon-timer-test: heartbeat timer fired");
}

My last comment is that I don't use syslogd. I'm wondering if any of your tests are failing, not because timers aren't running, but because NSLog statements aren't showing up in your log file. I've done all tests where I actually run the daemon executable at the command line, ssh'd into the phone, and I just watch the console for NSLog output. Take logging out of the list of possible failure points ...

于 2013-03-31T09:45:13.477 回答
4

I've worked out a method that works for me. As per my long exchange with Nate (and I definitely wouldn't have been able to work out what was going on without his help), this seems to happen automatically on some systems, but not on others. The problem on my phone seemed to be that powerd was putting the phone into some sort of deep sleep that paused the NSTimers and didn't allow them to fire properly.

Rather than disabling deep sleep (which I suspect has negative power implications) I scheduled a power event:

NSDate *wakeTime = [[NSDate date] dateByAddingTimeInterval:(delayInSeconds - 10)];
int reply = IOPMSchedulePowerEvent((CFDateRef)wakeTime, CFSTR("com.amm.daemontimertest"), CFSTR(kIOPMAutoWake));

This successfully wakes the phone 10 seconds before the alarm is supposed to go off. (The interval isn't precise. I wanted it to be short enough that the phone wouldn't go back to sleep, but long enough that if the phone takes a moment to wake up the timer can still go at the right time. I'll probably shorten it to just 3 or 4 seconds.

The remaining problem is that the NSTimer for the alarm itself won't update automatically, and so it'll be late by whatever period the phone was asleep for. To fix this you can cancel and reschedule the NSTimer whenever the phone wakes up. I did this by registering for a notification that the power management system posts whenever the power state changes:

int status, notifyToken;
status = notify_register_dispatch("com.apple.powermanagement.systempowerstate",
                                  &notifyToken,
                                  dispatch_get_main_queue(), ^(int t) {
                                      // do stuff to cancel currently running timer and schedule a new one here
                                  });

The inefficiency here is that the notification is posted both on sleeps and wakes, but I haven't been able to find an alternative yet.

I hope this is helpful to anyone else who was struggling with this issue.

于 2013-04-09T05:26:04.777 回答