LoginSignup
15
20

More than 5 years have passed since last update.

EventBus3の使い方とThreadModeについて

Last updated at Posted at 2017-03-03

EventBus

本ページでは、EventBus3の使い方と、あまり触れられていないEventBus.getDefault()以外のスコープのEventBusインスタンスの作成方法、および便利だが注意が必要なThreadModeについて説明を行う。
そもそもなぜEventBusを使うのかというような点については以下を読むのが良いと思う。
Android でイベントバスを使う

私も最初は上記にたどり着いたが、いざ使おうと思って中身を読んだところEventBus2の記載であり、EventBus3では使い方が違うことが分かったので、EventBus3での例を書く。
また、掲載されている例が自分の使いたい用途と少し違っていたので、自分の用途だとどうすべきなのかについても記述する。
EventBusはグローバルスコープでしか使えないという誤認識が広まっているようなので、実はそうではないという点についても記載する。
なお、ThreadModeの指定により実行スレッドの制御が可能で、これは非常に便利な機能だと思うが、その挙動については注意が必要なことが分かったので、ソースコードを読んで分かった点についても記載する。

使い方

使い方は以下の通り簡単。
ただ、公式ページやその他のサイトでも、他のクラスからActivityに通知するというユースケースばかりで、Activityが他のクラスに通知するというユースケースが見つからなかったので、そのケースで書いておく。

build.gradleに以下を記載。

build.gradle
compile 'org.greenrobot:eventbus:3.0.0'

イベント用クラスを作成

MessageEvent.java
public class MessageEvent {
    private int eventType
    public MessageEvent(int type) {
        this.eventType = type;
    }

    public int getEventType() {
        return eventType;
    }
}

通知を受けたいクラス

Subscriber.java
public class Subscriber {
    @Subscribe
    public void onEvent(MessageEvent event) {
        // do something
    }
}

通知を送るMainActivity

MainActivity.java
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インスタンスを作成する例を示す。

DifferentScopeActivity.java
    @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しているメソッド内部で守るべきリソースがある場合には、適切に保護しないといけない。

例を示すと、

Subscriber.java
int resource = 0;

@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void handleEvent() {
    resource++;
}
Publisher.java
private void postManyEvents() {
    for(int i=0; i<10000; i++) {
        EventBus.getDefault().post();
    }
}

という形で10000回のpostを行った場合、Subscriberのresourceは10000になっている保証はない(++演算子はアトミックに動作しないため)。
この問題を解決するためには、以下のようにアトミックなクラスを利用するか、syncronizedブロック等で保護すれば良い。

Subscriber.java
AtomicInteger resource = new AtomicInteger(0);

@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void handleEvent() {
    resource.incrementAndGet();
}
Publisher.java
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にて直列的に仕事をしてくれるとかあると個人的にはもっと使いやすそうなので、自分で作ってしまおうか。

15
20
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
15
20