【问题标题】:Android ContentProvider calls bursts of setNotificationUri() to CursorAdapter when many rows are inserted with a batch operation当使用批处理操作插入许多行时,Android ContentProvider 会向 CursorAdapter 调用 setNotificationUri() 突发
【发布时间】:2012-04-05 18:37:38
【问题描述】:

我有一个自定义的ContentProvider,它管理对 SQLite 数据库的访问。要将数据库表的内容加载到 ListFragment 中,我将 LoaderManagerCursorLoaderCursorAdapter 一起使用:

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 实现并覆盖其任何方法。

【问题讨论】:

  • 考虑在SQLiteContentProvider 之类的东西上做/基于你的提供者——它提供了一个基于 SQLite 的优秀和功能提供者的大部分基础——如果你这样做——你应该简单地使用 bulkInsert 或 applyBatch做你的“大量插入”。额外的好处是您的批量插入将在事务中执行,这会大大加快它们的速度。
  • @Jens 非常感谢您的指点,这看起来正是我所追求的。只是一个问题:谁开发并发布了这个代码?从标题看,它似乎是 Android 开源项目的一部分,但是,如果是这样,为什么 Google 没有发布标准 Android SDK?
  • 它是 AOSP 项目的一部分,但未在 SDK 中发布 - 为什么我不知道,因为包含它会阻止很多人编写他们自己的蹩脚的 ContentProvider 实现。
  • 我刚刚添加了一个关于此的功能请求问题:code.google.com/p/android/issues/detail?id=28597
  • 为什么@Jens 不将他的评论移至回答,而 Lorenzo Polidori 接受它作为答案。这对社区有好处。

标签: android sqlite android-contentprovider android-cursor android-cursoradapter


【解决方案1】:

我从 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();
    }
}

【讨论】:

  • 感谢@Dia,这似乎是最好和最简单的解决方案。而且由于它过于简单和通用,我想知道为什么 google 不将其作为所有批处理操作的默认值
  • 对我来说,这会触发同样多的事件,因为它仍然多次调用 insert(...) 方法,因此调用它在 insert(... ) 方法。
  • 只有一条评论,“这导致数量最小化”实际上它只是将通知推迟到执行结束。感谢您提出的解决方案!
  • 您可以将 for 循环替换为 ContentProviderResult[] results = super.applyBatch(operations);
【解决方案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,而不是直接触发通知。

可能有更好的方法来做到这一点 - 看起来确实应该是这样 - 但我就是这样做的。

【讨论】:

  • 非常感谢您的回答。为这个想法 +1,这是我想到的选项之一,但我宁愿使用更灵活和通用的方法,从 Jens 建议的抽象 SQLiteContentProvider 派生。
【解决方案3】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-24
    • 1970-01-01
    • 1970-01-01
    • 2015-03-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-10
    • 2021-10-17
    相关资源
    最近更新 更多