2012年3月9日金曜日

Cursorのclose()が原因で Attempted to access a cursor after it has been closed

Activity.managedQuery()でCursorを取得し、データ取得後にCursorをclose()していたら、close()のタイミングが悪くてRuntimeExceptionが発生。

java.lang.RuntimeException: Unable to resume activity {jp.hogehoge.test/jp.hogehoge.test.DownloadActivity}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.

ソースの抜粋
@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.download);

 // MediaStoreのDBを保存日の降順で検索
 Cursor cursor = managedQuery(URI, PROJECTIONS, WHERE,WHERE_PARAM, MediaStore.Images.Media.DATE_ADDED+ " desc");

 if (cursor.moveToFirst()) {
  do {
  } while (cursor.moveToNext());
 }
 cursor.close();

}// onCreate()

他のActivityを前面に出し、再度、DBからの一覧を表示した元の画面へ戻ってきたタイミングで発生する。

原因は、onCreate()の中でcursor.close()していたこと。

Activityのライフサイクルからいうと、再度前面へ戻ってきた時にonCreate()はコールされないので、DBからの一覧を取得できず「a cursor after it has been closed」となるわけだ。ごもっとも。

close()を明示的にコールしなければExceptionは発生しなくなった。


だがしかし、close()しないのはお作法として良いのか?と思い検索してみたら、Stack Overflowに参考になるポストがあった。
参考:managedQuery() vs context.getContentResolver.query() vs android.provider.something.query() - Stack Overflow

managedQuery() will use ContentResolver's query(). The difference is that with managedQuery() the activity will keep a reference to your Cursor and close it whenever needed (in onDestroy() for instance.) If you do query() yourself, you will have to manage the Cursor as a sensitive resource. If you forget, for instance, to close() it in onDestroy(), you will leak underlying resources (logcat will warn you about it.)

managedQuery() では、Activityがカーソルへの参照を保持していて必要なくなった時(onDestroy()が呼ばれた時)にクローズされる、とある。よって、明示的にclose()しなくてもよさそうだ。

ちなみに、ContentResolver.query()を使った場合は、自分で明示的にクローズしないとリークが起こるよとある。メモメモ。


managedQuery() はdeprecatedなので、本来なら使いたくないのだが、代替となるCursorLoaderはHoneycomb以上でしか使えない。

まだまだGingerbread搭載機種が発売されている現状では自分でコンパチビリティを書かないといけない。面倒なので使っていない\(^o^)/