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は共有保存ファイルからタイマーに関する情報を読み取り、 の init 関数で呼び出されます。また、管理者がUI アプリが保存ファイルを更新したAMMQRDaemonManagerという通知を ( で) 受け取るたびに呼び出されます。notify_post

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

編集 (2013 年 3 月 31 日 5:50 EDT): 次のコードをおもちゃアプリのコードに追加して、GCD のdispatch_after機能を使用するという Nate の提案を組み込みましたが、同じ時間の制約を受けるようです。追加のメモとして、メイン 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 分): もう 1 つの簡単なメモ。次の行は、syslog がディープ スリープに入る直前に (連続ではなく) 表示され、電話を起動する前にメッセージが表示されなくなります。関連する可能性があると思われるものを選択しました。最後のメッセージは、ディープ スリープの前に 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失敗させることができませんでした。ただし、あなたのアプリを定義するすべてのファイル/コードを確認したわけではないため、少なくとももう 1 つ懸念事項があります。

デーモンの存続

NSRunLoop runの Apple ドキュメントを参照すると、次のようになります。

実行ループに入力ソースまたはタイマーが接続されていない場合、このメソッドはすぐに終了します。それ以外の場合は、runMode:beforeDate: を繰り返し呼び出して、NSDefaultRunLoopMode でレシーバーを実行します。つまり、このメソッドは、実行ループの入力ソースとタイマーからのデータを処理する無限ループを効果的に開始します。

既知のすべての入力ソースとタイマーを実行ループから手動で削除しても、実行ループが終了する保証はありません。OS X は、必要に応じて追加の入力ソースをインストールおよび削除して、受信者のスレッドを対象とする要求を処理できます。したがって、これらのソースは実行ループの終了を妨げる可能性があります。

デーモンのmainプログラムに表示するコードでは、(直接) タイマーを作成しません。もちろん、私はあなたが で何をしているのか知らない[[AMMQRDaemonManager alloc] init]ので、間違っているかもしれません。次に、次を使用します。

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

実行ループを開始します。問題は、この時点でタイマーがない場合、デーモンが生き続けるかどうかわからないことです。上記の 2 番目の段落を見ると、それは生きている可能性があることも示しているため、コードを使用しようとしてもデーモンが停止しないのはそのためかもしれません。

あなたのコメントは、アラームが鳴るはずのときに、デーモンプロセスが生きているのを見ると言っています。ただし、デーモン プロセスが終了してから再起動されたのではないかと考えています。Launch Daemon に使用する .plist ファイル ( /System/Library/LaunchDaemons.

1 つの簡単な実験として、デーモンを自動的に開始しないようにすることがあります。フォルダーから plist ファイルをアンインストールし、必ずプロセスを強制終了してください。次に、コマンド ラインから手動で起動し、電話に ssh 接続します。LaunchDaemons

$ /Applications/MyApp.app/MyDaemon

次に、コマンド ラインに注意してください。死んでいるかどうかがわかります。実際には によって実行されているlaunchdわけではないため、死んでも再起動されません。

解決?

それが死ぬことに問題があることが判明した場合は、デーモンが終了したときに常に開始するタイマーを追加してみます。私の他の例、またはChris Alvares のデーモン チュートリアルを見ると、これが示されています。デーモンでは、メソッドをmain()起動するように設定します。その方法では、ループと呼び出しを使用できます。または、タイマーをスケジュールして、ゆっくりとした間隔で繰り返すようにします。NSTimerrun:run:whilesleep()

また、アプリ全体がどのように機能するかわかりません。NSTimer( ) アラームをスケジュールするためのツールだけですか? その場合、いつでもアラームが設定されていない可能性があります。UIApplicationtoを使用して新しいタイマーをデーモンに通信する代わりに、別の解決策として、単にデータファイルを監視するnotify_post()ようにデーモンを構成できます。新しいタイマーがあるときはいつでも、 はデータ ファイルを書き出します。次に、iOS はデーモンを起動して、. UIApplicationNSTimer

とにかく、これは元の問題とは別の問題かもしれませんが、アクティブなアラームがない場合は実際に実行する必要がないため、目覚まし時計デーモンを構築するより効率的な方法かもしれません。

これらのアイデアで解決できない場合は、さらに投稿してください (の本文が役立つ場合が[AMMQRDaemonManager init]あります)。

アップデート

さらに 2 つの提案:

  • アプリ (デーモンと UI) が にインストールされていることを確認してください/Applicationsこれは脱獄アプリの通常の場所ですが、サンドボックス領域にインストールしていないことを確認したかっただけです.

  • NSTimer実装を GCD ブロックに置き換えてみてください(アラームmain()については、デーモンキープアライブタイマーをそのままにしておくことができます)。

   // 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タイマーが実行されていないためではなく、ログ ファイルにステートメントが表示されないために、テストのいずれかが失敗していないかどうか疑問に思っています。コマンドラインでデーモン実行可能ファイルを実際に実行し、電話にssh接続し、コンソールのNSLog出力を監視するすべてのテストを実行しました。考えられる障害点のリストからログアウトしてください...

于 2013-03-31T09:45:13.477 に答える
4

自分に合った方法を編み出しました。Nate との長いやり取り (そして、彼の助けがなければ何が起こっているのかを理解することはできなかったでしょう) によると、これは一部のシステムでは自動的に発生するようですが、他のシステムでは発生しないようです。私の電話の問題は、電話powerdをある種の深いスリープ状態にして、NSTimersを一時停止し、適切に起動できないように思われました。

ディープ スリープを無効にするのではなく (これは電力に負の影響があると思われます)、電力イベントをスケジュールしました。

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

これにより、アラームが鳴るはずの 10 秒前に電話機が正常に起動されます。(間隔は正確ではありません。電話がスリープ状態に戻らない程度に短くしたかったのですが、電話が起動するのに少し時間がかかった場合でもタイマーが適切な時間に作動できるように十分に長くしたかったのです。私はおそらく、わずか 3 ~ 4 秒に短縮されます。

残っている問題はNSTimer、アラーム自体が自動的に更新されないことです。そのため、電話がスリープ状態になっていた時間だけ遅れてしまいます。これを修正するには、電話機が起動するたびに NSTimer をキャンセルして再スケジュールします。これは、電源状態が変化するたびに電源管理システムが投稿する通知を登録することで実現しました。

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
                                  });

ここでの非効率性は、通知がスリープとウェイクの両方に投稿されることですが、私はまだ代替手段を見つけることができませんでした.

これが、この問題に苦しんでいる他の人に役立つことを願っています.

于 2013-04-09T05:26:04.777 に答える