【Android】Serviceとのプロセス間通信でデータを送受信する

AndroidではServiceを実装することでプロセス間で通信することが出来ます。

今回はMessengerを使ったServiceとActivityのデータの送受信方法について説明します。

Serviceとは?

ドキュメントによるとこう説明されています。

  • サービスは、ユーザが異なるアプリケーションにいても作業を行うためにバックグラウンドで実行することができます。
  • サービスを使って双方向のやり取りを行い、プロセス間通信を行うために、他のコンポーネントがサービスにバインドすることが許されています。
  • サービスはデフォルトではホストのアプリケーションのメインスレッドで実行されます。

言ってしまえば、「Intentからのみ呼び出せるライブラリ / プラグイン」です。

当然Intent Filterを適切に設定すればどんなアプリケーションからも呼び出せます。と言うか、ManifestにIntent Filterを記述しないとそもそも呼び出すことが出来ません。

Serviceの呼び出し方

Serviceを呼び出すには呼び出し元のActivity(クライアント)からContext#startService (Intent service) を使う方法とContext#bindService (Intent service, ServiceConnection conn, int flags)を使う方法があります。どちらを使うにしても、actionやcategoryを指定する暗黙的Intentで呼び出します。

startServiceの方は一発使いきり、と考えていいです。何らかのデータ(テキスト、画像、動画etc…)をIntent経由で渡し、適当に処理してもらいます。コールバックが必要な場合はBroadcastReceiverを使用します。(この辺の話をすると記事が一つ書けてしまうので具体的な説明は割愛。参考:ブロードキャストレシーバの実装によるアクティビティとサービスの通信 - Android 開発入門

(2014/10/31追記:[Android][Twitter4J]Oauth認証をServiceで行う(あるいは、Serviceの結果をBroadcastReceiverで受け取る)と言う記事で解説しました。)

bindServiceはその名の通り、クライアント⇔Service間の接続をバインドし、クライアントは必要な時にバインドしたServiceにメッセージやデータを送ることが出来ます。

Serviceのバインドと双方向間通信

Serviceのバインドに関してはこのドキュメントに詳しく書かれています。そして双方向間の通信をするために以下の三つの方法が記述されています。

Binderクラスを拡張する方法はクライアントとServiceが同じアプリケーション内にあることが前提です。それは以下の二つでも問題なく動作するのであんまり使うことはないと思います。

AIDLはかなり低レイヤな部分をいじることでプロセス間通信を可能にします。「ほとんどのアプリケーションでは、マルチスレッドの機能が必要になり、さらに複雑な実装になってしまう可能性があることから、AIDL を使ってバインドされたサービスを作成すべきではありません。」とまで書かれるほど面倒です。一応AIDLのドキュメントもありますが、この記事では説明しません。

と言うわけで、今回はMessengerを介した双方向間通信の実装方法について説明します。

Service / クライアントの最低限の実装

まずはServiceを継承したクラスを作成しましょう。

abstractなメソッドonBind(Intent intent)のみです。

public class TestService extends Service {

    @Override
    public IBinder onBind(Intent i) {
        Toast.makeText(getApplicationContext(), "Bindしました", Toast.LENGTH_SHORT).show();
        //TODO: return IBinder
    }
}

ここで返すIBinderは何が適切なのでしょうか?そもそもこれを返すとどうなるのでしょうか?

その答えはContext#bindServiceで渡すServiceConnectionにあります。次にこれを実装したActivity(クライアント)を用意しましょう。

public class MainActivity extends Activity implements ServiceConnection {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Toast.makeText(getApplicationContext(), "サービスに接続しました", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Toast.makeText(getApplicationContext(), "サービスから切断しました", Toast.LENGTH_SHORT).show();
    }
}

Service#onBindで返されるIBinderはServiceConnection#onServiceConnectedのserviceです。この返ってきたIBinderを操作することで、Service⇔クライアントで通信することが出来ます。

が、IBinderそのものでやりとりするのは流石に厳しいので、もっと便利なクラスでラップします。それがMessengerクラスです。

MessengerクラスのコンストラクタにはHandlerを受け取るものがあります。Messenger#send(Message message)メソッドを使うことで、そのHandlerに対してMessageを渡すことが出来ます。Messageを受け取ったHandlerはHandler#handleMessage(Message msg)メソッドでそれを処理します。

後はコードを見せてしまったほうが早いでしょう。Serviceとクライアントに以下のようなコードを追記します。

public class TestService extends Service {

    private Messenger _messenger;

    static class TestHandler extends Handler {

        private Context _cont;

        public TestHandler(Context cont) {
            _cont = cont;
        }

        @Override
        public void handleMessage(Message msg) {
            Toast.makeText(_cont, "Messageを受信しました", Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    public void onCreate() {
        super.onCreate();
        _messenger = new Messenger(new TestHandler(getApplicationContext()));
    }
    
    @Override
    public IBinder onBind(Intent i) {
        Toast.makeText(getApplicationContext(), "Bindしました", Toast.LENGTH_SHORT).show();
        return _messenger.getBinder();
    }

}
public class MainActivity extends Activity implements ServiceConnection{

    private Messenger _messenger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Toast.makeText(getApplicationContext(), "サービスに接続しました", Toast.LENGTH_SHORT).show();
        _messenger = new Messenger(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Toast.makeText(getApplicationContext(), "サービスから切断しました", Toast.LENGTH_SHORT).show();
        _messenger = null;
    }

}

ついでに忘れないうちにAndroidManifest.xmlにServiceを登録しておきましょう。これがないとIntentを受信することが出来ません。

<service android:name="com.example.servicetest.service.TestService">
    <intent-filter>
        <action android:name="com.example.servicetest.service.TestService" />
    </intent-filter>
</service>

後はMainActivity(クライアント)で保持している_messangerに対してsendメソッドを発行していけば、TestServiceで定義されているTestHandlerのhandleMessageメソッドで処理してくれる、と言う寸法です。

クライアント→Serviceの通信

そんなわけで次はクライアント側の細かい実装に入っていきましょう。適当なレイアウトファイルを用意します。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/btnConnect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="接続"
        android:onClick="connect" />

    <Button
        android:id="@+id/btnSend"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/btnConnect"
        android:layout_below="@+id/btnConnect"
        android:layout_marginTop="25dp"
        android:enabled="false"
        android:onClick="send"
        android:text="メッセージ送信" />

    <Button
        android:id="@+id/btnDisconnect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/btnSend"
        android:layout_below="@+id/btnSend"
        android:layout_marginTop="22dp"
        android:enabled="false"
        android:onClick="disconnect"
        android:text="切断" />

</RelativeLayout>

クリックイベントをガシガシ書いていきます。

public class MainActivity extends Activity implements ServiceConnection{

    private Messenger _messenger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Toast.makeText(getApplicationContext(), "サービスに接続しました", Toast.LENGTH_SHORT).show();
        _messenger = new Messenger(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Toast.makeText(getApplicationContext(), "サービスから切断しました", Toast.LENGTH_SHORT).show();
        _messenger = null;
    }

    /**
     * サービスへの接続
     * @param v
     */
    public void connect(View v) {
        bindService(new Intent("com.example.servicetest.service.TestService")
            , this, Context.BIND_AUTO_CREATE);
        findViewById(R.id.btnSend).setEnabled(true);
        findViewById(R.id.btnDisconnect).setEnabled(true);
        v.setEnabled(false);
    }

    /**
     * メッセージの送信
     * @param v
     */
    public void send(View v) {
        //TODO: send message
    }

    /**
     * サービスからの切断
     * @param v
     */
    public void disconnect(View v) {
        unbindService(this);
        findViewById(R.id.btnConnect).setEnabled(true);
        findViewById(R.id.btnSend).setEnabled(false);
        v.setEnabled(false);
    }

}

具体的なMessageの送信方法ですが、Messageのstaticメソッドであるobtainメソッドを使うのが一番楽だと思います。

/**
 * メッセージの送信
 * @param v
 */
public void send(View v) {
    try {

        // メッセージの送信
        _messenger.send(Message.obtain());

    } catch (RemoteException e) {
        e.printStackTrace();
        Toast.makeText(getApplicationContext(), "メッセージの送信に失敗しました。", Toast.LENGTH_SHORT).show();
    }
}

極端な話、「クライアントからServiceに対して通信する」と言う意味ではこれだけでOKです。

クライアント→Serviceへデータを渡す

では次にクライアント側からServiceにデータを渡してみましょう。

Message#obtainには色々なオーバーロードがあり、その中にobtain(Handler h, int what, Object obj)なんてものがあります。このobjに何かを渡すとpublicなフィールドにセットされるので、そこから取得することが出来ます。

/**
 * メッセージの送信
 * @param v
 */
public void send(View v) {
    try {

        // メッセージの送信
        _messenger.send(Message.obtain(null, 0, "hoge"));

    } catch (RemoteException e) {
        e.printStackTrace();
        Toast.makeText(getApplicationContext(), "メッセージの送信に失敗しました。", Toast.LENGTH_SHORT).show();
    }
}

Service側はwhatで分岐し、objからデータを取得します。

@Override
public void handleMessage(Message msg) {
    switch(msg.what) {
        case 0:
            Toast.makeText(_cont, (String)msg.obj, Toast.LENGTH_SHORT).show();
            break;
        default:
            Toast.makeText(_cont, "Messageを受信しました", Toast.LENGTH_SHORT).show();
            super.handleMessage(msg);
    }
}

また、他にもMessageはBundleのsetter / getterがあるので、それを使ってもいいでしょう。と言うか、そっちの方が便利だと思います。

ただし、Bundleが渡せるからと言って自作クラスをputSerializableしても流石にアプリ間でのやりとりは出来ません。(普通に考えたら当たり前だが、他アプリから渡されたオブジェクトをデシリアライズ出来ない。)

Parcelableならなんとかなるので、頑張ってそっちを実装すれば自作クラスでもやりとり可能です。

勿論同アプリ内であればSerializableな自作クラスを受け渡すことが出来ます。


public class TestData implements Serializable {

    private static final long serialVersionUID = -8710373208447953446L;

    private String name;
    private String value;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object o) {
        if(o != null && !(o instanceof TestData)) return false;
        return this.name.equals(((TestData)o).getName());
    }
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
    @Override
    public String toString() {
        return this.name;
    }
}
/**
 * メッセージの送信
 * @param v
 */
public void send(View v) {
    try {

        // メッセージにBundleを付与して送信
        TestData data = new TestData();
        data.setName("piyo");
        data.setValue("huga");
        Bundle arg = new Bundle();
        arg.putSerializable("testData", data);
        Message msg = Message.obtain(null, 1);
        msg.setData(arg);


        _messenger.send(msg);

    } catch (RemoteException e) {
        e.printStackTrace();
        Toast.makeText(getApplicationContext(), "メッセージの送信に失敗しました。", Toast.LENGTH_SHORT).show();
    }
}
@Override
public void handleMessage(Message msg) {
    switch(msg.what) {
        case 0:
            Toast.makeText(_cont, (String)msg.obj, Toast.LENGTH_SHORT).show();
            break;
        case 1:
            Bundle arg = msg.getData();
            TestData data = (TestData) arg.getSerializable("testData");
            Toast.makeText(_cont, data.toString(), Toast.LENGTH_SHORT).show();
            break;
        default:
            Toast.makeText(_cont, "Messageを受信しました", Toast.LENGTH_SHORT).show();
            super.handleMessage(msg);
    }
}

Service→クライアントへの通信

MessageにはreplyToと言うpublicなフィールドがあります。そしてこのフィールドの型はMessangerです。もうおわかりですね。今度は全く逆のことをすればいいわけです。

と言うわけで、クライアント側にもHandlerを定義し、Messanger#sendを呼ぶ前にMessageのreplyToにそれをセットします。

public class MainActivity extends Activity implements ServiceConnection{

    private Messenger _messenger;
    private Messenger _replyMessanger;

    static class ReplyHandler extends Handler {

        private Context _cont;

        public ReplyHandler(Context cont) {
            _cont = cont;
        }

        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case 2:
                    Toast.makeText(_cont, (String)msg.obj, Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Toast.makeText(getApplicationContext(), "サービスに接続しました", Toast.LENGTH_SHORT).show();
        _messenger = new Messenger(service);
        _replyMessanger = new Messenger(new ReplyHandler(getApplicationContext()));
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Toast.makeText(getApplicationContext(), "サービスから切断しました", Toast.LENGTH_SHORT).show();
        _messenger = null;
        _replyMessanger = null;
    }

    /**
     * サービスへの接続
     * @param v
     */
    public void connect(View v) {
        bindService(new Intent("com.example.servicetest.service.TestService")
            , this, Context.BIND_AUTO_CREATE);
        findViewById(R.id.btnSend).setEnabled(true);
        findViewById(R.id.btnDisconnect).setEnabled(true);
        v.setEnabled(false);
    }

    /**
     * メッセージの送信
     * @param v
     */
    public void send(View v) {
        try {
            // メッセージにBundleを付与して送信
            TestData data = new TestData();
            data.setName("piyo");
            data.setValue("huga");
            Bundle arg = new Bundle();
            arg.putSerializable("testData", data);
            Message msg = Message.obtain(null, TestService.M_SEND_BUNDLE);
            msg.setData(arg);

            // コールバックを設定
            msg.replyTo = _replyMessanger;

            _messenger.send(msg);

        } catch (RemoteException e) {
            e.printStackTrace();
            Toast.makeText(getApplicationContext(), "メッセージの送信に失敗しました。", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * サービスからの切断
     * @param v
     */
    public void disconnect(View v) {
        unbindService(this);
        _messenger = null;
        _replyMessanger = null;
        findViewById(R.id.btnConnect).setEnabled(true);
        findViewById(R.id.btnSend).setEnabled(false);
        v.setEnabled(false);
    }

}

ServiceはhandleMessage内でreplyToを取得、nullでなければsendメソッドを呼び出します。


public class TestService extends Service {

    private Messenger _messenger;

    static class TestHandler extends Handler {

        private Context _cont;

        public TestHandler(Context cont) {
            _cont = cont;
        }

        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case 0:
                    Toast.makeText(_cont, (String)msg.obj, Toast.LENGTH_SHORT).show();
                    break;
                case 1:
                    Bundle arg = msg.getData();
                    TestData data = (TestData) arg.getSerializable("testData");
                    Toast.makeText(_cont, data.toString(), Toast.LENGTH_SHORT).show();
                    break;
                default:
                    Toast.makeText(_cont, "Messageを受信しました", Toast.LENGTH_SHORT).show();
                    super.handleMessage(msg);
            }

            Messenger reply = msg.replyTo;
            if(reply != null) {
                try {
                    reply.send(Message.obtain(null, 2, "受信が終わりました"));
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Override
    public void onCreate() {
        super.onCreate();
        _messenger = new Messenger(new TestHandler(getApplicationContext()));
    }

    @Override
    public IBinder onBind(Intent i) {
        Toast.makeText(getApplicationContext(), "Bindしました", Toast.LENGTH_SHORT).show();
        return _messenger.getBinder();
    }

}

これで双方向間通信が実現出来ました。

まとめ

今回作ったものもGitHubにあげておきました。

参考