Logcat Viewにログが表示されない時に試すこと

以下Windowsプラットホーム固有の問題かもしれない話。(又は知らないのは私だけだったのかもしれない話)

Eclipse + ADTでの開発でAndroidエミュレータを再起動すると、その後Logcat Viewのログが表示されなくなってしまうことがある。そんな時はEclipseを再起動する、ADBプロセスを再起動することで復旧することが多いが、その前に以下の操作をしてみてほしい。※

1. DDMSパースペクティブを開く
2. Devicesタブを選択する
3. 対象のエミュレータ又は機器を示すアイコン(大抵は頂上だ)をクリック

エミュレータ、ADB共に問題が無ければ、この操作でLogcat Viewが再度流れ始めるはずだ。(恐らくなんらかの原因でデバイスの選択が外れているだけなのだろう) もし、これでも駄目ならば諦めてEclipseなりADBを再起動しよう。

※Logcat Viewは未だに日本語を正しく表示できないこともあり、そんな時はコマンドラインからlogcatを起動すれば事足りてしまうのだが。

Bundleをプロセス間通信で使う場合の注意

以前、AIDLを使ってサービスとアクティビティで通信を行う際に引数にBundleを渡すと原因不明の上記例外が発生したことがあった。
AIDLによるサービスとの通信でBundleにParcelableをセットするとBadParcelableException
AIDLによる通信でBundleにParcelableをセットするとBadParcelableException(その2)
不可解な結末

この時はエラーが消失してしまったため追求もそこで止めてしまっていたのだが、これはBundleクラスが内部に持つクラスローダー(ClassLoader)の問題が原因であることが判明した。

途中を端折っていきなり核心に入るが、Bundleクラス内部のデータはParcelクラスであり、こいつはの内部データをアンマーシャル(アンパーセル)する際に必要ならばクラスローダを使用する。(内部データの型によっては遅延評価を行い、デマンドでオブジェクトを再構成するためだ)

    • Bundle.java#unparcel
synchronized void unparcel() {
    if (mParcelledData == null) {
        return;
    }

    int N = mParcelledData.readInt();
    if (N < 0) {
        return;
    }
    if (mMap == null) {
        mMap = new HashMap();
    }
    mParcelledData.readMapInternal(mMap, N, mClassLoader);
    mParcelledData.recycle();
    mParcelledData = null;
}

このフィールドmParcelledDataはPacel型でありreadMapInternalはParcel#readParcelableを呼び出す。

    • Parcel.java#readParcelable
    try {
        Class c = loader == null ?
            Class.forName(name) : Class.forName(name, true, loader);
        Field f = c.getField("CREATOR");
        creator = (Parcelable.Creator)f.get(null);
    }
    :略
    :
    catch (ClassNotFoundException e) {
        Log.e("Parcel", "Class not found when unmarshalling: "
                            + name + ", e: " + e);
        throw new BadParcelableException(
                "ClassNotFoundException when unmarshalling: " + name);
    }

ここでクラスをロードできない場合に例外がスローされるのだ。

