2013年4月12日金曜日

startService()とbindService()の4つの使い分け

 startService()で実行するサービスは、バックグラウンドで動き続けるという特徴があります。bindService()で実行するサービスとは、相互通信が行えるという特徴があります。

 startService()で実行するサービスは自身で停止できますが、bindService()で実行するサービスはクライアントの接続がなくなるまで停止すべきではありません。

 startService()で実行するサービスは、どちらかというと指示を受けた後、自立的に動作するようなケースで使われることが多いのに対し、bindService()で実行するサービスはIPCで明確に指示されて動作するケースとなるでしょう。

 startService()は、どのコンテキストでも呼び出せますが、bindService()はBroadcastReceiverのコンテキストからは呼び出せません。

 サービスを使用して何かを実現する場合、startService()でもbindService()でもどちらでも実現可能であるケースが多いと思いますが、上記を参考に設計してみてください。

プロセス間通信時のデータをバイト配列化する「Parcel」

 Androidの根幹をなすInetntによるメッセージングを支える技術として「Parcel」「Parcelable」というものが存在します。これは、普通にAndroidアプリを作成していても特に気が付かないかもしれませんが、とても多くの場所で使われています。

 誤解を恐れずに簡潔に説明すると、ParcelableはJavaの「Serializable」で、ParcelはParcelable専用のストリームです。Parcelは、カーネルを経由してプロセス間で受け渡しされるデータを抽象化したクラスです。

 Parcelというのは日本語では「小包」という意味で、Androidにおいては小さいデータを意味します(具体的にはデータが128bytes以内であれば、データコピーのオーバーヘッドが抑えられるように設計されています)。

 Parcelのメソッド

 Parcelには、データを書き込む「writeXxx」というメソッドと、書き込んだデータを読み出す「readXxx」というメソッドが存在します。

 書き込まれたデータはJavaヒープの外にバイト配列として展開されます。データが追加されるたびに「realloc」で領域が確保されます。Javaヒープの外にデータを保持するのは、プロセス間通信でカーネルモジュールがダイレクトにアクセスできるようにするためです。

 Parcelには、「marshall()」「unmarshall(byte[], int, int)」が用意されており、それぞれ、Parcelをbyte[]に変換するメソッドと、byte[]からParcelに変換するメソッドです。これらのメソッドはParcelableをバイト配列に変換したり、バイト配列からParcelableに戻したりできるため、プロセス間通信以外でも便利に使えます。

 Parcelは永続化やマシン間通信には使用しないように

 ただ残念ながら、Parcelableが入れ子になっていると、marshall()が行えない(例外が発生する)という制限が存在します。Parcelを使用すると、制限の範囲内でインスタンスをストレージに保存して永続化可能です。

 ただしParcelは、あくまでもプロセス間通信などの一時的なデータの受け渡しに使うことを想定しており、将来的なデータフォーマットの互換を保証するものではないため、Javadocでは永続化やマシン間通信には使用しないように忠告しています。

 ParcelとAIDLで扱えるデータ型

 Parcelに読み書き可能なデータ型は、AIDL同様に制限されています。以下の表は、ParcelとAIDLで扱えるデータ型の一覧です。

種別 Parcelの型 AIDLの型 補足
プリミティブ byte byte
double double
float float
int int
long long
String String 文字列はプリミティブ扱い
プリミティブ配列 boolean[] boolean[]
byte[] byte[]
char[] char[]
double[] double[]
float[] float[]
int[] int[]
long[] long[]
String[] String[]
SparseBooleanArray × キーがint、値がbooleanのマップ型
Parcelables Parcelableを実装したクラスと、その配列すべて Parcelableを実装したクラスと、その配列すべて 読み出す際にクラスローダを指定する必要がある
Bundles Bundle × 読み出す際にクラスローダを指定する必要がある場合がある
アクティブオブジェクト IBinder、IBinder[] IBinder、IBinder[]
IInterface、IInterface[] ×
ParcelFileDescriptor × ファイルディスクリプタ
コンテナ Object、Object[] × Serializableも扱える
List List
Map Map
SparseArray × キーがint、値がオブジェクトのマップ型。読み出す際にクラスローダを指定する必要がある

 ParcelやAIDLでは、charやshortなど、すべてのプリミティブ型ないしプリミティブ配列型をサポートしていないことに注意してください。

 ParcelとAIDLでは、サポートする型に違いがあり、AIDLの方が若干少ないです。しかし、Parcelableを実装した独自コンテナではParcelを直接扱え、AIDLはParcelableが扱えるため、実質Parcelで扱える型のすべてがプロセス間通信で扱えるようになります。

