Cocoaでいこう! Macらしく 第34回
Yoshiki(DreamField)
この記事は、MOSAが発行するデベロッパ向けのデジタルマガジンMOSADeN 第96号(2004年1月13日発行)に掲載された記事です。2〜3ヶ月遅れで、ここに掲載して行きます。

NSTimerを使ってみよう(前編)

前回、NSTimerクラスを使用しましたが、これの説明はしていませんでした。そこで今回、その説明をするために最新のリファレンスを読んだのですが、「え?こんなこと書いてあったっけ?」。ご免なさい。前回紹介したプログラムは余計なことをしてしまっています。そう言えばNSTimerについて調べたのは、まだJaguarさえ出ていない頃。確かこんなに詳しくは書いてありませんでした。もちろん、この連載を書く時には、念のためになるべく最新のリファレンスを確認しているのですが、前回、NSTimerの説明を次回に廻してしまったため、確認を怠たっていました。申し訳ない。ただし、このままでも動作はします。

さて、どこがまずいのかは説明の中で触れます。まずはwindowControllerDidLoadNib:の最後に付け加えたコードを見て下さい。

adjustWindowTimer =
    [[NSTimer scheduledTimerWithTimeInterval:0
                                      target:self
                                    selector:@selector( timerAdjustWindowSize:)
                                    userInfo:theWindow
                                     repeats:NO
     ] retain];

NSTimerクラスは、一定時間後に、指定したメッセージを指定したオブジェクトに送ることが出来るクラスです。ここでは、NSTimerクラスのインスタンスを生成しています。上記コードだけではパラメタの説明がしにくいので、リファレンスを合わせて見て下さい(NSTimerクラスが書いてあるのは、Foundation Reference for Objective-Cの方です)。リファレンスには次の様に書いてあります。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                     target:(id)target
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats

secondsは、何秒後にメッセージを送信するのかを指定します。ここには0を指定していますから、0秒後です。targetは、どのオブジェクトにメッセージを送信するのかを指定します。selfを指定していますから、自分自身です。aSelectorは送信するメッセージを指定します。送信するメッセージはtimerAdjustWindowSize:なのですが、これを文字列としてそのまま指定することは出来ません。何故ならメッセージはコンパイル時に、SEL型の内部表現の値に変換されるからです。ここで使われている@selector()は、プログラム上の文字列表記から、SEL型のデータを得るコンパイル指示子です。userInfoは、何か付加情報を渡したい時に指定します。必要無い時にはnilでかまいません。ここではウィンドウを指すポインタを指定しています。最後のrepeatsは、このメッセージを繰り返し送信するか否かを指定します。ここではNOを指定していますから、送信するのは一度だけです。

まとめますと、ここの実装では、0秒後に一度だけ自分自身にtimerAdjustWindowSize:メッセージを送信するという指定になります。ここで勘違いしないで欲しいのは、NSTimerはマルチスレッドは使っていないということです。NSTimreオブジェクトはイベントループに組み込まれ、そこから指定した時間後にメッセージを送信してもらうことにより、見かけ上の並行処理を実現しています。この辺りの方式は、Macでプログラムを組んでいた方にとってはおなじみですね。このため、イベントループに戻らなければ決して実行されませんし、確実に指定した時間に実行されるとも限りません。こう書くと、マルチスレッドを使った方が良いのではないかと思われるかもしれませんが、そもそもマルチスレッドというのは、本当に並行に処理をする必要が無い時には、かえってプログラムの複雑さが増し、性能も落ちるのです。ですから、本当に必要なのかどうかを良く考え、その必要が無ければやみくもに使うべきではありません。今回の場合は、ウィンドウが表示されてから実行したいだけなのですから、むしろマルチスレッドを使わない方が都合が良いと言えます。

次に、メッセージを送信された結果、実行されるメソッドの実装を見てみましょう。

- (void)timerAdjustWindowSize:( NSTimer *)aTimer
{
    NSWindow *theWindow;
    NSRect theWindowRect;

    theWindow = [ adjustWindowTimer userInfo];
    [ adjustWindowTimer invalidate];
    [ adjustWindowTimer release];
    adjustWindowTimer = nil;

    theWindowRect =
        NSIntersectionRect( [ theWindow frame], [ [ theWindow screen] visibleFrame]);
    [ theWindow setFrame:theWindowRect display:YES];
}

送信するメッセージの形は決まっています。戻り値無しのvoid型で、引数としてNSTimerオブジェクト自身が渡って来ます。このNSTimerオブジェクトにuserInfoメッセージを投げますと、NSTimerオブジェクト生成時に指定したuserInfoを返してもらえます。こうやって付加情報のやり取りをします。ここではウィンドウを指すポインタを受け取っていますね。さて、ここで引数で渡って来るのなら、adjustWindowTimerなんてインスタンス変数は必要なかったのではないかという疑問が湧くと思います。ここで書いた範囲のコードでは、その通りですね。ここも見落としていました。これの説明は後で行います。

最初に書いた、明らかに余計なことをやっているのは、この次の行です。

[ adjustWindowTimer invalidate];

invalidateメッセージを投げると、投げた先のNSTimerオブジェクトが無効になり、遠からずイベントループから取り除かれます。ところが、リファレンスによると、repeatsにNOを指定した時には、メッセージを投げた後に自動的にこれが行われると書いてありました。従いまして、今回、これは必要ありませんでした。

さて、何でNSTimerオブジェクトをretainしているか等、まだまだ説明が残っていますが、それは次回に続きます。

前頁目次次頁