SQLite Android数据库Cursor窗口分配2048 kb失败。

79
我有一个程序,每秒钟会反复运行不同的查询来访问SQLite数据库。一段时间后,我会在LogCat中看到错误信息:"android.database.CursorWindowAllocationException: - Cursor window allocation of 2048 kb failed. # Open Cursors = "
我让应用记录内存使用情况,并且当使用达到某个限制时,我就会得到这个错误,表明内存已经用完了。我的直觉告诉我,每次运行查询时,数据库引擎都会创建一个新的缓冲区(CursorWindow),即使我关闭了游标,既无法及时地释放内存,垃圾回收器也无能为力,SQLiteDatabase.releaseMemory()也无法快速释放内存。我认为解决方案可能在于“强制”数据库始终写入同一个缓冲区,而不是创建新的缓冲区,但我一直没有找到这样做的方法。我尝试过实例化自己的CursorWindow,并将SQLiteCursor设置为它,但没有成功。
有什么想法吗?
编辑:根据@GrahamBorland的要求,提供示例代码:
public static CursorWindow cursorWindow = new CursorWindow("cursorWindow"); 
public static SQLiteCursor sqlCursor;
public static void getItemsVisibleArea(GeoPoint mapCenter, int latSpan, int lonSpan) {
query = "SELECT * FROM Items"; //would be more complex in real code
sqlCursor = (SQLiteCursor)db.rawQuery(query, null);
sqlCursor.setWindow(cursorWindow);
}
理想情况下,我希望在提供新查询之前能够使用 .setWindow() 并且每次获取新数据时都将数据放入同一个 CursorWindow

我不知道问题出在哪里,但我习惯将SQLiteOpenHelper类设为单例模式。因此,我从未遇到过这样的问题。 - Mohsin Naeem
不,我不使用SQLiteOpenHelper,我创建了一个包含SQLiteDatabase的静态DataAccess类。这很好用,我怀疑问题不在那里。问题更多地与SQLite库有关,它们会创建一个新容器来放置每个新查询的结果,而不是一遍又一遍地使用同一个容器。虽然我可以关闭游标,但GC清理的速度比新容器创建的速度慢,从而产生内存占用问题。 - alex
8个回答

113

这种错误最常见的原因是未关闭游标。确保在使用完毕后关闭所有游标(即使出现错误的情况下也要关闭)。

Cursor cursor = null;
try {
    cursor = db.query(...
    // do some work with the cursor here.
} finally {
    // this gets called even if there is an exception somewhere above
    if(cursor != null)
        cursor.close();
}

为了在没有关闭游标时使您的应用程序崩溃,您可以在应用程序的onCreate中启用Strict Mode和detectLeakedSqlLiteObjects

StrictMode.VmPolicy policy = new StrictMode.VmPolicy.Builder()
   .detectLeakedClosableObjects()
   .detectLeakedSqlLiteObjects()
   .penaltyDeath()
   .penaltyLog()
   .build();
StrictMode.setVmPolicy(policy);

显然,您只会在调试版本中启用此选项。


你实际上可以简化那个例子,以避免空值和空值检查。 - aij
3
就像打开文件指针一样,始终在finally部分处理关闭操作以确保代码的清晰退出。 - slott

86

如果你需要在大量的SQL代码中查找问题,可以通过将以下代码片段放在MainActivity中启用StrictMode来加速调试。如果检测到泄漏的数据库对象,则应用程序现在会崩溃并记录日志信息,突出显示泄漏的位置。这帮助我在几分钟内找到了一个不受控制的光标。

@Override
protected void onCreate(Bundle savedInstanceState) {
   if (BuildConfig.DEBUG) {     
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
         .detectLeakedSqlLiteObjects()
         .detectLeakedClosableObjects()
         .penaltyLog()
         .penaltyDeath()
         .build());
    }
    super.onCreate(savedInstanceState);
    ...
    ...

7
太好了!我已经在我的主类的一个静态块内使用了 if (BuildConfig.DEBUG) {...} 来制定这个标准。 - Brian White
太好了!但在发布时,它会在没有StrictMode的情况下工作吗? - alfdev
根据StrictMode javadoc的说法:“Android的未来版本可能会捕获更多(或更少)的操作,因此您不应该在分发到Google Play的应用程序中启用StrictMode。” - demaksee

11

我刚刚遇到了这个问题,而建议不关闭游标的答案并没有解决我的问题。我的问题是当SQLite试图重新填充其游标时关闭了数据库。我打开数据库,查询数据库以获取一个指向数据集的游标,关闭数据库并遍历游标。我注意到每当我在该游标中达到某个记录时,我的应用程序就会崩溃,并出现OP中相同的错误。

我认为游标需要访问某些记录时,它需要重新查询数据库,如果关闭数据库,则会发生此错误。我通过在完成所需工作之前不关闭数据库来解决了这个问题。


5

Android SQLite游标窗口可以承载的最大大小确实存在,即2MB。如果超过此大小,将会导致上述错误。通常,这个错误是由于在SQL数据库中存储了作为blob的大型图像字节数组或过长字符串而引起的。以下是我如何解决它的方法。

创建一个Java类,例如FixCursorWindow,并将下面的代码放入其中。

    public static void fix() {
        try {
            Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
            field.setAccessible(true);
            field.set(null, 102400 * 1024); //the 102400 is the new size added
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

现在,请到您的应用程序类(如果没有,请创建一个)并像这样调用FixCursorWindow: public class App extends Application {
public void onCreate()
{
    super.onCreate();
    CursorWindowFixer.fix();

}

最后,请确保将您的应用程序类添加到清单文件中的应用程序标记中,如下所示:

    android:name=".App">

That's all, it should work perfectly now.


1
由于使用反射API,您可能会在Android Pie上遇到问题。 - Alex Kucherenko
这不是正确的解决方案。 - Subhalaxmi

1
如果您正在运行Android P,则可以按照以下方式创建自己的光标窗口:
if(cursor instanceof SQLiteCursor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    ((SQLiteCursor) cursor).setWindow(new CursorWindow(null, 1024*1024*10));
}

这样可以让您修改特定光标的光标窗口大小,而无需使用反射。

1
这里是 @whlk 答案,涉及 Java 7 中的 try-finally 块的 自动资源管理
try (Cursor cursor = db.query(...)) {
    // do some work with the cursor here.
}

-1

当我们使用外部SQLite时,这是一个常见的异常。您可以通过关闭Cursor对象来解决它,就像以下方式一样:

if(myCursor != null)
        myCursor.close();

它的意思是,如果光标具有内存并且已经打开,则关闭它,以使应用程序更快,所有方法占用的空间更小,并且与数据库相关的功能也将得到改进。


-3
public class CursorWindowFixer {

  public static void fix() {
    try {
      Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
      field.setAccessible(true);
      field.set(null, 102400 * 1024);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

突破CursorWindow仅有2兆字节的限制。 - jingyuan iu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接