Запрос SQLite выполняется в потоке пользовательского интерфейса с помощью ExpandableListView / SimpleCursorTreeAdapter


Я в процессе разработки приложения для Android для отображения ряда RSS-каналов (да, я знаю, что таких приложений уже много). Данные, которые будут отображаться, поддерживаются поставщиком контента, и я хочу быть обратно совместимым с API уровня 4.

Я использую ExpandableListView для отображения содержимого трех различных RSS-каналов. Адаптер ExpandableListView реализован как подкласс SimpleCursorTreeAdapter:

private class RssFeedLatestListAdapter extends SimpleCursorTreeAdapter {

    public static final String FEED_NAME_COLUMN = "feedName";

    public RssFeedLatestListAdapter(Context ctx, Cursor groupCursor, int groupLayout,
            String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
            int[] childTo) {
        super(ctx, groupCursor, groupLayout, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
    }

    @Override
    protected Cursor getChildrenCursor(final Cursor groupCursor) {
        final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
        return managedQuery(LATEST_URI, null,
                FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
                new String[] { mCategory.getId(), feedName }, null);
    }

    @Override
    protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
        super.bindGroupView(view, context, cursor, isExpanded);

        // Bind group view (impl details not important) ...
    }

    @Override
    protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
        super.bindChildView(view, context, cursor, isLastChild);

        // Bind child view (impl details not important) ...
    }
}

При такой настройке все содержимое загружается, как и ожидалось. Однако, пользовательский интерфейс зависает/заикается случайным образом во время загрузки содержимого списка. Обычно не хватает, чтобы получить АНР, но все равно заметно и очень раздражает!

Чтобы отладить эту проблему, я включил android.ос.StrictMode (отличная новая функция / инструмент кстати!), и запустил эмулятор (на Android 2.3). Когда содержимое списка загружается, я получаю следующий вывод StrictMode:

D/StrictMode(  395): StrictMode policy violation; ~duration=1522 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2
D/StrictMode(  395):  at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745)
D/StrictMode(  395):  at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1345)
D/StrictMode(  395):  at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:330)
D/StrictMode(  395):  at com.mycompany.myapp.provider.RSSFeedProvider.query(RSSFeedProvider.java:128)
D/StrictMode(  395):  at android.content.ContentProvider$Transport.query(ContentProvider.java:187)
D/StrictMode(  395):  at android.content.ContentResolver.query(ContentResolver.java:262)
D/StrictMode(  395):  at android.app.Activity.managedQuery(Activity.java:1550)
D/StrictMode(  395):  at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.getChildrenCursor(MultipleFeedsActivity.java:388)
D/StrictMode(  395):  at android.widget.CursorTreeAdapter.getChildrenCursorHelper(CursorTreeAdapter.java:106)
D/StrictMode(  395):  at android.widget.CursorTreeAdapter.getChildrenCount(CursorTreeAdapter.java:178)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.refreshExpGroupMetadataList(ExpandableListConnector.java:561)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:682)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:636)
D/StrictMode(  395):  at android.widget.ExpandableListView.expandGroup(ExpandableListView.java:608)
D/StrictMode(  395):  at com.mycompany.myapp.activity.MultipleFeedsActivity.onReceiveResult(MultipleFeedsActivity.java:335)
D/StrictMode(  395):  at com.mycompany.myapp.service.FeedResultReceiver.onReceiveResult(FeedResultReceiver.java:40)
D/StrictMode(  395):  at android.os.ResultReceiver$MyRunnable.run(ResultReceiver.java:43)
D/StrictMode(  395):  at android.os.Handler.handleCallback(Handler.java:587)
D/StrictMode(  395):  at android.os.Handler.dispatchMessage(Handler.java:92)
D/StrictMode(  395):  at android.os.Looper.loop(Looper.java:123)
D/StrictMode(  395):  at android.app.ActivityThread.main(ActivityThread.java:3647)
D/StrictMode(  395):  at java.lang.reflect.Method.invokeNative(Native Method)
D/StrictMode(  395):  at java.lang.reflect.Method.invoke(Method.java:507)
D/StrictMode(  395):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
D/StrictMode(  395):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
D/StrictMode(  395):  at dalvik.system.NativeStart.main(Native Method)

Это кажется разумным! В SimpleCursorTreeAdapter.getChildrenCursor() поставщик содержимого запрашивается в потоке пользовательского интерфейса, который из конечно, это плохая идея, которая наверняка повесит пользовательский интерфейс!

Я посмотрел на JavaDoc (уровень API 4) CursorTreeAdapter (суперкласс SimpleCursorTreeAdapter) и для getChildrenCursor() он говорит:

"Если вы хотите асинхронно запросить поставщика, чтобы предотвратить блокировку пользовательского интерфейса, можно вернуть null и позже вызвать setChildrenCursor(int, Cursor).".

