LoginSignup
355
359

More than 5 years have passed since last update.

コールバックと上手に付き合う

Last updated at Posted at 2014-08-25

コールバック、よく使いますよね。
非同期処理の結果を受け取るには、必ずと言っていいほど付き合うことになるコールバックですが、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からのコールバックをActivityFragmentで受ける場合、既に継承関係が出来てしまっているので、直接コールバックインタフェースを実装することになります。

上級編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 やイベントバスなどの仕組みを活用する
355
359
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
355
359