11

我有一个自定义ContentProvider管理对 SQLite 数据库的访问。要在 a 中加载数据库表的内容ListFragment,我使用LoaderManagerwith aCursorLoader和 a 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 数据库由后台任务更新,该任务从 Web 服务中获取许多项目,并通过ContentProvider批处理操作将这些项目插入数据库 ( ContentResolver#applyBatch())。

即使这是一个批处理操作,ContentProvider#insert()每个行的调用都会被插入到数据库中,并且在当前的实现中,每个插入命令的ContentProvider调用setNotificationUri()

结果是CursorAdapter接收到大量通知,导致 UI 更新过于频繁,从而产生烦人的闪烁效果。

理想情况下,当批处理操作正在进行时,应该有一种方法ContentObserver只在任何批处理操作结束时通知,而不是在每个插入命令时通知。

有人知道这是否可能吗?请注意,我可以更改ContentProvider实现并覆盖其任何方法。

4

3 回答 3

13

我从 Google 的 I/O 应用程序中发现了一个更简单的解决方案。您只需覆盖 ContentProvider 中的 applyBatch 方法并在事务中执行所有操作。在事务提交之前不会发送通知,这会最大限度地减少发送出去的 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();
    }
}
于 2012-05-19T21:57:01.300 回答
7

为了解决这个确切的问题,我覆盖了 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,而不是直接触发通知。

很可能有更好的方法来做到这一点——看起来它们当然应该是这样——但这就是我所做的。

于 2012-03-21T09:48:01.603 回答
0

在对原始答案的评论中,Jens 将我们引向 AOSP 中的 SQLiteContentProvider。SDK 中没有(还没有?)的一个原因可能是 AOSP 似乎包含此代码的多个变体。

例如com.android.browser.provider.SQLiteContentProvider似乎是一个稍微完整的解决方案,它结合了 Phillip Fitzsimmons 提出的“延迟通知”原则,同时通过使用 ThreadLocal 进行批处理标志和同步访问来保持提供者线程安全到延迟通知集。

然而,即使对要通知更改的 URI 集的访问是同步的,我仍然可以想象可能会发生竞争条件。例如,如果一个长操作发布了一些通知,然后被一个较小的批处理操作取代,该操作会触发通知并在第一个操作提交之前清除集合。

尽管如此,在实现您自己的提供程序时,上述版本似乎是作为参考的最佳选择。

于 2012-11-22T00:25:44.407 回答