Developer's Blog

UIKit とスレッドのお約束

アプリが小気味よく動くようにするために、メインスレッドでの重たい処理を避けて、適宜バックグラウンドスレッドに処理を回すことはよくあります。

このとき注意が必要なのは、バックグラウンドでの処理結果を画面に反映させる時の以下のお約束です。

UIKit のオブジェクトはメインスレッド以外からアクセスしてはいけない

Apple のドキュメントとしては Cocoa Fundamentals Guide に “All UIKit objects should be used on the main thread only” と書いてあります。この制限は UIKit 特有ではなく、Java の Swing などでも共通の設計です。GUI のパフォーマンスを良くするために、スレッドセーフをあえて捨てて単純化しているのです。

ではどうすれば良いのかと言うと、バックグラウンドスレッドからメインスレッドに処理を渡すために、NSObject にある次のメソッドを使います。

- (void)performSelectorOnMainThread:(SEL)selector
        withObject:(id)arg
        waitUntilDone:(BOOL)wait

これで、メインスレッドでレシーバのメソッドが呼び出されます。たとえば、バックグラウンドからある NSString を UITextView に表示させたい場合は、次のように書けばメインスレッドで安全に画面表示を更新できます。

[textView performSelectorOnMainThread:@selector(setText:)
        withObject:text
        waitUntilDone:NO];

「安全に」と書きましたが、メインスレッド以外から UIKit のオブジェクトにアクセスすると何が起こるのでしょうか。

結論から書くと、何が起こるかは「不定」です。まったく何事もなく動作することもありますし、同じ処理でもタイミングによってはクラッシュを起こすこともあります。私がよく目にするのは、UIView のレイアウトの崩れです。「何かな?」と思って調べていくと、バックグラウンドスレッドから UIView にアクセスしているということが何度かありました。

“UIKit はメインスレッドで”と意識して実装しているにもかかわらず、バックグラウンドスレッドから UIKit にアクセスしてしていたのです。

このようなバグは、Key-Value Observing(KVO) や NSNotificationCenter 経由で呼び出されるメソッド、デリゲートメソッドなどに入りこむ傾向があります。たとえば、KVO しているプロパティがバックグラウンドスレッドで変更されたら、オブザーバのメソッドも同じバックグラウンドスレッドで呼び出されます。それを考慮せずに処理して、結果的に UIView を更新してしまったりするのです。

回避するためには、そもそもの変更や通知の発生源をメインスレッドに限定するか、呼び出された側でメインスレッドかどうかを判別して適宜処理を振り分けます。メインスレッドかどうかは NSThread の isMainThread メソッドでチェックできます。

UIKit にバックグラウンドスレッドからアクセスするのはさまざまなやっかいな問題を引き起こします。UIKit 内の原因不明のクラッシュやレイアウト崩れは、メインスレッド以外からのアクセスが発生していないかも含めて調査してみることをおすすめします。

Copyright © 2019 Fenrir Inc. All rights reserved.