2012年1月7日土曜日

android 2重起動の禁止

いやー、android 開発、難しいですね~。こんな事は、もう語り尽くされているだろうと思ったかもしれませんが、もうちょい突っ込んだ内容になります。 android 関連の情報では、圧倒的にお世話になっている yanzm さんのところに Android launchMode の違いと、あるんで、基本は、これでオッケーなんです。  ところが、アプリケーションを終了して、直後に、アプリケーションを起動すると、一時的にアプリケーションというか、Activity が2重起動している状態になります。この時に初期化処理と終了処理が前後してしまう事があるんですわ。いやいや、ちゃんと行儀よくActivityを書いていれば、何の問題もないんです。どういう事をやっているかというと、
  
  ...
  class FooActivity extends Activity {
    public static Hoge hoge_ = null;
  ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      hoge_ = new Hoge();
    }
    @Override
    protected void onDestroy() {
      hoge_.close();
      super.onDestroy();
    }
  ...
  }
みたいな事をやってるんです。フッ、static な global 変数ですか?って笑われるかもしれませんが、正直、あちこちの Activity が関連を持って利用しなければならないとなると、こうするより他無いんです。こんなんで共有できてしまうのは、将来的に地雷を踏む可能性もあります。だって、アプリケーション全体でデータを共有するためには、どうしたらいいですか?に対する回答は、「ApplicationBean を使いましょう」だからです。データは、Parcelable で受け渡しすべきです。さて、この android.app.Application ですが、onDestroy に相当するメソッドがありません。正直言って使えません。また、画面の回転でレイアウトが変わる度に onDestroy と onCreate が呼ばれるので、うざくてしょうがありません。だから、私はダミーの BaseActivity を作成して、そこで共通リソースの初期化と破棄を行なってます。そうすれば、MainActivity の onDestroy と onCreate が何回呼ばれようが気にしなくて済みます。  話を巻き戻して、SingleTask なのに一時的に2重起動してもプロセスが別なら問題ないんじゃないですか?と思うやないですか?ところが、違うんです。この2つのアプリケーションから hoge_ が共有できてしまっており、先に終了したアプリの onDestroy よりも先に、後から起動したアプリの onCreate がコールされる状況があるんです。いや、もうビックリですね?しょうがないので、
  ...
  class FooActivity extends Activity {
    public static Hoge hoge_ = null;
    private static int process_count_ = 0;
    ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      if( 0 == process_count_ ) {
        ++process_count_;
        hoge_ = new Hoge();
      } else {
        ++process_count_;
      }
    }
    @Override
    protected void onDestroy() {
      --process_count_;
      if( 0 == process_count_ ) {
        hoge_.close();
      }
      super.onDestroy();
    }
  ...
  }
という感じで、プロセス?カウントをとって、対処してます。ちなみにタイミングがシビアじゃ無いので、カウントの保護は一切行なっていません。 追記:static 変数の記述場所を間違えてたので修正しました。やっぱりカウンタは同期保護した方が良いかもしれません。synchronized(this)ではダメっぽい気がしますが、突っ込んでません。やるとすれば private static Object sync_ = new Object(); しておいて、synchronized(sync_) でしょうか?
訂正: final 修飾子が余計だったのを取った。
2012/01/15 追記: どうも、Activity 再開時の挙動など、singleTask 等で細かく異なるようである。参考になるサイトを見つけた。こちらも目を通した方が良いです。一旦 HOME ボタンを押した後に再開すると、singleTask では BaseActivity の onDestroy がコールされるので思わしくない。そこで、このような事をやる場合は、singleInstance を選択する事にしました。 singleInstance は、Activity のフォーカスが外れる時点で、onDestroy がコールされ破棄されるので、使えない。別に single である必要は無い気がしてきた…。もう少し突っ込んで、整理した後に書く予定。追試する度に、挙動が違って困る…。
 2012/01/17 追記: BaseActivity に Manifest で android:screenOrientation="landscape" と指定するようにした。 BaseActivity -> MainActivity と表示している時にレイアウトの縦横が変更された時、MainActivity だけが変更の影響を受けるわけではなく、BaseActivity にまで変更の影響を受けている事がわかった。そのせいで、BaseActivity の onDestroy と onCreate がコールされる事になる。この辺の挙動は、かなりトリッキーだ。BaseActivity の onCreate 時に MainActivity を起動しているのだが、MainActivity が縦横変更時にどんどん増殖するという恐ろしい事態を引き起こしていた。現在は、singleInstance で実装している。
2012/08/29 追記:Android SDK における sqlite3 のコードが、データベースを開いたプロセスと閉じたプロセスが同一でないと、エラーで動作しないようなチェックが入っております。なので、最終的には、2重起動を自前で禁止するために、process_count_ >= 2 ならば、アプリケーションを終了するようにしました。  一方のプロセスがデータベースを閉じるまで待ってからオープンしようと synchronized メソッドでカウントを検知したら、そこから抜けてThread.sleep(200); とか入れてみたんですが、プロセス|スレッドのコンテキストが全く切り替わりません。このため、2重起動を自前で防ぐコードを選択せざるを得ませんでした。こういうのは、singleInstance を指定しているんだから、アプリがやる仕事じゃ無いと思います。

0 件のコメント: