Androidアプリがどうデータベースを取り扱うか

端末にデータを保存するアプリを4日くらい書いてたので、その時に調べたことのメモ。
あ、書いてたアプリはこれです。 https://market.android.com/details?id=jp.juggler.SubwayTweeter

基本的な構造

    SQLiteOpenHelper
    ↓生成
    SQLiteDatabase
    ↓生成
    SQLiteCursor
        has CursorWindow(native code)
        has SQLiteQuery(native code)

参照カウントとデータベースオブジェクトの再利用

SQLiteDatabase は SQLiteClosable を継承していて、参照カウントが全て解放された時点でSQLiteDatabaseの後処理が行われる。

SQLiteDatabase と SQLiteCursor は finalize() も実装しているが、特にcursorに関しては明示的にclose()を呼んだ方が良い。

SQLiteOpenHelper は 前回に生成したSQLiteDatabaseがまだ使用可能であればそれを再利用するようになっている。ただし getReadableDatabase と getWritableDatabase を交互に呼ぶなどした場合は再利用は行われない。

managedQueryとrequery の廃止

managedQueryはActivityがUIスレッドに置いてクエリのdeactivate() と close() を管理する機構だったが、Honeycomb(3.0,API level 11)で廃止になった。

ドキュメントには非推奨とあるが、実際には廃止である。2.2までのアプリをそのまま3.0の端末で動かすと例外がでる。

3.0以降のAPIでは CursorLoader を使う。

互換性とアプリサイズを意識する場合、上記のいずれも使えない訳でActivity.javaに書かれた以下の動作を自前で管理することになる。

  • ActivityのonDestroyでCursor.close()
  • ActivityのonRestartでCursor.requery()
  • ActivityのonStopでCursor.deactivate()
  • ActivityのonPause,onStop などでCursor.commitUpdates() これは deactivate より前に行う。

ContentProviderと通知

ContentProviderを継承したクラスを実装する際、以下のような記述を行う。

  • cursor.setNotificationUri(getContext().getContentResolver(), uri);
  • getContext().getContentResolver().notifyChange(uri, null);

前者のメソッドはAbstractCursorで実装されていて、 ContentResolver.registerContentObserver() を使って通知を受け取れるようにする。
後者のメソッドはContentResolverで実装されていて、ContentService.notifyChange()を呼び出す。

実際に通知が送られる時は以下の様な流れになる。

  • (ContentProviderを継承したクラス)
  • ContentResolver.notifyChange は IContentService.notifyChange を呼び出す。ここでプロセス間通信が行われる。
  • (ここからサービス)
  • ContentService.notifyChange は IContentObserver.onChange を呼び出す。ここでプロセス間通信が行われる。
  • (通知を受け取ったカーソル)
  • AbstractCursor#mSelfObserver の onChange が呼ばれる。
  • AbstractCursor.onChange が呼ばれる。
  • AbstractCursor#mContentObservable の dispatchChange が呼ばれる。
  • (ここからAdapter)
  • CursorAdapter#mChangeObserver の onChange が呼ばれる。
  • CursorAdapter.onContentChanged が呼ばれる。
  • カーソルのrequeryが行われる。
  • (ここからSQLiteCursor)
  • SQLiteCursor.requery は 再クエリを行う。
  • AbstractCursor.requery は AbstractCursor#mDataSetObservable.notifyChanged() を呼び出す。
  • (ここからAdapter)
  • CursorAdapter#mDataSetObserver の onChanged() が呼ばれる。
  • BaseAdapter.notifyDataSetChanged() は BaseAdapter#mDataSetObservable の notifyChanged() を呼び出す。
  • (ここからAdapterView)
  • AbsListView#mDataSetObserver.onChanged が呼ばれる。 ちなみにこのメンバを定義してるのはAbsListViewだが、クラス定義自体は AdapterView の中にある。
  • データ件数の取得と再レイアウトが行われる。

ContentProviderを継承したクラスを実装する際、Webにあるいくつかのサンプルコードではdeleteを実装する際に通知を忘れているといったバグがあえて残されているようだ。もちろん通知は必要になる。

ContentProviderを使わずに通知を行ないたい場合、CursorAdapter.onContentChanged() を派生クラスから呼ぶのが手っ取り早いようだ。しかし通知漏れやプロセス間の処理まで考えると、ContentProviderを書いてしまう方が利便性が高い。

requery

実は SQLiteCursor.requery はその場では再クエリを行わない。mCountをNO_COUNTに初期化して呼び出されたSQLiteQuery.requeryがバインドパラメータを更新するだけだ。
その後getCountが初回に呼び出された時点で mQuery.fillWindow が呼ばれる。
SQLiteQuery.native_fill_window が実際の仕事を行い、戻り値として件数を返す。
また、mQuery.fillWindow は実際の仕事が終わる前にリターンする場合があり、その場合getCount()は「読めた件数」を返す。
この時同時に別スレッドが開始されて、クエリ完了時にリスナへの通知が行われる。

deactivate

SQLiteCursor の場合、deactivate はその時保持しているwindowを閉じるようになっている。
CursorWindow は native なコードとデータを含むオブジェクトだ。

検索

sqlite全文検索は日本語に滅法弱いので何かしら工夫するか、妥協することになる。
妥協した場合はクイック検索ボックスの検索候補などで、遅い検索速度が問題になるだろう。

まとめ

CursorAdapterとAdapterViewでUIとデータベースの連携をスマートに書けるのはAndroidのいいところでした。しかし「UIスレッドでDBアクセスする」というのはUIイベントのブロッキング時間という点では深刻だったようで、3.0以降では設計が改善されているようです。しかし今は過渡期なので、2.3以前と3.0以後の両方をサポートするアプリを書くには何かしら工夫が必要です。Android compatibility library を組み込むのも手の一つではありますが、サイズが100KBあるのと、UIは別に古くてもいいという場合は導入が躊躇われます。

「データをDBに保存」というと SQLiteDatabase をベタに使ってしまいがちですが、200行くらいのほぼ定石な ContentProvider を書くことでデータ変更の通知がフォーマルな流れになり、実装で意識しないといけないことが格段に減ります。単一の ContentProvider で完結している分には、ベタに使う場合と比べて機能的なデメリットは何もありません。

今回調べて分かったのは「deactivateの呼び出しは別に必須ではない」です。単に消費メモリを低減させるだけで、それ以外の効果はありません。