Android ContentProvider вызывает всплески setNotificationUri() в CursorAdapter, когда много строк вставляются с пакетной операцией

У меня есть пользовательский ContentProvider, который управляет доступом к базе данных SQLite. Чтобы загрузить содержимое таблицы базы данных в ListFragment, я использую LoaderManager с CursorLoader и CursorAdapter:

public class MyListFragment extends ListFragment implements LoaderCallbacks<Cursor> {
    // ...
    CursorAdapter mAdapter;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mAdapter = new CursorAdapter(getActivity(), null, 0);
        setListAdapter(mAdapter);
        getLoaderManager().initLoader(LOADER_ID, null, this);
    }

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), CONTENT_URI, PROJECTION, null, null, null);
    }

    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
        mAdapter.swapCursor(c);
    }

    public void onLoaderReset(Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }
}

База данных SQLite обновляется с помощью фоновой задачи, которая извлекает несколько элементов из веб-службы и вставляет эти элементы в базу данных с помощью ContentProvider пакетных операций (ContentResolver#applyBatch()).

Даже если это пакетная операция, вызывает вызов ContentProvider#insert() для каждой строки, которая вставляется в базу данных, а в текущей реализации ContentProvider вызывает setNotificationUri() для каждой команды вставки.

В результате CursorAdapter получает всплески уведомлений, в результате чего пользовательский интерфейс часто обновляется с последующим раздражающим мерцающим эффектом.

В идеале, когда выполняется пакетная операция, должен быть способ оповестить ContentObserver только в конце любой пакетной операции, а не с каждой командой вставки.

Кто-нибудь знает, возможно ли это? Обратите внимание, что я могу изменить реализацию ContentProvider и переопределить любые его методы.

Ответ 1

Я нашел более простое решение, которое я обнаружил из приложения ввода-вывода Google. Вам просто нужно переопределить метод applyBatch в ContentProvider и выполнить все операции в транзакции. Уведомления не отправляются до момента совершения транзакции, что приводит к минимизации количества уведомлений об изменениях ContentProvider, которые отправляются:

@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {

    final SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
    db.beginTransaction();
    try {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        db.setTransactionSuccessful();
        return results;
    } finally {
        db.endTransaction();
    }
}

Ответ 2

Чтобы решить эту проблему, я переопределил applyBatch и установил флаг, который блокировал другие методы при отправке уведомлений.

    volatile boolean applyingBatch=false;
    public ContentProviderResult[] applyBatch(
        ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    applyingBatch=true;
    ContentProviderResult[] result;
    try {
        result = super.applyBatch(operations);
    } catch (OperationApplicationException e) {
        throw e;
    }
    applyingBatch=false;
    synchronized (delayedNotifications) {
        for (Uri uri : delayedNotifications) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
    }
    return result;
}

Я выставил метод "хранения" уведомлений, которые нужно отправить, когда пакет был завершен:

protected void sendNotification(Uri uri) {
    if (applyingBatch) {
        if (delayedNotifications==null) {
            delayedNotifications=new ArrayList<Uri>();
        }
        synchronized (delayedNotifications) {
            if (!delayedNotifications.contains(uri)) {
                delayedNotifications.add(uri);
            }
        }
    } else {
        getContext().getContentResolver().notifyChange(uri, null);
    }
}

И любые методы отправки уведомлений используют sendNotification, а не прямое извещение об уведомлении.

Там могут быть лучшие способы сделать это - конечно, кажется, что они должны быть, но это то, что я сделал.

Ответ 3

В комментарии к исходному ответу Йенс направил нас к SQLiteContentProvider в AOSP. Одна из причин, почему это не является (пока?) В SDK, может заключаться в том, что AOSP, похоже, содержит несколько вариантов этого кода.

Например com.android.browser.provider.SQLiteContentProvider представляется чуть более полным решением, включая предлагаемый принцип "отложенных уведомлений" Phillip Fitzsimmons, сохраняя при этом поточный поток провайдера, используя ThreadLocal для пакетного флага и синхронизируя доступ к набору задержанных уведомлений.

Тем не менее, хотя доступ к набору URI, который будет уведомлен об изменении, синхронизирован, я все еще могу предположить, что могут возникнуть условия гонки. Например, если длинная операция помещает некоторые уведомления, то их обгоняет меньшая операция партии, которая запускает уведомления и очищает набор до того, как совершит первую операцию.

Тем не менее, вышеприведенная версия, по-видимому, является лучшей ставкой в ​​качестве ссылки при реализации вашего собственного провайдера.