Отлично! Давайте сделаем это! Я изменяю свою реализацию getChildrenCursor(), чтобы начать новую задачу, ответственную за выполнение запрос и установка дочернего курсора на адаптере. После запуска задачи я просто возвращаю null в getChildrenCursor():

@Override
protected Cursor getChildrenCursor(final Cursor groupCursor) {
    final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
    new RefreshChildrenCursorTask(groupCursor.getPosition()).execute(feedName);
    return null;
}

, где RefreshChildrenCursorTask реализуется как:

private class RefreshChildrenCursorTask extends AsyncTask<String, Void, Cursor> {

    private int mGroupPosition;

    public RefreshChildrenCursorTask(int groupPosition) {
        this.mGroupPosition = groupPosition;
    }

    @Override
    protected Cursor doInBackground(String... params) {
        String feedName = params[0];
        return managedQuery(LATEST_URI, null,
                FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
                new String[] { mCategory.getId(), feedName }, null);
        }

        @Override
        protected void onPostExecute(Cursor childrenCursor) {
            mLatestListAdapter.setChildrenCursor(mGroupPosition, childrenCursor);
        }
    }
}
Я переустановил приложение на эмулятор 2.3 и запустил его. Это сработало как заклинание, пока-пока не реагирующий пользовательский интерфейс! Но радость длилась недолго ... Затем нужно было запустить тот же код на целевом устройстве (в данном случае Samsung Galaxy S под управлением Android 2.2). Затем я получил следующее Exception во время загрузки списка содержимого ленты:
E/AndroidRuntime(23191): FATAL EXCEPTION: main
E/AndroidRuntime(23191): java.lang.NullPointerException
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.initFromColumns(SimpleCursorTreeAdapter.java:194)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.initChildrenFromColumns(SimpleCursorTreeAdapter.java:205)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.init(SimpleCursorTreeAdapter.java:186)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.(SimpleCursorTreeAdapter.java:136)
E/AndroidRuntime(23191):  at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.(MultipleFeedsActivity.java:378)
E/AndroidRuntime(23191):  at com.mycompany.myapp.activity.MultipleFeedsActivity$2.run(MultipleFeedsActivity.java:150)
E/AndroidRuntime(23191):  at android.os.Handler.handleCallback(Handler.java:587)
E/AndroidRuntime(23191):  at android.os.Handler.dispatchMessage(Handler.java:92)
E/AndroidRuntime(23191):  at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(23191):  at android.app.ActivityThread.main(ActivityThread.java:4627)
E/AndroidRuntime(23191):  at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(23191):  at java.lang.reflect.Method.invoke(Method.java:521)
E/AndroidRuntime(23191):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:871)
E/AndroidRuntime(23191):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
E/AndroidRuntime(23191):  at dalvik.system.NativeStart.main(Native Method)
Быстрый взгляд на исходный код SimpleCursorTreeAdapter говорит мне, что он не может справиться с null, возвращаемым асинхронно из getChildrenCursor(). Они, кажется, решили эту проблему в Android 2.3, но как вы должны обойти ее в более ранних версиях Android? Действительно ли мне нужно написать свою собственную реализацию CursorTreeAdapter, чтобы иметь возможность асинхронно обновлять дочерний курсор?

У кого-нибудь была такая же проблема и возможно, даже нашли (осуществимый) обходной путь? Если нет, то может ли кто-нибудь подсказать мне, как действовать дальше?? я был бы очень признателен за любую помощь , которую я могу получить!

Заранее спасибо!

С уважением, Иаков

2 3

2 ответа:

Если они исправили это в 2.3 SimpleCursorTreeAdapter, почему бы вам просто не загрузить файл java и включить его в свой проект и использовать эту версию, а не ту, что на телефоне?

Я не смотрел на код, но пока они не сделали никаких новых вызовов API 2.3, он должен работать.

Спасибо за ваши отзывы, mp2526! Прошу прощения за мой запоздалый ответ!

Это отличное предложение! Тем не менее, я действительно думал, что это должно быть возможно сделать (получить курсоры детей асинхронно) в API 8, и что я ничего не понял ... По-видимому, это невозможно (по крайней мере, с использованием SimpleCursorTreeAdapter)!

В любом случае, я сделал различие между API 8 и API 9 Для SimpleCursorTreeAdapter, и то, что они сделали в Android 2.3, состоит в том, что идентификаторы столбцов childFrom и groupFrom являются теперь инициализируется лениво, то есть уже не в конструкторе. Вместо этого эти члены теперь инициализируются в первом вызове bindView(). Таким образом, теперь есть время для асинхронного извлечения детских курсоров. Поскольку между API 4 и API 9 не было никаких изменений в общедоступном API этого класса (за исключением нового метода setViewText()), ваше решение использовать API версии 9 этого класса должно работать без изменений! Я попробовал, и получилось!

Большое спасибо, mp2526!