如何实现无限的 CursorAdapter?
How to implement an endless CursorAdapter?
在我们公司,我们正在开发一个显示时间轴的应用程序。我们愿意让用户(几乎)无限期地滚动它。
现在有 2 个事实需要考虑:
- 加载大游标会对性能产生不良影响(尤其是对于旧设备)
- 游标 seem to have a size limit,共 1MB
在当前的实现中,我们默认加载 40 个项目,然后当用户滚动超过某个阈值时,我们通过将限制增加到 40+20 个项目来重复查询,依此类推。
然而,这种方法似乎相当薄弱,因为它与前面所述的两个原则相冲突:查询最终会变得相当大,并且在某些时候光标可能会命中1MB 的内存限制(我们加载了很多字符串)。
现在我们正在考虑利用 MergeCursor 并像这样进行:
- 第一次加载 40 个项目的游标
- 当用户滚动超过一定级别时,我们加载另一个包含接下来的 40 个项目的游标,并在游标适配器中设置一个 MergeCursor 将新游标连接到前一个游标.
- 继续这种方法直到最多 X 步(取决于测试)以避免遇到一些 OOM 异常。最后,时间轴光标将是 X 光标的串联。
您如何看待这种做法?任何弱点(除了开销,应该很小)?
万一,你能point/describe更好的解决方案吗?
提前致谢
在评论中 pskink 建议使用 AbstractWindowedCursor
.
我不熟悉这个 class 并调查了一下。事实证明 SQLiteCursor
已经扩展了它。文档说明了这一点:
The cursor owns the cursor window it uses. When the cursor is closed, its window is also closed. Likewise, when the window used by the cursor is changed, its old window is closed. This policy of strict ownership ensures that cursor windows are not leaked.
这意味着在任何给定时刻,只有一小部分从数据库查询的数据实际上保存在内存中。这是 SQLiteCursor
:
中代码的有趣部分
@Override
public boolean onMove(int oldPosition, int newPosition) {
// Make sure the row at newPosition is present in the window
if (mWindow == null || newPosition < mWindow.getStartPosition() ||
newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
fillWindow(newPosition);
}
return true;
}
@Override
public int getCount() {
if (mCount == NO_COUNT) {
fillWindow(0);
}
return mCount;
}
private void fillWindow(int requiredPos) {
clearOrCreateWindow(getDatabase().getPath());
try {
if (mCount == NO_COUNT) {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
mCursorWindowCapacity = mWindow.getNumRows();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
}
} else {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
mCursorWindowCapacity);
mQuery.fillWindow(mWindow, startPos, requiredPos, false);
}
} catch (RuntimeException ex) {
// Close the cursor window if the query failed and therefore will
// not produce any results. This helps to avoid accidentally leaking
// the cursor window if the client does not correctly handle exceptions
// and fails to close the cursor.
closeWindow();
throw ex;
}
}
这意味着两件事:
- 加载整个数据集应该是安全的,因为它不会完全保存在内存中。任何时候只有它的一部分(
CursorWindow
)在内存中。 1MB 的大小限制要么(可能)是一个神话,要么它指的是 CursorWindow 对象,在这种情况下它是一个安全大小
- 性能应该不是问题,因为游标总是处理固定数量的数据。初始查询(计算数据集的总大小,存储在
mCount
变量中)可能会对感知性能产生一些影响。我需要进一步测试这个。
总之,很可能没有必要使用 MergeCursor 技巧或过度担心 OOM。
我本可以在源代码中进行更好的调查,但我在网上阅读的内容让我有点受骗。
在我们公司,我们正在开发一个显示时间轴的应用程序。我们愿意让用户(几乎)无限期地滚动它。
现在有 2 个事实需要考虑:
- 加载大游标会对性能产生不良影响(尤其是对于旧设备)
- 游标 seem to have a size limit,共 1MB
在当前的实现中,我们默认加载 40 个项目,然后当用户滚动超过某个阈值时,我们通过将限制增加到 40+20 个项目来重复查询,依此类推。
然而,这种方法似乎相当薄弱,因为它与前面所述的两个原则相冲突:查询最终会变得相当大,并且在某些时候光标可能会命中1MB 的内存限制(我们加载了很多字符串)。
现在我们正在考虑利用 MergeCursor 并像这样进行:
- 第一次加载 40 个项目的游标
- 当用户滚动超过一定级别时,我们加载另一个包含接下来的 40 个项目的游标,并在游标适配器中设置一个 MergeCursor 将新游标连接到前一个游标.
- 继续这种方法直到最多 X 步(取决于测试)以避免遇到一些 OOM 异常。最后,时间轴光标将是 X 光标的串联。
您如何看待这种做法?任何弱点(除了开销,应该很小)?
万一,你能point/describe更好的解决方案吗?
提前致谢
在评论中 pskink 建议使用 AbstractWindowedCursor
.
我不熟悉这个 class 并调查了一下。事实证明 SQLiteCursor
已经扩展了它。文档说明了这一点:
The cursor owns the cursor window it uses. When the cursor is closed, its window is also closed. Likewise, when the window used by the cursor is changed, its old window is closed. This policy of strict ownership ensures that cursor windows are not leaked.
这意味着在任何给定时刻,只有一小部分从数据库查询的数据实际上保存在内存中。这是 SQLiteCursor
:
@Override
public boolean onMove(int oldPosition, int newPosition) {
// Make sure the row at newPosition is present in the window
if (mWindow == null || newPosition < mWindow.getStartPosition() ||
newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
fillWindow(newPosition);
}
return true;
}
@Override
public int getCount() {
if (mCount == NO_COUNT) {
fillWindow(0);
}
return mCount;
}
private void fillWindow(int requiredPos) {
clearOrCreateWindow(getDatabase().getPath());
try {
if (mCount == NO_COUNT) {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
mCursorWindowCapacity = mWindow.getNumRows();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
}
} else {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
mCursorWindowCapacity);
mQuery.fillWindow(mWindow, startPos, requiredPos, false);
}
} catch (RuntimeException ex) {
// Close the cursor window if the query failed and therefore will
// not produce any results. This helps to avoid accidentally leaking
// the cursor window if the client does not correctly handle exceptions
// and fails to close the cursor.
closeWindow();
throw ex;
}
}
这意味着两件事:
- 加载整个数据集应该是安全的,因为它不会完全保存在内存中。任何时候只有它的一部分(
CursorWindow
)在内存中。 1MB 的大小限制要么(可能)是一个神话,要么它指的是 CursorWindow 对象,在这种情况下它是一个安全大小 - 性能应该不是问题,因为游标总是处理固定数量的数据。初始查询(计算数据集的总大小,存储在
mCount
变量中)可能会对感知性能产生一些影响。我需要进一步测试这个。
总之,很可能没有必要使用 MergeCursor 技巧或过度担心 OOM。
我本可以在源代码中进行更好的调查,但我在网上阅读的内容让我有点受骗。