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

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

前回の続きです。まずは、説明を続ける前に、本来はどう組めば良かったのかを記述しておきます。

まずは、windowControllerDidLoadNib:の最後に付け加えたコードですが、次の様に書くべきでした。

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

retainするのをやめて、adjustWindowTimerにポインタを格納するのもやめています。これにより、インスタンス変数、adjustWindowTimerは必要無くなりましたので、MyDocument.hから削除してかまいません。何でかと言いますと、自分自身では後でadjustWindowTimerを使う必要が無いからです。このコードを実行すると、NSTimerクラスのインスタンスが生成され、イベントループに組み込まれ、そちらでretainします。従いまして、自分で所有していなくても消えませんし、後で使うわけでも無いので、所有する必要は無いというわけです。

次に、timerAdjustWindowSize:の実装ですが、次の様になります。

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

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

theWindowをaTimerから取得するようになりました。このメソッドの実行が終われば、自動的にNSTimerクラスのインスタンスは無効化され、イベントループから取り除かれることになります。

最初からこうやって書けば混乱は少なかったのですが、誤った書き方をしてしまい、申し訳無かったです。そこで、この際ですから、NSTimerについて、もう少し突っ込んだ説明をしようと思います。今回の例では、Timerクラスのインスタンスから投げられるメッセージは一度だけですが、これが繰り返し投げるような指定になっていたらどうなるでしょうか。つまり、次の様なコードです。

hogeTimer =
    [[NSTimer scheduledTimerWithTimeInterval:hogeSec
                                      target:hogeTarget
                                    selector:@selector( timerHoge:)
                                    userInfo:hogeObject
                                     repeats:YES
     ] retain];

この場合は、後に次のようなコードで繰り返しを止める必要があります。

[ hogeTimer invalidate];
[ hogeTimer release];
hogeTimer = nil;

つまり、後でhogeTimerが必要になるというわけですね。hogeTimerにinvalidateメッセージを送ってhogeTimerを無効化し、必要無くなったのでreleaseしています。この後、hogeTimerにnilをセットしているのは、無くなってしまったインスタンスに誤ってメッセージを送信しないようにするためです。もちろん、そんなことしないようにプログラムを組めば良いだけなのですが、こうやっておけば、間違えて送信しようとしても問題が起こりません。一種のフェイルセーフです。

ところで、入門書の中には、NSTimerのインスタンスを生成する時に、retainしているものとしていないものの二種類があることをご存知でしょうか。実は、これはどちらでも動作します。こう書くと、この連載を読んで来た方は、後で自分が使う必要があるのに何でretainしなくても良いのだろうと、不思議に思われると思います。その理由は、hogeTimerが生成されると同時にイベントループに組み込まれ、そちらでretainされ、無効化されるまでは決してreleaseされないからです。つまり、invalidateメッセージを送るまで、消えることはありません。従いまして、自分でretainしなくても、それまでは大丈夫ということになります。でも、NSTimerの実装がそうだからと言って、retainしなくても良いのでしょうか。私個人の考えでは、これはたまたまreleaseされないだけだと思います。そもそもretain、releaseとは、自分に関係無いものは無視できるようにするための仕組みです。相手の実装を知ったからretainしなくても良いというのは、この目的に反します。従いまして、この場合でもretainするべきであると、私は思います。ただし、retainしなくても消えないということは、NSTimerのドキュメントにも書いてあることですから、どうするかは皆様の判断にお任せします。

さて、それでは最後に、前回積み残した説明をします。何で最初に次の様なコードを書いたかです。

- (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];
}

invalidateを送信しなければならないと勘違いしていたとはいえ、aTimerでNSTimerクラスのインスタンスへのポインタは渡って来るのですから、これをadjustWindowTimerに格納しておいて使う必要は無いでしょう。実は、普段組んでいるプログラムでは、さらに別の所でadjustWindowTimerを使っているのです。

- (void)windowWillClose:(NSNotification *)aNotification{
    [ adjustWindowTimer invalidate];
    [ adjustWindowTimer release];
    adjustWindowTimer = nil;
}

ウィンドウのdelegateをMyDocumentにしておいて、上記メソッドを実装しておきますと、ウィンドウが閉じる直前に、このメソッドが呼び出されます。何のためにこんなことをしているのかと言うと、timerAdjustWindowSize:メッセージが投げられる前にウィンドウが閉じてしまう可能性を考えてのことです。もし、そんなことが起こってウィンドウが閉じた後にtimerAdjustWindowSize:メソッドが呼び出されてしまったら。存在しないウィンドウに対してメッセージを投げることになり、このプログラムは吹っ飛んでしまいます。もっとも、今回の場合、そんなタイミングでウィンドウを閉じることが可能なのか疑問も感じますが。本連載では、ここまで説明すると本質が分かりにくくなってしまうと思い(本当は必要無い可能性もありますし)、こういった対処は省いています。ですが実際に自分でプログラムを組む時には、微妙なタイミングで起こり得る事象を熟考し、この様な対処も組み込んでいます。

前頁目次次頁