EventBus
本ページでは、EventBus3の使い方と、あまり触れられていないEventBus.getDefault()以外のスコープのEventBusインスタンスの作成方法、および便利だが注意が必要なThreadModeについて説明を行う。
そもそもなぜEventBusを使うのかというような点については以下を読むのが良いと思う。
Android でイベントバスを使う
私も最初は上記にたどり着いたが、いざ使おうと思って中身を読んだところEventBus2の記載であり、EventBus3では使い方が違うことが分かったので、EventBus3での例を書く。
また、掲載されている例が自分の使いたい用途と少し違っていたので、自分の用途だとどうすべきなのかについても記述する。
EventBusはグローバルスコープでしか使えないという誤認識が広まっているようなので、実はそうではないという点についても記載する。
なお、ThreadModeの指定により実行スレッドの制御が可能で、これは非常に便利な機能だと思うが、その挙動については注意が必要なことが分かったので、ソースコードを読んで分かった点についても記載する。
使い方
使い方は以下の通り簡単。
ただ、公式ページやその他のサイトでも、他のクラスからActivityに通知するというユースケースばかりで、Activityが他のクラスに通知するというユースケースが見つからなかったので、そのケースで書いておく。
build.gradleに以下を記載。
compile 'org.greenrobot:eventbus:3.0.0'
イベント用クラスを作成
public class MessageEvent {
private int eventType
public MessageEvent(int type) {
this.eventType = type;
}
public int getEventType() {
return eventType;
}
}
通知を受けたいクラス
public class Subscriber {
@Subscribe
public void onEvent(MessageEvent event) {
// do something
}
}
通知を送るMainActivity
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
private Subscriber mSubscriber;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSubscriber = new Subscriber();
}
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(mSubscriber);
}
@Override
protected void onStop() {
super.onStop();
EventBus.getDefault().unregister(mSubscriber);
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected(MenuItem item) {
// Send event
EventBus.getDefault().post(new MessageEvent(item.getItemId()));
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
}
}
ポイントは、EventBus.getDefault().register()
にて、通知を受けたいインスタンスを登録すること。
EventBusでは、内部で、registerされたインスタンスの集合を保持しており、EventBus.getDefault().post()
が呼ばれた際に、インスタンス集合の一つ一つについて、post時に引数となっているクラスの型を@Subscribe
しているメソッドがあるかどうかを検査しており、該当するものがあったら、そのメソッドを呼ぶという手順で通知が行われる。
この例では、
EventBus.getDefault().post(new MessageEvent(item.getItemId()));
が呼ばれているので、EventBusにregisterされたインスタンス(今回はmSubscriberしかない)において@Subscribe
が定義されたメソッドの中から、MessageEventを引数としているメソッドが呼ばれる。
Scopeの異なるEventBusインスタンスの定義
私が見てきた公式ページや記事では、EventBus.getDefault()
を使ったデフォルトのEventBusインスタンスを使った例しか紹介されていなかった。EventBus.getDefault()
だけを使う場合、register()
にて登録したインスタンス全てがpost先のスコープとなる。これは、お手軽な反面、オブジェクトのスコープは可能な限り狭い方がよいという経験則に反することになるし、クラス数が増えると、実際にどのメソッドが呼ばれるのかが分かりにくくなる。
あまり知られていないが、実は、EventBus.getDefault()
はEventBusのシングルトンインスタンスを返す便利メソッドというだけであって、ユーザーがnew EventBus()
を実行することで異なるEventBusインスタンスを生成することも可能だ。EventBusインスタンス間でregister()
されたインスタンスの共有等は行われていないため、生成したEventBusインスタンスに特定のインスタンスだけをregister()
することで、EventBusインスタンス毎にスコープを設定することができる。
以下に、ViewModel1を対象にしたEventBusインスタンスと、ViewModel2を対象にしたEventBusインスタンスを作成する例を示す。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mEventBusForViewModel1 = new EventBus();
mEventBusForViewModel2 = new EventBus();
mViewModel1 = new ViewModel1();
mViewModel2 = new ViewModel2();
}
@Override
protected void onStart() {
super.onStart();
mEventBusForViewModel1.register(mViewModel1);
mEventBusForViewModel2.register(mViewModel2);
}
@Override
protected void onStop() {
super.onStop();
mEventBusForViewModel1.unregister(mViewModel1);
mEventBusForViewModel2.unregister(mViewModel2);
}
以下のDroidKaigi2017で大人気だった発表でも、
DataBindingで実現するMVVM Architecture
EventBusはグローバルになるのでどこから通知が来るのかが分かりにくい
と記載があり、おそらく発表者もEventBus.getDefault()
を使った使用方法しかないと認識をされているのではないかと思う。それだけEventBusをnewするという方法が知られていないということだと思う。
(断っておくが、上記発表に対する批判では毛頭ない。よくある誤解の例として挙げさせてもらった。)
ThreadMode
@Subscribe
においてThreadModeを指定することで、どのスレッドで実行するかの設定ができる。
POSTING
@Subscribe(threadMode = ThreadMode.POSTING)
を指定すると、postを行ったスレッドと同一のスレッドで、スレッドをインタラプトして動作する。ゆえに、@Subscribe
しているメソッドが終了するまで、postが返ってこない。
MainThreadからpostして、@Subscribe
側が重い処理をするとANRするので注意が必要。
(new Handler()).postの場合はpost行為は直ちに終了だったはずなので、その点異なっている。
- post側:MainThread -> subscribe側:MainThread
- post側:SubThread1 -> subscribe側:SubThread1
MAIN
@Subscribe(threadMode = ThreadMode.MAIN)
を指定すると、必ずプロセスのMainThreadで実行される。もしpostした側もMainThreadで動作している場合は、POSTINGを指定した場合と同様に、スレッドをインタラプトして動作する。ゆえに、@Subscribe
しているメソッドが終了するまで、postが終わらない。
- post側:MainThread -> subscribe側:MainThread
- post側:SubThread1 -> subscribe側:MainThread
BACKGROUND
@Subscribe(threadMode = ThreadMode.BACKGROUND)
を指定すると、MainThread以外のスレッドで処理が実行される。post側がMainThreadでなかった場合は、POSTINGを指定した場合と同様に、スレッドをインタラプトして動作する。ゆえに、@Subscribe
しているメソッドが終了するまで、postが返ってこない。
post実行がMainThreadの場合は、
ExecutorService service = Executors.newCachedThreadPool();
service.execute(runnable);
をしているのとほぼ同等の処理が行われている。これにより、(よっぽど多数のpostが実行されない限りは、)各処理はすべて並行に実行されるため、@Subscribe
しているメソッド間でのブロックが生じない。
- post側:MainThread -> subscribe側:SubThread2 or 3 or 4 or ...
- post側:SubThread1 -> subscribe側:SubThread1
BACKGROUND指定の注意点
post側がMainThreadの場合は、@Subscribe
側の処理の直列化(逐次化)が行われないということなので、@Subscribe
しているメソッド内部で守るべきリソースがある場合には、適切に保護しないといけない。
例を示すと、
int resource = 0;
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void handleEvent() {
resource++;
}
private void postManyEvents() {
for(int i=0; i<10000; i++) {
EventBus.getDefault().post();
}
}
という形で10000回のpostを行った場合、Subscriberのresourceは10000になっている保証はない(++演算子はアトミックに動作しないため)。
この問題を解決するためには、以下のようにアトミックなクラスを利用するか、syncronizedブロック等で保護すれば良い。
AtomicInteger resource = new AtomicInteger(0);
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void handleEvent() {
resource.incrementAndGet();
}
private void postManyEvents() {
for(int i=0; i<10000; i++) {
EventBus.getDefault().post();
}
}
ASYNC
@Subscribe(threadMode = ThreadMode.ASYNC)
を指定していると、postしたスレッドがなにであるかにかかわらず、必ずバックグラウンドスレッドで処理が実行される。
post側がMainThreadである場合のBACKGROUND指定と同一の挙動である。つまり、BACKGROUND指定の際の注意点と同等の注意が必要。
- post側:MainThread -> subscribe側:SubThread2 or 3 or 4 or ...
- post側:SubThread1 -> subscribe側:SubThread2 or 3 or 4 or ...
ThreadModeの使い分け
MainThreadにて極力処理を行わないほうがいいが、かといって全て別スレッドだと予期せぬところでデータ不整合がおこったりExceptionしたりするので、使い分けが難しい。
MainThreadとWorkerThreadの2つだけを使うというのが安全とレスポンスのバランスが一番良いのではないかと思うので、基本はPOSTINGを指定して、メソッド内でWorkerThreadに処理を投げるという方針が結局一番安全なのかもしれない。
@Subscribe
側でMainThreadでの実行が必要な処理がある場合にはMAIN指定が必要だろう。
また、並行動作しても何も問題のない処理についてはASYNCを指定するのが良いのではないかと思う。
最後に
Androidのコードをコールバック地獄から開放するEventBus3について記述した。
ThreadModeについては案外直感的でない動きをするので、注意が必要。
@WorkerThread
とかそういう指定をすると、単一のSubThreadにて直列的に仕事をしてくれるとかあると個人的にはもっと使いやすそうなので、自分で作ってしまおうか。