Bundleクラスはデフォルトでは自らの型から取得した(Class#getClassLoader())クラスローダを使用するが、こいつはjava.lang.BootClassLoaderのインスタンスでありブートストラップローダ、つまりクラスローダのルートである。

public Bundle() {
    mMap = new HashMap();
    mClassLoader = getClass().getClassLoader();
}

通常であれば問題になることはないはずだが、今回のようにプロセスが互いに分離されたアプリケーションとサービスのプロセス下のDalvikが別個にロードしたクラス(今回は別プロセスのサービスがロードしたクラス)であり、同じ型のはずだが違う型と見なされてロードに失敗する訳だ。

android.os.BadParcelableException: ClassNotFoundException when unmarshalling

なんだかんだとJavaを使い始めて10年以上が経つのだが、こうやって未だにクラスローダに悩まされる。


解決策として一番簡単なのは、AIDL等外部と通信を行うパラメタにBundleを含む場合はそのクラスローダをコンテキストのもので置換えることである。

Bundle bundle = new Bundle(this.getClassLoader());

本当はBundleを、例えばRemotableBundleとか拡張すれば良いのだが、ここでもBundleクラスはfinalなのだった。

分離されたプロセスをデバッグする

前エントリで言及したようにAndroidアプリケーションはその内部に「別プロセスで実行する = android:process」を明示した場合はプロセスを分離することができるが、Eclipseで分離したプロセスをデバッグするにはどうすればよいのだろう。

通常Eclipse ADTのデバッグセッションはAndroidマニフェストに従って生成されたアプリケーションのプロセスにアタッチした状態で開始されるが、この状態でデバッグできるコードは元々のアプリケーション-プロセスに属したコードだけであり、分離したプロセスで実行されている部分はデバッグセッションでは見えないのでデバッグできない。(当然である)

では一度分離されてしまったプロセスはデバッグできないのだろうか? 否。

デバッグセッションを開始後、DDMSパースペクティブのDeviceビューを見ると実行されているプロセスの一覧が表示されておりデバッグ中のプロセスに緑色の「バグ・アイコン」が表示されているのが分るだろう。

バグ・アイコンが表示されている(デバッグ対象のプロセス)の下にあるプロセスがサービスプロセスだが、この状態で行を選択してツールバー上のバグアイコンが描画されているボタンを押下することで、同様にサービスプロセスのデバッグセッションを開始することができる。

ここでデバッグパースペクティブに戻ると、もう一つのデバッグセッションが開始していることが分るだろう。

なお、プロセス分離であってもプレフィクス":"つまりプライベートプロセスとして分離した場合は上記の方法でデバッグが開始せず、以下のようなエラーダイアログが表示されるだろう。

この場合、DDMSパースペクティブのDeviceビュー上に表示されている、デバッグ対象プロセスのポート番号を覚えておき

(8632)、Eclipseのメニュー「デバッグ構成」から「リモートJavaアプリケーション」に構成を新たに作成して、このポートを指定することで、同様にデバッグセッションを開始できる。

なお、一度デバッグ無しでアプリケーションを起動しておいて、後からデバッガをアタッチする方法は起動に時間がかからないため、初期化部分(Activityがshowされるまで)をデバッグする必要が無い場合は時間の節約になるので分離されたプロセスのデバッグ以外でもお勧めの方法だ。

サービスをアプリケーションプロセスから分離する

Androidのサービス(Service)はマニフェスト要素におけるandroid:process属性の記述により、サービスが実行されるプロセスをアプリケーションから分離することができる。

  • android:process の記述無し (デフォルト)

サービスはアプリケーションに割り当てられたプロセス上で、他のActivityと共に実行される。
  • android:process = ":servicename" とコロン":"で始まる名前を指定した場合

サービスはアプリケーションのプロセスからプライベートに生成された、新たなプロセス上で実行される
  • android:process = "servicename" と英子文字で始まる名前を指定した場合※

サービスはアプリケーションのプロセスからグローバルに生成された、新たなプロセス上で実行される

実際に記述して実行してみれば分るが、別プロセスで実行するようにした場合はアプリケーションのプロセスを終了してもサービスのプロセスはシステムに常駐し続けるのが分るはずだ。

プロセスを分離した場合の「プライベート」か「グローバル」かの違いが今ひとつよく分っていないのだが、リファレンスによると

If the name assigned to this attribute begins with a colon (':'), a new process, private to the application, is created when it's needed and the service runs in that process. If the process name begins with a lowercase character, the service will run in a global process of that name, provided that it has permission to do so. This allows components in different applications to share a process, reducing resource usage. 
service > android:process - Android Developers

とあるので、資源の共有が可能なのがグローバル、それが不可能なのがプライベートなのだろう。(この辺詳しい解説が見あたらない。組込みLinux関連だろうか)

以前に書いたTagsoupを利用したWebスクレイピングを実行するサービスなど、他の複数のアプリケーションと共有したいサービスは、当然グローバルプロセスとして起動すべきだろう。

プロセスを分離することによりシステムリソースの節約になり、サービスを起動する際のCPUサイクルを軽減することが出来るため、良いことずくめなのだが、分離することにより新たな問題が発生することもあるので注意が必要だ。その辺はまた別な機会に。


※アプリケーション全体にパッケージの指定がある場合、小文字で開始するといっても最初の一文字は"."ピリオドである必要がある。

もりぞー? すーも? はろ? 否どろいどです

こんなの注文してしまった。
http://www.squishable.com/mm5/graphics/00000001/android.jpg
Squishable Android

ちょっと後悔して、、ないぞ。

というかこれ位の大きさの縫いぐるみだと、大抵は送料の方が高くなるんだよな。

追記: 10/15 Order Shippedのメールが届いていた。