コールバック、よく使いますよね。
非同期処理の結果を受け取るには、必ずと言っていいほど付き合うことになるコールバックですが、UI のようにライフサイクルを持つオブジェクトと共存するには、考慮すべきことがいくつかあります。
ここでは、おおまかに、上手にコールバックと付き合う方法を見ていきます。
基本となるポイント
なんといってもまず抑えなければいけないポイントは、ライフサイクルを持つオブジェクトとの共存です。世に出回っている様々なコールバック管理のためのライブラリは、このライフサイクルを持つオブジェクトとの共存をいかに楽に、あるいは直感的にするか、ということをもとに作られています。
ライフサイクルとはつまり、オブジェクトが生成されてから消滅するまでの一連の流れのことです。
new
したりalloc
したりしたタイミングでオブジェクトが生成され、GC に回収されたりdealloc
したりするタイミングでオブジェクトが消滅します。
オブジェクトを消滅させてもよいと判断する最も単純な方法は、そのオブジェクトを誰も使っていない(参照していない)かどうかです。誰も使っていないのであれば、もうそれをメモリに乗せておく必要はないので、ニフラムなり何なりで消してしまっても良いはずですね。
そうすると、ライフサイクルを持ったオブジェクトが、コールバックが呼ばれる前に光の彼方へ消し去られてしまったり、コールバックとライフサイクルオブジェクトが循環参照していつまでもメモリに居座り続けたり、ということが起こりえます。
前者は例外によるクラッシュ等で問題が明らかになりますが、後者はなかなか目につきにくい問題のため、厄介です。
いずれにしても、きちんとした方法でコールバックを管理しないと良くないことが起こる、ということがわかると思います。
初級編1:WeakReference で参照を管理する
コールバックと上手に付き合う最初の一歩は、ライフサイクルを持つオブジェクトへの参照を、弱い参照で持つことです。
多くの場合、コールバックは、ライフサイクルを持つオブジェクトが実装します。そして、そのオブジェクトへの参照は別のオブジェクトが持ちます。この時、コールバックが呼ばれる側(ライフサイクルを持つ側)とコールバックを呼ぶ側が互いに互いを参照しあう為、通常の参照のままでは、どちらもメモリから破棄できなくなります。
また、Java においては、static
でない内部クラスは暗黙的に外側のクラスのインスタンスへの参照を持つことが知られており、これもまた有名なメモリリークの一因です。
いずれにしても、これらは強い参照で持っていることが問題となる為、これらは弱い参照で管理することになります。
public class Something {
SomeCallback mCallback; // 強い参照
WeakReference<SomeCallback> mWeakCallback; // 弱い参照
public static interface SomeCallback {
public void done();
}
}
@interface Something : NSObject {
__weak SomeCallback *callback;
}
弱い参照は、参照しているオブジェクトへの強い参照がなくなり次第すぐに破棄してもよい参照ですので、常に参照が生きているかどうか確認する必要が有ることに注意しましょう。
初級編2:ライフサイクルに応じて管理が必要なコールバックは、それを明示する
弱い参照で管理する以外に、もう一つ、コールバックを管理する方法があります。
それは、ライフサイクルの各段階において、ライフサイクルを持つオブジェクト自身に、コールバックを適切に管理するよう明示する方法です。
例えば、Android であれば、EditText への入力を拾うコールバックには、登録と解除の2つのメソッドがあり、それぞれ必要に応じて登録・解除を行うようになっています。
概ね、命名規則によって、ライフサイクルに応じた管理が必要なコールバックの登録の方法が明示されるようです。
名前 | 用途 |
---|---|
set | 通常のコールバックの登録 |
add/remove, register/unregister | ライフサイクルに応じた管理が必要なコールバックの登録/解除 |
中級編1:複数のコールバックに対応する
設計によっては、複数のライフサイクルオブジェクトが、同じ一つのシングルトンなオブジェクトにコールバックを登録するような事も有り得ます。
そういう場合にも、基本は弱い参照でコールバックを管理します。ただし、今度は管理するコールバックが複数になります。
コールバックは、常にライフサイクルオブジェクトと共に管理することになります。つまり、ライフサイクルオブジェクトを key にしてコールバックオブジェクトを引いてくることができるような関係にあります。よって、Map
のようなデータ構造でもってコールバックが管理できそうです。
そして、コールバックへの参照は弱い参照で持つべきですので、value が弱い参照となるWeakHashMap
で管理するのがよさそうです。
public class Callbacks {
final HashMap<Object, SparseArray<Callback>> mCallbacks = new WeakHashMap<>();
}
中級編2:コールバックのメソッドを上手く取り扱う
通常、コールバックはインタフェースとして定義し、それを実装したクラスを作ることで実際の処理を書いていきます。
コールバックのインタフェースに複数のメソッドがあり、すべての実装に具体的な処理が必要ない場合には、メソッドの中身のないインタフェースの実装クラスを作り、使う側が好きなものだけを選択してオーバライドするようにすることもあります(Simple~~Listener等)。
public interface HogeCallback {
public void onHoge();
public void onFuga();
public void onPiyo();
public void onFoo();
public void onBar();
}
public class SimpleHogeCallback implements HogeCallback {
@Override
public void onHoge() {}
@Override
public void onFuga() {}
@Override
public void onPiyo() {}
@Override
public void onFoo() {}
@Override
public void onBar() {}
}
public class ConcreteHogeCallback extends SimpleHogeCallback {
@Override
public void onHoge() {
}
}
ただし、Fragment
等どうしてもこのようなコールバックの管理の仕方が出来ない場合もあります。例えば、DialogFragment
からのコールバックをActivity
やFragment
で受ける場合、既に継承関係が出来てしまっているので、直接コールバックインタフェースを実装することになります。
上級編1:Promise を用いてコールバックと付き合う
コールバックを必要とするレイヤ(モデル)が増えれば増えるほど、コールバックを管理するコストは増えていきます。使う側の立場としても、数多くのコールバックがあるということは、どの順番でそのコールバックを呼ぶか(呼び出してもらうか)ということも気になるところですし、ライフサイクルに応じて管理する必要のあるコールバックの場合、それを管理するコストも増えていきます。
よくある悩みとしては、コールバックAの結果を元に別の処理を実行し、コールバックBを受け取る、というようなもので、これを素直に記述していくと、コールバックのネストが何層にもわたって展開されるようになってしまいます。
public class Sample {
private SomeModel mModel;
public void doSomething() {
mModel.doFirst(new Callback() {
@Override
public void onDone() {
mModel.doSecond(new Callback() {
@Override
public void onDone() {
// ...
}
});
}
});
}
}
ネストが深ければ深いほど、潜在的な難しい問題を仕込んでしまいやすく、また、見た目にも綺麗とはいえないコードになってしまいます。
コールバックの呼び出しに順番を考慮に入れる必要がある場合、このようなコールバックのネストを避けるためのパターンとして、Promise
があり、これの実装をしているライブラリがいくつか有ります。
おおざっぱに、メインメソッドとは異なるメソッドの中で非同期に実行したいこと、メインメソッドで実行したいことをチェイン状につなげ、それぞれのモジュラな処理の結果をオブジェクトとして次々に渡していくパターンになり、上手く設計すれば、ライフサイクルオブジェクトの中ではPromise
だけを管理し、それぞれのモジュラな処理の中はモデル等への委譲だけで済ませることが出来ます。
上級編2:イベントバスを用いて、リアクティブに振る舞うようにする
もう一つの方法として、処理の結果をイベントバスという透過的な仕組みに委譲し、結果を待っているオブジェクトへイベントバスを経由して伝えるようにする方法があります。
このようにしておくと、コールバックの管理はすべてイベントバスの仕組み上で一元管理されるようになり、結果を受け取りたいオブジェクトでは、結果の種類ごとに定義されたイベントオブジェクトを引数に取るメソッドを宣言し、その中で必要な処理を記述すれば良いことになります。
コールバックの中で次の処理へつなげるような場合も、イベントを待ち受けているところで次へのイベントを投げればよいだけです。
これで、イベントバスの仕組み上でイベントの待ち受け管理をしてさえいれば、様々なレイヤのオブジェクトが生きている限り自立して処理をしていくようになります。
この仕組も、いくつかの実装が有ります。
ただし、イベントバスを用いる場合、結果をやりとりするためのイベントオブジェクトを別途定義しておく必要があります。これが、種類が増えれば増えるほどその分だけイベントオブジェクトも必要となるため、その管理コストを払わなければいけません。この辺りは、インタフェースとして定義するべきか、イベントオブジェクトを使うべきか議論が分かれるところになりそうです。
まとめ
- ライフサイクルのあるオブジェクトでコールバックを取り扱うときは、参照の管理に注意する
- モデルにコールバック管理を隠蔽できない場合(コールバック管理を明示的に行う必要がある場合)は、専用のメソッドを用意しておく
- コールバックに沢山のメソッドがある場合、可能であれば、中身の無い実装クラスを用意し、継承して使う側が使いたいものだけを選択的にオーバライド出来るようにしておく
- コールバックの順番が重要になる場合、Promise やイベントバスなどの仕組みを活用する