AIDL(Androidのプロセス間通信のためのインターフェイス定義言語)

 「AIDL」とはAndroidのプロセス間通信のためのインターフェイス定義言語で、Javaのinterfaceを定義する書式に似ている独自言語で記述します。

 拡張子は「.aidl」で、慣習としてインターフェイスには「I」で始まる名前が付けられます。このファイルをEclipseのプロジェクト内に入れておけば、自動的に対応するJavaのソースコードが「gen」ディレクトリに生成されます。

 AIDLのコード例

 以下が今回のサンプルで使用しているAIDLです。

  ICalculatorService.aidl
package com.example.android.service; // 【1】     import com.example.android.service.ICalculatorCallback; // 【2】  import com.example.android.service.CalculatorExpression;     interface ICalculatorService { // 【3】      oneway void registerCallback(ICalculatorCallback callback); // 【4】      oneway void unregisterCallback(ICalculatorCallback callback);      int add(int lhs, int rhs);      void sum(in List values); // 【5】      void rotate(inout int[] array, int num);      int eval(in CalculatorExpression exp);  }     

  ICalculatorCallback.aidl
package com.example.android.service;     oneway interface ICalculatorCallback { // 【6】      void resultSum(int value);  }   

  CalculatorExpression.aidl
package com.example.android.service;     parcelable CalculatorExpression; // 【7】  

 青い部分がAIDL固有のキーワードです。

 【1】のように、Javaと同じパッケージ宣言が必要です。一方で、Javaとは異なり、【2】のように同一パッケージのAIDLも明示的にインポートしなければなりません。【3】のようにインターフェイス名を宣言します。この名前とファイル名は同一でなければなりません。

 【4】の「oneway」というキーワードは、そのメソッドの終了を待つ必要がないことを意味しています。【5】のように、オブジェクトの引数には「in」「out」「inout」のいずれかのキーワードを指定します。プリミティブ型には、この指定は必要ありません。「in」が入力にのみ使用する、「out」は出力にのみ使用する、「inout」は入出力に使用することを意味します。適切に指定することで、プロセス間でのデータ転送量を抑えられます。

 【6】のように、インターフェイスのすべてのメソッドにonewayを指定する代わりに、インターフェイスにonewayを指定することも可能です。

 【7】の「parcelable」キーワードで指定したクラスが「Parcelable」であることを指定します。このクラス名とファイル名は同一でなければなりません。

 AIDLの書き方

 AIDLを一般化すると、次のような形式になります。

package <PackageName>;  [import <FQCN>;]  [parcelable <FQCN>;]     [oneway] interface <InterfaceName> {      [oneway]<ReturnType> <MethodName> ([in|out|inout] <ArgType> <ArgName> ……);  }

 上記の「ReturnType」「ArgType」、戻り値と引数に使用可能な型には制限があります。後ほど一覧にして示します。

 AIDLは、簡潔な定義でプロセス間通信が行え、かつコールバックまで実現可能です。コールバックを介して、サービスとクライアントで相互通信が実現できるわけです。

 AIDLの利点

 もし、AIDL以外でプロセス間通信を行おうとすると、TCP/IPを使用する(非公開クラスの「WindowManagerService」「ViewServer」のように)、UNIXドメインを使用する(「netd」「ndc」のように)、共有メモリを使用する(「Ashmem」「Zygote」のように)、といったローテクな方法に頼らざるを得ません。

 AIDLは定義ファイルだけをクライアントに配布することで、Javaコンパイラで保証される安全で簡単なプロセス間通信が行えます。

IntentServiceクラスとは

内部にワーカースレッドを持ち、「startService()」で渡されたIntentを1つずつ処理することを容易にする「IntentService」というクラスもあります。

このクラスを使う際は、「onStartCommand()」はオーバーライドしてはならず、代わりに「onHandleIntent(Intent)」を実装します。

ServiceとIntentServiceの差異

ServiceとIntentServiceの差異
  Service IntentService
Intentの処理 並行処理 逐次処理
抽象メソッド onBind(Intent) onHandleIntent(Intent)

 IntentServiceは受け取ったInetntを内部にキューイングし、逐次処理を行うという特徴を持ちます。そして、キューから処理がなくなったら自動的にサービスが終了します。

 例えば、アプリ終了時のデータ保存処理をIntentServiceで行うことで、アプリは即座に終了し、バックグラウンドでデータ保存処理が行えます。AsyncTaskや別スレッドではこうした処理は行えません。

 IntentServiceを実装する際の注意点として、onCreate()、onDestory()、onStart()、onStartCommand()をオーバーライドしないことです。どうしてもそのタイミングで何か処理を行いたいのであれば、必ずスーパークラスの同一メソッドを呼び出すようにします。そうしなければ期待通りに動作しなくなります。

 Toastを表示するサービスとしてIntentServiceを使用することを試みましたが、Toast表示とIntentServiceは相性が悪く、Toastが表示される前にIntentServiceが終了してしまい、Toastは表示されませんでした。IntentServiceから他のサービスを呼び出す際には注意が必要です。

Androidサービス定義

AndroidManifest.xmlには、サービスを定義する必要があります。

<service
android:name=".CalculatorService"
android:process=":calculator" >
<intent-filter> <action android:name="com.example.android.service.ICalculatorService" /> </intent-filter>
</service>
 このサービスには「android:process」という属性が定義されています。これを定義することで、サービスを独立したプロセスで実行させることが可能になります。

 android:processに指定する値がコロンで始まる場合、独立したプライベートプロセスになり、アルファベットの小文字で始まる場合はグローバルプロセスになるという規則があります。

 サンプルアプリは、この属性が付いている状態であればbinder経由のプロセス間通信になり、付いていない状態であればローカルサービスの呼び出しになります。

 なお、AndroidManifest.xmlには「Exported service does not requre permission」という警告が出ています。

 これはインテントフィルタを定義したサービスは、ほかのアプリから呼び出すことが可能だけど、パーミッションが定義されていないため、どんなアプリからも呼び出せてしまうけど、それでよいのか、ということを警告しています。

 この警告をなくすにはandroid:permission属性を追加し、呼び出し元もそのパーミッションを宣言する必要があります。ローカルサービスではインテントフィルタが必要ありません。そのため、ほかから呼び出される心配もないので、やはりローカルサービスは簡単です。

2013年4月10日水曜日

Activity#runOnUiThread(Runnable)の実装を読む&注意点

実はrunOnUiThreadを最近まで知らなくていつもアホみたいにHandler#postしていてなんだか悔しかったのでActivity#runOnUiThread(Runnable)の実装を読んでみました。

Activity#runOnUiThread


Activity#runOnUiThreadの実装がこちら。

1
2
3
4
5
6
7
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

内部的にHandler#postしていますね。
ここでちょっと気になるのが
1
Thread.currentThread() != mUiThread
この比較です。runOnUiThreadを実行しているスレッドがUIスレッドかどうかチェックしています。
そしてUIスレッドである場合は
1
action.run();
指定したRunnableをrunOnUiThreadの中で即実行しています。つまりrunOnUiThreadはRunnableを実行する為にブロッキングされるという事です。

どんな時問題になるか


runOnUiThreadをHandler#postと同じモノだと考えて利用しているとハマる可能性があります。
まぁ多分大丈夫と思いますが以下のhello2()様な実行順に依存性があるような実装をしちゃうとヤバイですね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Handler mHandler = new Handler();
String mMessage = "hello!";
private void hello(){
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}
private void hello2(){
    runOnUiThread(new Runnable(){
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}

別スレッドからhello(),hello2()、UIスレッドからhello(),hello2()を実行するとToastに何が表示されるでしょうか?

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。


結論


色々省略しますが
・Handler#post, runOnUiThreadに渡すRunnableが触る変数にはHandler#post, runOnUiThread呼び出し後触らない。
・Handler#post, runOnUiThreadはなるべくメソッドの最後に呼ぶ
・Handler#post, runOnUiThreadに渡すRunnableの中では呼び出し元のメンバ変数などを触らない。
Handler#post, runOnUiThreadを呼び出すメソッド内でfinalな変数を宣言しておいてそれにアクセスしたりする様にする。

とかやってるといいんじゃないでしょかー
 

追記

実際hello(), hello2()のmMessage = "good bye!";の直前にThread.sleep(1000);を追加して実行してみるとどうなるか実験してみました。
結果は以下です

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。
以下の様な事になります。

呼び出しパターン 動き
別スレッドからHandler#postを呼ぶ(hello()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからHandler#postを呼ぶ(hello()) 必ず呼び出し後の処理が実行された後にRunnableが実行される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) 必ずRunnableが実行された後に呼び出し後の処理が実行される


結論は変わらず。
どちらにせよHandler#post, runOnUiThreadを呼び出した後にHandler#post, runOnUiThread内部で利用されうる値などの書き換えはやばいよ、という事ですね。