Android ContentProvider вызывает пакеты setNotificationUri () для CursorAdapter, когда много строк вставляются с помощью пакетной операции - PullRequest
11 голосов
/ 21 марта 2012

У меня есть пользовательский 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 и переопределить любой из ее методов.

Ответы [ 3 ]

13 голосов
/ 20 мая 2012

Я нашел более простое решение, обнаруженное в приложении ввода-вывода 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();
    }
}
7 голосов
/ 21 марта 2012

Чтобы решить эту проблему, я переопределил 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 вместо непосредственного запуска уведомления.

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

0 голосов
/ 22 ноября 2012

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

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

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

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

...