Ошибка Android: java.lang.IllegalStateException: попытка запросить уже закрытый курсор
Среда (Linux/Eclipse Dev для планшета Xoom под управлением HoneyComb 3.0.1)
В моем приложении я использую камеру (startIntentForResult()), чтобы сделать снимок. После того, как снимок сделан, я получаю обратный вызов onActivityResult() и могу загрузить растровое изображение, используя Uri, переданный через намерение "сделать снимок". В этот момент моя деятельность возобновляется, и я получаю сообщение об ошибке при попытке перезагрузить изображения в галерею:
FATAL EXCEPTION: main
ERROR/AndroidRuntime(4148): java.lang.RuntimeException: Unable to resume activity {...}:
java.lang.IllegalStateException: trying to requery an already closed cursor
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2243)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1019)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:126)
at android.app.ActivityThread.main(ActivityThread.java:3997)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:491)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalStateException: trying to requery an already closed cursor
at android.app.Activity.performRestart(Activity.java:4337)
at android.app.Activity.performResume(Activity.java:4360)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2205)
... 10 more
Единственная логика курсора, которую я использую, заключается в том, что после получения изображения я конвертирую Uri в файл, используя следующую логику
String [] projection = {
MediaStore.Images.Media._ID,
MediaStore.Images.ImageColumns.ORIENTATION,
MediaStore.Images.Media.DATA
};
Cursor cursor = activity.managedQuery(
uri,
projection, // Which columns to return
null, // WHERE clause; which rows to return (all rows)
null, // WHERE clause selection arguments (none)
null); // Order-by clause (ascending by name)
int fileColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
if (cursor.moveToFirst()) {
return new File(cursor.getString(fileColumnIndex));
}
return null;
Есть идеи, что я делаю не так?
6 ответов
Похоже, что вызов управляемого Query() устарел в Honeycomb API.
Документ для managedQuery() гласит:
This method is deprecated.
Use CursorLoader instead.
Wrapper around query(android.net.Uri, String[], String, String[], String)
that the resulting Cursor to call startManagingCursor(Cursor) so that the
activity will manage its lifecycle for you. **If you are targeting HONEYCOMB
or later, consider instead using LoaderManager instead, available via
getLoaderManager()**.
Также я заметил, что я вызывал cursor.close() после запроса, который, я думаю, является нет-нет. Нашел эту действительно полезную ссылку. После некоторого чтения я пришел с этим изменением, которое, кажется, работает.
// causes problem with the cursor in Honeycomb
Cursor cursor = activity.managedQuery(
uri,
projection, // Which columns to return
null, // WHERE clause; which rows to return (all rows)
null, // WHERE clause selection arguments (none)
null); // Order-by clause (ascending by name)
// -------------------------------------------------------------------
// works in Honeycomb
String selection = null;
String[] selectionArgs = null;
String sortOrder = null;
CursorLoader cursorLoader = new CursorLoader(
activity,
uri,
projection,
selection,
selectionArgs,
sortOrder);
Cursor cursor = cursorLoader.loadInBackground();
Для справки, вот как я исправил это в своем коде (который работает на Android 1.6 и выше): проблема в моем случае заключалась в том, что я случайно закрывал управляемые курсоры, вызывая CursorAdapter.changeCursor(). Вызов Activity.stopManagingCursor() для курсора адаптера перед его изменением решил проблему:
// changeCursor() will close current one for us: we must stop managing it first.
Cursor currentCursor = ((SimpleCursorAdapter)getListAdapter()).getCursor(); // *** adding these lines
stopManagingCursor(currentCursor); // *** solved the problem
Cursor c = db.fetchItems(selectedDate);
startManagingCursor(c);
((SimpleCursorAdapter)getListAdapter()).changeCursor(c);
ИСПРАВИТЬ: использовать context.getContentResolver().query
вместо activity.managedQuery
,
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, PROJECTION, null, null, null);
} catch(Exception e) {
e.printStackTrace();
}
return cursor;
Я создал этот вопрос здесь, так как не мог прокомментировать последний ответ (комментарии по каким-то причинам отключены). Я думал, что открытие новой темы об этом только усложнит ситуацию.
Я получаю сбои приложения, когда я перехожу из Действия A в Деятельность B и затем возвращаюсь к Деятельности A. Это не происходит все время - только иногда, и мне трудно найти именно то, где это происходит. Все происходит на одном устройстве (Nexus S), но я не верю, что это проблема устройства.
У меня есть несколько вопросов относительно ответа @Martin Stine.
- В документации говорится о
changeCursor(c);
: "Изменить базовый курсор на новый курсор. Если он существует, он будет закрыт". Так почему я долженstopManagingCursor(currentCursor);
- Разве это не избыточно? - Когда я использую код, предложенный @Martin Stine, я получаю исключение нулевого указателя. Причина в том, что при первом "запуске" приложения
((SimpleCursorAdapter)getListAdapter())
будет иметь значение NULL, потому что курсор еще не был создан. Конечно, я мог проверить, не получаю ли я нулевое значение, и только затем попытаться остановить управление курсором, но в конце концов я решил разместить свой `stopManagingCursor(currentCursor); в методе onPause() этого действия. Я думал, что таким образом у меня наверняка будет курсор, чтобы перестать управлять, и я должен сделать это непосредственно перед тем, как я оставлю Activity в другой. Проблема - я использую несколько курсоров (один для заполнения текста поля EditText, а другой для представления списка) в своей деятельности, я думаю, не все они связаны с курсором ListAdapter -- Как я знаю, какой из них перестать управлять? Если у меня есть 3 разных вида списка?
- Должен ли я закрыть их все во время
onPause()
? - Как получить список всех моих открытых курсоров?
Так много вопросов... Надеюсь, кто-нибудь может помочь.
Когда я доберусь до onPause()
У меня есть курсор, чтобы остановить управление, но я еще не определился, решает ли это проблему, так как эта ошибка появляется время от времени.
Большое спасибо!
ПОСЛЕ НЕКОТОРОГО ИССЛЕДОВАНИЯ:
Я нашел кое-что интересное, что могло бы дать ответ на "загадочную" сторону этого вопроса:
Упражнение A использует два курсора: один для заполнения поля EditText. Другой заключается в заполнении ListView.
При переходе от действия A к действию B и возвращении поле + ListView в действии A должно быть заполнено снова. Кажется, что поле EditText никогда не будет иметь проблемы с этим. Я не мог найти способ получить текущий курсор поля EditText (как в Cursor currentCursor = ((SimpleCursorAdapter)getListAdapter()).getCursor();
) и причина говорит мне, что поле EditText не сохранит его. С другой стороны, ListView будет "запоминать" свой курсор с прошлого раза (до Действия A -> Деятельность B). Кроме того, и это странно, Cursor currentCursor = ((SimpleCursorAdapter)getListAdapter()).getCursor();
будет иметь другой идентификатор после Действия B -> Деятельность A, и все это БЕЗ моего когда-либо звонящего
Cursor currentCursor = ((SimpleCursorAdapter)getListAdapter()).getCursor();
stopManagingCursor(currentCursor);
Я предполагаю, что в некоторых случаях, когда системе необходимо освободить ресурсы, курсор будет уничтожен, а когда операция B -> операция A, система все равно будет пытаться использовать этот старый мертвый курсор, что приведет к исключению. А в других случаях система создаст новый курсор, который еще жив, и, таким образом, исключение не произойдет. Это может объяснить, почему это появляется только иногда. Я предполагаю, что это трудно отладить из-за разницы в скорости приложения при запуске или отладке приложения. При отладке это занимает больше времени и, следовательно, может дать системе время для создания нового курсора или наоборот.
В моем понимании это делает использование
Cursor currentCursor = ((SimpleCursorAdapter)currentListAdapter).getCursor();
stopManagingCursor(currentCursor);
В соответствии с рекомендациями @Martin Stine в НЕКОТОРЫХ случаях и избыточно в ДРУГИХ: если я возвращаюсь к методу, и система пытается использовать мертвый курсор, необходимо создать новый курсор и заменить его в ListAdapter, иначе я разозлюсь пользователи приложения с упавшим приложением. В другом случае, когда система найдет себе новый курсор - строки выше избыточны, поскольку они лишают законной силы хороший курсор и создают новый.
Я предполагаю, что для предотвращения этой избыточности мне нужно что-то вроде этого:
ListAdapter currentListAdapter = getListAdapter();
Cursor currentCursor = null;
Cursor c = null;
//prevent Exception in case the ListAdapter doesn't exist yet
if(currentListAdapter != null)
{
currentCursor = ((SimpleCursorAdapter)currentListAdapter).getCursor();
//make sure cursor is really dead to prevent redundancy
if(currentCursor != null)
{
stopManagingCursor(currentCursor);
c = db.fetchItems(selectedDate);
((SimpleCursorAdapter)getListAdapter()).changeCursor(c);
}
else
{
c = db.fetchItems(selectedDate);
}
}
else
{
c = db.fetchItems(selectedDate);
}
startManagingCursor(c);
Я хотел бы услышать, что вы думаете об этом!
Просто добавьте следующий код в конец блока курсора.
try {
Cursor c = db.displayName(number);
startManagingCursor(c);
if (!c.moveToFirst()) {
if (logname == null)
logname = "Unknown";
System.out.println("Null " + logname);
} else {
logname = c.getString(c
.getColumnIndex(DataBaseHandler.KEY_NAME));
logdp = c.getBlob(c
.getColumnIndex(DataBaseHandler.KEY_IMAGE));
// tvphoneno_oncall.setText(logname);
System.out.println("Move name " + logname);
System.out.println("Move number " + number);
System.out.println("Move dp " + logdp);
}
stopManagingCursor(c);
}
Эта проблема мучила меня долгое время, и я наконец-то нашел простое решение, которое работает как шарм на всех версиях Android. Во-первых, не используйте startManagingCursor(), так как он явно содержит ошибки и не рекомендуется в любом случае. Во-вторых, закройте курсор как можно скорее после того, как с ним покончено. Я использую попытку и наконец, чтобы убедиться, что курсор закрывается при любых обстоятельствах. Если ваш метод должен вернуть Cursor, то вызывающая подпрограмма отвечает за его скорейшее закрытие.
Раньше я оставлял курсоры открытыми на весь срок действия Activity, но с тех пор я отказалась от этого для этого транзакционного подхода. Теперь мое приложение очень стабильно и не страдает от "ошибки Android: java.lang.IllegalStateException: попытка запросить уже закрытый курсор" при переключении операций, даже если они обращаются к одной и той же базе данных.
static public Boolean musicReferencedByOtherFlash(NotesDB db, long rowIdImage)
{
Cursor dt = null;
try
{
dt = db.getNotesWithMusic(rowIdImage);
if ( (dt != null)
&& (dt.getCount() > 1))
return true;
}
finally
{
if (dt != null)
dt.close();
}
return false;
}