libuv (libev) と Objective-C autorelease のはまりポイント
iOS や Mac アプリで HTTP 以外のネットワーク機能をつけたいといった場合に、libuv や libev を組み込んで使うというのを割とよくする。方法としては以下のような感じでその機能用のスレッドをつくる:
-(void)run {
NSThread* thread = [[NSThread alloc] initWithTarget:self
selector:@selector(loop)
object:nil];
self.thread = thread;
[thread release];
[thread start];
}
スレッドの中身は大体こんな感じ:
-(void)loop {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
uv_loop_t* loop = uv_loop_new();
// いろいろ初期化
// ...
// libuv イベントループ
uv_run(loop);
uv_loop_delete(loop);
[pool drain];
}
このスレッドは uv_run
でブロックしてしまう。本来ここではCocoaのイベントループ(NSRunLoop)をまわす部分だが、かわりに libuv のイベントループを回している感じになっている。
したがってこのスレッドで Objective-C を混ぜる場合には autorelease がスレッド終了まで基本的にされなくなるから注意が必要。
これを解決する方法で最初に思いつくのは、uv_run
(ev_run
) のかわりに uv_run_once
(ev_run(..., EVRUN_ONCE)
) を使うことだ。
while (1) {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
uv_run_once(loop);
[pool drain];
}
これは一見簡単だけど while ループをとめるフラグを別途用意しないといけないし、uv_run
とちがってループを抜けるときには各イベントハンドラが終了しているかを確かめる必要もありめんどくさい。
結果いまはこんな感じにしている:
static void idle_cb(uv_idle_t* handle, int status) {
uv_idle_stop(handle);
[(NSAutoreleasePool*)handle->data drain];
handle->data = NULL;
}
static void check_cb(uv_check_t* handle, int status) {
uv_idle_t* idle = (uv_idle_t*)handle->data;
if (NULL != idle->data) return;
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
idle->data = (void*)pool;
uv_idle_start(idle, idle_cb);
}
-(void)loop {
uv_loop_t* loop = uv_loop_new();
uv_check_t check;
uv_check_init(loop, &check);
uv_check_start(&check, check_cb);
uv_idle_t idle;
uv_idle_init(loop, &idle);
check.data = (void*)&idle;
idle.data = NULL;
// いろいろ初期化
// ...
uv_run(loop);
uv_loop_delete(loop);
}
uv_check
(ev_check
) で NSAutoreleasePool
をつくりつつ idle タイマーを作って、 その idle タイマー時に [pool drain]
する。 これだとイベントが詰まっている場合は drain は呼ばれず、キリの良いときに呼んでくれるから run_once
でいちいちやるよりは効率も良さそうな気がする。
このコードでは省略しているけど実際に使うときには check
や idle
も終了しないと uv_run
から抜けてこないのでどこかのオブジェクトにまとめて突っ込んでおいたりして使うのが吉。
こういうのを何も考えずに Objective-C をまぜるともりもりメモリ食うようになるから気をつけよう!