6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NSSOL その2Advent Calendar 2019

Day 19

Android端末内マイクロサービスという謎アーキテクチャの提案

Last updated at Posted at 2019-12-18

Android + Microservices

Androidアプリのバックエンドをマイクロサービスにする話ではありません。
Android端末内で複数アプリを用いたマイクロサービスアプリ開発です!!
つまり、変態近未来アーキテクチャの紹介です。(普通のアプリを書いている人には恐らく無縁の技術です。)
でも、一部有用な内容もあるので、とりあえず読んでいってください。

導入

Androidは、ほかのアプリのActivityやServiceにIntentを飛ばせます。
暗黙的IntentとActionを使う方法(特定の機能を持つアプリに遷移する)は有名だと思います。

しかし、この暗黙的Intentを使う方法には一つ大きな欠点があります。

Serviceを叩けないという最大の問題です。
起動しているアプリの裏で複数のアプリが連携するためにはActivityではなく、Serviceを動かす必要があります。

どうしても複数のServiceが連携し、Microservice Android Applicationを実現したい・・・!!

この野望に答える手法がありました。
実は明示的Intent(特定のクラスに遷移する)で、他のアプリのActivityやServiceを直接呼ぶことができます。

実際のところ、他のアプリのActivityやServiceを明示的Intentで呼び出すことなど、普通はありえません。
何故ならば、ユーザが呼び出し先のアプリをインストールしてくれている保証が無いからです。
しかし、この手法を使えば複数のアプリがデータをやり取りして一つの機能を実現するマイクロサービスAndroidアプリケーションが作れます!
有用なユースケースは思いつきませんが、面白いと思いませんか?

あと、Microservice Android Applicationがかっこいい・・・

(Dynamic Feature Moduleに似ていますが、よりダイナミックになっています)

目次

  • Android内マイクロサービスアーキテクチャの説明
    • 基本編: Serviceとデータやり取りする方法
    • 変わった人向け: 他のアプリのServiceやActivityを呼び出す方法
    • 変態向け: 端末内にインストールされているアプリのスキャン方法
  • で結局何ができるの?

Android内マイクロサービスアーキテクチャの説明

例えばこんな構成が作れます

スクリーンショット 2019-12-14 14.47.28.png

Host AppからPlugin Appに対してサーバへの通信を依頼し、最終的にその結果を受け取るアプリが作れます!
(別のアプリに分ける必要がある?というツッコミは野暮ってもんですよ。)

基本編: Serviceとデータをやり取りする方法(アプリ内、アプリ間共通)

AndroidのServiceは主に4種類あります。サービスについて

  • background Service
  • Intent Service
  • foreground Service
  • bind Service

(実はIntentServiceはbackgroundServiceの仲間だったりするのですが、分かりやすく4種類としています)

このうち、呼び出し元に値を返しやすいのは

  • Intent Service
  • bind Service
    です。
    IntentServiceはresultReceiverというクラスを使って呼び出し元に値を返すことができます。

以下、resultReceiverのサンプルコードです。

//ResultReceiverの引数は実行スレッドを指定するもの。nullなら任意のスレッド
this.startIntentService(Hoge::class.java, object: ResultReceiver(null) {
    override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
        when (resultCode) {
            // 予めIntent先と決めておいた定数
            RESULT_CODE -> {
                resultData?.getSerializable(Fuga)
                // 以下略
            }
            else -> {}
        }
    }
})

BindServiceは3種類の値の返し方がありますが、よく使われるのはMessengerクラスを使った方法です。
Messengerについてはこの後詳しく説明します。
3種類の返し方については公式に詳しく書いてあります

ここまでは一般的なServiceの話です。ここからアプリ間通信の話に踏み込みます。

変わった人向け: 他のアプリのServiceやActivityを呼び出す方法

bindServiceは他のアプリから呼び出せますが、IntentServiceは呼び出せません。
なので、ここから先はbindServiceを使います。bindServiceについてはこちら

基本的にはアプリ内のbindServiceを呼び出す方法と変わりません。
しかし、他アプリゆえに気をつけるポイントが3点あります。

serviceの可視性をexported=trueにする

AndroidManifest.xmlのService項目にexported=trueを設定しましょう。
この設定が無いとアプリ外からIntentで呼べません。

AndroidManifest.xml

<service
    android:name=".HogeService"
    android:enabled="true"
    android:exported="true">
</service>

Messengerクラスでデータのやり取りをする

Host AppもPlugin Appも互いのクラスを知りえません(別アプリなので当然)。
なので、IBinderを拡張する方法は使用できません。
MessengerBundleを詰めてやり取りします。
HTTP通信で例えるならば、jsonに値を詰めてやり取りするイメージです。

