(前回)Cocoaの日々: SimpleCap - アプリケーションボタン
前回 SimpleCap にアプリケーションパレットを追加した。これを使って他のアプリケーションで画像を編集することもできるのだが、編集後は最新の画像を SimpleCap側でも表示したい。その為には画像ファイルの更新状況を知る必要があるが今回は検証も兼ねて FSEvent を調べてみた。
FSEvent
FSEvent は指定したフォルダの変更を通知してくれるフレームワーク。Mac OS X v10.5 から導入された。
(参考)
Leopard解体新書--第5回:ファイル監視を行うFSEvent - Leopard解体新書 - page2 - ZDNet Japan
Mac Dev Center: File System Events Programming Guide: Using the File System Events API
日本語もあった。
ファイルシステムイベント プログラミングガイド: イベントストリームの作成
監視対象はファイルではなくてフォルダ。今回の用途ではこれで十分。通知がきたら表示中の画像ファイルの更新状況を自前でチェックすればいい。
サンプル
動作確認のためサンプルを作ってみた。Objective-C ベースの書き方はここが参考になる。
Apple Developer Connection: Monitoring File Changes with the File System Events API
まず登録部分。
- (void)awakeFromNib
{
FSEventStreamEventId lastEventId =
[[[NSUserDefaults standardUserDefaults]
objectForKey:@"LAST_EVENT_ID"] unsignedLongLongValue];
NSLog(@"Last event id = %llu", lastEventId);
if (!lastEventId) {
lastEventId = kFSEventStreamEventIdSinceNow;
}
NSArray* pathsToWatch = [NSArray arrayWithObjects:
@"/Users/hashi/Desktop",
@"/Users/hashi/Desktop/SimpleCap Images", nil];
void *selfPointer = (void*)self;
FSEventStreamContext context = {0, selfPointer, NULL, NULL, NULL};
FSEventStreamRef stream;
NSTimeInterval latency = 3.0; /* Latency in seconds */
/* Create the stream, passing in a callback */
stream = FSEventStreamCreate(NULL,
&fsevents_callback,
&context,
(CFArrayRef)pathsToWatch,
lastEventId,
latency,
kFSEventStreamCreateFlagNone /* Flags explained in reference */
);
/* Create the stream before calling this. */
FSEventStreamScheduleWithRunLoop(stream,
CFRunLoopGetCurrent(), kCFRunLoopDefaultMode
);
FSEventStreamStart(stream);
}
デスクトップと SimpleCap 画像フォルダを監視対象としてみた。Objective-C のインスタンスをコールバック関数へ渡す場合には FSEventStreamContext を用意しておく。
監視は
FSEventStreamCreaate()
FSEventStreamScheduleWithRunLoop()
FSEventStreamStart()
の順で呼び出すだけ。
イベントが発生するとコールバック関数が呼び出される。サンプルではイベントが来たことを標準出力へ書き出している。
void fsevents_callback(
ConstFSEventStreamRef streamRef,
void *userData,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
int i;
char **paths = eventPaths;
NSLog(@"clicentCallBackInfo: %@", userData);
for (i=0; i
/* flags are unsigned long, IDs are uint64_t */
printf("Change %llu in %s, flags %lu\n",
eventIds[i], paths[i], eventFlags[i]);
}
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithUnsignedLongLong:eventIds[numEvents-1]] forKey:@"LAST_EVENT_ID"];
}
実行して監視対象のフォルダ内でファイルを追加したり削除、変更してみる。
デバッガコンソールの出力
Change 64250232 in /Users/hashi/Desktop/SimpleCap Images/, flags 0
Change 64250236 in /Users/hashi/Desktop/, flags 0
Change 64250243 in /Users/hashi/Desktop/, flags 0
Change 64250913 in /Users/hashi/Desktop/MyStuffs/, flags 0
指定した3秒程度の遅れで通知が来た。
同じフォルダ階層を監視対象にした場合は、下の階層のフォルダ /Users/hashi/Desktop/SimpleCap Images での変更はそれ単体のイベントだけが送られる。 /User/hashi/Desktop としてのイベントは来ない。
監視対象 /User/hashi/Desktop の下階層 /User/hashi/Desktop/MyStuffs の変更を行った場合はそれがイベントとして送られる。
このことはつまりイベントは指定フォルダのみが知らされるのではなく、具体的に変更のあったフォルダ名で通知されることがわかった。
永続イベント
FSEvent の面白いところは、通知開始以降のイベントだけでなく過去のイベントを取得できる点。
ファイルシステムイベント プログラミングガイド: 永続イベントの使用
アプリ終了時にイベントID をとっておいて、次回起動時にそれを使えばアプリが停止していた間の変更イベントをあとから取得できる。
サンプルコードではイベントハンドラで最新のイベントIDを NSUserDefaultsへ保存しておき、次回起動時にそれを使って停止中のイベントを取得するようにしてある。
挙動を見てみよう。
まず実行してフォルダ内でファイルを削除する。
FSEventSample[24527:80f] Last event id = 0
Change 64214130 in /Users/hashi/Desktop/SimpleCap Images/, flags 0
一旦アプリを停止しする。そして同じフォルダ内で別のファイルを削除する。
その後、再起動すると
FSEventSample[24539:80f] Last event id = 64214130
Change 64214227 in /Users/hashi/Desktop/SimpleCap Images/, flags 0
Change 64214235 in /Users/hashi/Desktop/SimpleCap Images/, flags 0
Change 64214235 in /Users/hashi/Desktop/SimpleCap Images, flags 16
出た。前回のイベントID 6421430 以降のイベントを取り出すことができた。最後のエントリはパスの最後に/ がついていないのflagsが16となっている。FSEvent.h によれば historical(履歴)の最後を表すマークとのこと。
kFSEventStreamEventFlagHistoryDone = 0x00000010,
ソースコード
GitHub からどうぞ
FSEventSample at 20091217 from xcatsan's SampleCode - GitHub
参考情報
FSEvents.h