问题 CursorLoader的onLoadFinished回调中的RxJava2


要从我使用的数据库中获取数据 CursorLoader 在应用程序中。一旦 onLoadFinished() callback方法调用app转换的逻辑 Cursor 反对 List 业务模型要求中的对象。如果有大量数据,那么转换(繁重操作)需要一些时间。这会降低UI线程的速度。我尝试在非UI中启动转换 Thread 运用 RxJava2 通过 Cursor 对象,但得到了 Exception

Caused by: android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.

这是部分内容 Fragment的代码:

@Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        QueryBuilder builder;
        switch (id) {
            case Constants.FIELDS_QUERY_TOKEN:
                builder = QueryBuilderFacade.getFieldsQB(activity);
                return new QueryCursorLoader(activity, builder);
            default:
                return null;
        }
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        if (cursor.getCount() > 0) {
            getFieldsObservable(cursor)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(this::showFields);
        } else {
            showNoData();
        }
    }

private static Observable<List<Field>> getFieldsObservable(Cursor cursor) {
            return Observable.defer(() -> Observable.just(getFields(cursor))); <-- Exception raised at this line

        }

private static List<Field> getFields(Cursor cursor) {
            List<Field> farmList = CursorUtil.cursorToList(cursor, Field.class);
            CursorUtil.closeSafely(cursor);
            return farmList;
        }

使用目的 CursorLoader 如果有数据存储更新,这里是从DB获取通知。

更新 正如Tin Tran所说,我删除了 CursorUtil.closeSafely(cursor); 现在我得到另一个例外:

Caused by: java.lang.IllegalStateException: attempt to re-open an already-closed object: /data/user/0/com.my.project/databases/db_file
                                                          at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
                                                          at android.database.CursorWindow.getNumRows(CursorWindow.java:225)
                                                          at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:121)
                                                          at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:236)
                                                          at android.database.AbstractCursor.moveToNext(AbstractCursor.java:274)
                                                          at android.database.CursorWrapper.moveToNext(CursorWrapper.java:202)
                                                          at com.db.util.CursorUtil.cursorToList(CursorUtil.java:44)
                                                          at com.my.project.MyFragment.getFields(MyFragment.java:230)

cursorToList() 的方法 CursorUtil

public static <T> ArrayList<T> cursorToList(Cursor cursor, Class<T> modelClass) {
        ArrayList<T> items = new ArrayList<T>();
        if (!isCursorEmpty(cursor)) {
            while (cursor.moveToNext()) { <-- at this line (44) of the method raised that issue
                final T model = buildModel(modelClass, cursor);
                items.add(model);
            }
        }
        return items;
    }

8229
2018-06-06 12:29


起源

不是答案,但我强烈建议你去看看 sqlbrite。 - Lukasz
@Lukasz,是的,我知道sqlbrite并且它将是一个很好的解决方案,但我不能将它添加到项目,因为它已经很大并使用了另一个orm。 - devger
可以在处理以前的数据时更新此列表吗? - azizbekian


答案:


如你所见 我的评论 对于你的问题,我感兴趣的是数据是否正在更新 getFieldsObservable() 尚未归还。我收到了我感兴趣的信息 在你的评论中

我可以判断,这是你的情况:

  • onLoadFinished() 用Cursor-1调用
  • RxJava的方法正在另一个使用Cursor-1的线程上执行(尚未完成,这里使用Cursor-1)
  • onLoadFinished() 使用Cursor-2调用,LoaderManager API负责关闭 Cursor-1,RxJava仍然在另一个线程上查询

因此,会产生异常。

所以,你最好坚持创建你的自定义 AsyncTaskLoader (哪一个 CursorLoader 延伸自)。这个 AsyncTaskLoader 将纳入所有的逻辑 CursorLoader 有(基本上是一对一的复制),但会返回已经排序/过滤的对象 onLoadFinished(YourCustomObject)。因此,您希望使用RxJava执行的操作实际上将由您的加载器完成 loadInBackground() 方法。

这是变化的快照 MyCustomLoader 将有 loadInBackground() 方法:

public class MyCustomLoader extends AsyncTaskLoader<PojoWrapper> {
  ...
  /* Runs on a worker thread */
  @Override
  public PojoWrapper loadInBackground() {
    ...
    try {
      Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
          mSelectionArgs, mSortOrder, mCancellationSignal);
      ...

      // `CursorLoader` performs following:
      // return cursor;

      // We perform some operation here with `cursor`
      // and return PojoWrapper, that consists of `cursor` and `List<Pojo>`
      List<Pojo> list = CursorUtil.cursorToList(cursor, Field.class);
      return new PojoWrapper(cursor, list);
    } finally {
      ...
    }
  }
  ...
}

哪里 PojoWrapper 是:

public class PojoWrapper {
  Cursor cursor;
  List<Pojo> list;

  public PojoWrapper(Cursor cursor, List<Pojo> list) {
    this.cursor = cursor;
    this.list = list;
  }
}

因此,在 onLoadFinished() 你不必把工作委托给另一个线程,因为你已经在你的工作中完成了 Loader 执行:

@Override public void onLoadFinished(Loader<PojoWrapper> loader, PojoWrapper data) {
      List<Pojo> alreadySortedList = data.list;
}

这里的 整个代码 MyCustomLoader


6
2018-06-09 12:02



很清楚/很好的答案 - Tin Tran


一旦知道应用程序不再使用它,加载器就会释放数据。例如,如果数据是来自CursorLoader的游标,则不应自行调用close()。   从: https://developer.android.com/guide/components/loaders.html

你不应该自己关闭光标我认为 CursorUtil.closeSafely(cursor) 确实。

您可以使用 switchMap 运营商实现这一点。它完全符合我们的要求

private PublishSubject<Cursor> cursorSubject = PublishSubject.create()

public void onCreate(Bundle savedInstanceState) {
    cursorSubject
        .switchMap(new Func1<Cursor, Observable<List<Field>>>() {
             @Override public Observable<List<Field>> call(Cursor cursor) {
                  return getFieldsObservable(cursor);
             }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(this::showFields);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    cursorSubject.onNext(cursor)
}

你现在需要修改 showFields 和 getFieldsObservable 占空 Cursor


4
2018-06-09 07:56



是的,确实关闭了 Cursor。但当他不再需要时,OP会关闭它,这意味着他已经拥有它 “解析”  farmList 来自 Cursor 他不再需要离开那个 Cursor 打开。我觉得这里没问题。 - azizbekian
是的,但在他关闭之后 Cursor, CursorLoader 可能仍需要访问光标。如果确实如此,那么 Exception被抛出 - Tin Tran
我明白你的意思了。我不知道是否 LoaderManager 表现不够聪明 if 检查并加载另一个时间 Cursor 关闭了。 - azizbekian
我已经测试了没有关闭光标,因为Tin Tran建议并得到另一个例外,添加了跟踪作为问题的更新。 - devger
问题是 Cursor 不是线程安全的。当您将光标转换为 List<Field>一个新的 Cursor 到达 onLoadFinished 并且旧的光标很接近。因此,访问它的io线程将抛出异常。 - Tin Tran