class SampleBindService : Service() {
    private val messageHandler = Handler { msg ->
        when (msg.what) {
            // msg.whatに0が来たらログに吐く
            0 -> {
                Log.d("SampleBindService", "message: ${msg.obj}")
            }
            else -> {}
        }
        val bundle = Bundle().also {
            it.putString("sample", "This is test.")
        }
        // what=0にbundleデータを詰めてreplyする
        msg.replyTo?.send(Message.obtain(null, 0, bundle))
        // replyは何度でも送ることができる
        msg.replyTo?.send(Message.obtain(null, 0, bundle))
        true
    }
    private val messenger: Messenger = Messenger(this.messageHandler)

    override fun onBind(intent: Intent): IBinder {
        return this.messenger.binder
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }
}

Host AppはPlugin AppのapplicationIdとサービスのclassName(FQDN)を知っていること

Intentで呼び出すときに、呼び出し先のapplicationIdclassNameが必要になります。
つまり、なんらかの方法でHost AppはPlugin Appの上記2つの情報を知る必要があります。
そして、その方法は変態向け: 端末内にインストールされているアプリのスキャン方法で解説します。

val intent = Intent().also {
    it.setClassName(applicationId, className)
}

変態向け: 端末内にインストールされているアプリのスキャン方法

ここから先はほとんど使う人が居なさそうな機能の紹介です。

先ほど、Host AppはPlugin AppのapplicationIdclassNameを知っていなければならない。という話をしました。
1人でHost AppとPlugin Appの両方を作る場合は、これらの情報を知っているはずなので問題ありません。
一方で、Microserviceのように連携する場合やPlugin AppがHost Appの機能拡張アプリであった場合、Host AppはPlugin Appの存在を知りえません。(プラグインは本体を意識するが、本体がプラグインを意識したら変ですよね?)

では、いかにしてHost AppはPlugin Appの存在を知るのか。
その答えは・・・Host Appが端末内にインストールされているアプリをスキャンすれば良いのです。
(実はAndroidのアプリは端末内のアプリをスキャンすることができます。)

詳しくはこの記事をお読み下さい。
ただ、1点上記の記事と異なる点があります。上記の記事ではActivityを呼び出していましたが、今回はServiceを呼び出します。
LAUNCHERとして登録されているActivityはアプリスキャンで検索できるのですが、それ以外のActivityとServiceは検索できません。(少なくとも私は知らないので、知っていたら教えてください。)
ですので、ServiceのclassNameはスキャンしても知ることはできません。
なので、苦肉の策として、Plugin Appが提供するServiceは
applicationIdと同じパッケージの直下にPluginServiceという名前で置く
などの約束をして、Host AppがIntentを飛ばせるようにする必要があります。

スキャンをした後にIntentを飛ばすサンプルコードが以下です。

val pm = packageManager
val packageInfoList = pm.getInstalledPackages(
    PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES
)
val targetPackageName = packageInfoList
    // 特定のパッケージで始まるアプリを検索対象とする
    .filter { it.packageName.startWith("jp.co.hoge.fuga") }
    .map { it.packageName }
}.first() // 今回は見つかる前提。見つからないとここで落ちる

val intent = Intent().also {
    // 上記で得られたapplicationIdとclassNameを詰める(classNameはパッケージ直下にPluginServiceで固定)
    it.setClassName(targetPackageName, targetPackageName + ".PluginService")
}

val connection = object: ServiceConnection {
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        val messenger = Messenger(service)
        messenger.send(Message.obtain(null, 0, null).also {
            it.replyTo = Messenger(ReplyHandler())
        })
    }

    override fun onServiceDisconnected(name: ComponentName) {
    }
}

    bindService(intent, connection)
}

で、結局何ができるの?

例えば、私が思いつくのは以下のような物です。
しかし、複数アプリを連携させてまでやるものかと言われると。うーん

案1: ニュースまとめアプリ

各種ニュースサイトをまとめて見れるアプリ。
各Plugin Appはそれぞれのニュースサイトからの情報を取ってくる昨日を有し、
Host AppはPlugin Appから得たデータを元に、画面を作る。

ユーザはPlugin Appを入れれば入れるほど、様々なニュースサイトから情報を集められるようになる。

(でも、それってDynamic Feature Moduleでよくね?そもそもプラグイン構成取る必要ある?最初から様々なサイトから集めておけよ。)

案2: みんなで情報提供Serviceを公開し合う文化を作り、Microserviceを実現

この記事を読んでくれた人がAndroidを書いたときに、何かしらのデータを提供するbindServiceを作り、それを外部に公開したとする。
そして、そのApplicationIdclassNameを公開したとする。
これらの情報が蓄積されれば、互いに互いのServiceを叩き合うMicroserviceの誕生!!かっこいい!!

(セキュリティ的に大丈夫なんかとか、依存したいアプリが入っているかわからない問題が課題。)

何か使える案下さい

面白そうな機能を見つけたので、変な構想を練って見ましたが、何に使えるのかイメージが湧きません・・・
一番のネックは、ユーザがアプリを入れてくれるとは限らない点ですね。
そこさえ克服できれば、アプリ間をService同士が連携し合う、Android内Microserviceアーキテクチャが陽の目を見る日も近い!

6
4
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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?