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

我正在尝试为越狱的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];

编辑:上面的代码在称为createTimers的方法中运行,该方法由reloadData调用。 reloadData从共享的保存文件中读取有关计时器的信息,并在AMMQRDaemonManager的init函数中以及管理器每当收到通知(带有notify_post)UI应用程序已更新保存文件的调用时,该信息就会被调用。
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
}

这可以很好地工作,并且会在手机处于解锁状态或锁定状态时显示警报。但是,如果手机被锁定足够长的时间才能进入深度 sleep 状态,则计时器将无法启动。

我不需要打开屏幕(虽然那会很好),因为除了显示警报外,我还将播放声音,但是我确实需要启动计时器,这样我才能知道何时启动声音。

有任何想法吗?

编辑:这是守护程序的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秒关闭的计时器,然后唤醒电话),但不会无限期地保存(预定时间在手机锁定后30分30秒的计时器没有关闭) )。

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

我编译了此代码,通过SSH登录并运行了它,然后锁定了我的手机。如果将dateByAddingTimeInterval:480更改为dateByAddingTimeInterval: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

编辑(13/31/13 5:50 EDT):我在玩具应用程序代码中添加了以下代码,以结合内特关于使用GCD的dispatch_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):另一个快速说明。在系统日志似乎进入深度 sleep 之前,以下几行(不连续)显示在syslog中,并且在我唤醒电话之前没有更多消息。我选择了看起来可能相关的内容;最后一条消息是深度 sleep 之前发送到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

最佳答案

简短答案

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

我尝试了几种不同的方法,但无法按照您所描述的方式将守护程序/NSTimer设置为失败,而失败。但是,我还没有看到定义您的应用程序的所有文件/代码,因此我至少还要担心一件事。

使守护程序保持 Activity 状态

如果您在Apple文档中查找NSRunLoop run:



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

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

开始运行循环。问题是,如果此时没有没有计时器,则我不确定您的守护程序是否会继续存在。如果您查看上面的第二段,它也表明它可能仍然存在,所以也许这就是为什么当我尝试使用您的代码时,我的守护进程没有消失的原因。

您的评论说,当警报响起时,您可以看到守护进程还在运行。但是,我想知道您的守护进程是否终止,然后重新启动。也许您也可以向我们展示您用于启动守护程序的.plist文件(该文件在/System/Library/LaunchDaemons中)。

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

$ /Applications/MyApp.app/MyDaemon

然后,观看命令行。您将看到它是否死亡,并且由于它实际上不是由launchd运行的,因此即使它死了也不会重新启动。

解决方案?

如果事实证明您快要死了,那么我将尝试添加一个计时器,该计时器始终在守护程序执行时启动。如果您查看at my other exampleChris Alvares' daemon tutorial,则显示此内容。在守护程序main()中,设置一个NSTimer来触发run:方法。在该run:方法中,您可以使用while循环和sleep()调用。或者只是安排计时器以某个缓慢的间隔重复。

我也不确定整个应用程序的工作方式。它是仅用于调度(NSTimer)警报的工具吗?如果是这样,则有可能在任何时候都未设置任何警报。也许是另一种解决方案,您可以将守护程序配置为simply watch a data file,而不是使用UIApplicationnotify_post()将新计时器传递给守护程序。每当有新计时器时,UIApplication都会写出数据文件。然后,iOS可以唤醒您的守护程序以安排NSTimer

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

如果这些想法不能帮助您解决问题,请发布更多信息([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触发日期。如果需要,您什么也不能做。这只是一个心跳。

    main.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];
    

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

    我的最后一句话是我不使用syslogd。我想知道您的任何测试是否失败,不是因为计时器未运行,而是因为NSLog语句未显示在日志文件中。我已经在命令行中实际运行守护程序可执行文件的所有测试中完成了这些操作,然后将它们押入电话中,并且仅在控制台中查看NSLog输出。从可能的故障点列表中注销...

    10-07 19:54
    查看更多