TimeoutException: net.sqlcipher.database.SQLiteCompiledSql.finalize() 在 10 秒后超时 (Android)

TimeoutException: net.sqlcipher.database.SQLiteCompiledSql.finalize() timed out after 10 seconds (Android)

在我的 android 应用程序中,我将 Room 与 encrypt/decrypt 的 sqlcipher 库一起使用。我经常在 Crashlytic 中看到以下崩溃:

java.util.concurrent.TimeoutException: net.sqlcipher.database.SQLiteCompiledSql.finalize() timed out after 10 seconds at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:868) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:902) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1227) at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:231) at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:294) at net.sqlcipher.database.SQLiteDatabase.lock(SQLiteDatabase.java:567) at net.sqlcipher.database.SQLiteCompiledSql.releaseSqlStatement(SQLiteCompiledSql.java:104) at net.sqlcipher.database.SQLiteCompiledSql.finalize(SQLiteCompiledSql.java:146) at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:289) at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:276) at java.lang.Daemons$Daemon.run(Daemons.java:137) at java.lang.Thread.run(Thread.java:929)

它崩溃的行是SQLiteDatabase.lock() line 567

之前是第 566 行,但在那个方法中我插入了一个检查:如果数据库未打开 -> return 并且不继续锁定,但它没有帮助并且崩溃又出现了。 我认为这次崩溃可能是因为垃圾收集发生在应用程序处于后台时(我们的应用程序有一个一直运行的前台服务)。但不确定如何修复它。 至于房间:我没有关闭它,它一直是打开的,因为我的应用程序一直在工作,所以经常需要它。在每次查询数据库后关闭它是不好的做法。

我问过 sqlcipher 的开发人员,但他们不知道,什么会导致此崩溃。也许有人知道?

我在 Crashlytics 中看到这个问题已经有一段时间了,似乎最终能够完全解决这个问题,我将 post 在这里进行完整的研究。

问题

对于这样的查询:

@Query("DELETE FROM table WHERE id NOT IN (:ids)")
abstract fun deleteNotInList(ids: List<String>): Int

Room 生成的代码不清除游标和生成的语句(在下图中,比较方法一个不使用游标,也不调用 release() 方法,与底部的方法相比calls cursor.close(); 那里的方法名称和查询略有不同,因为我简化了片段):

在这种情况下,语句保留在内存中未释放,释放转移到 GC 阶段,在 finalize() 方法内。而SqlCipher(SQLiteCompiledSql)中的finalize()又需要锁住Database才能释放语句:

问题是数据库可能被锁定超过 10 秒的长事务(或一批较短的事务,使用此类锁的唤醒顺序无法保证且不公平)。

GC 看门狗在达到 10/20 秒时崩溃线程,具体取决于 OS/JVM 确切版本。

解决方案

是使用 IN 运算符重写所有 DELETE / UPDATE 和手动原始查询,如下所示:

@RawQuery
abstract fun deleteNotInListRaw(query: SimpleSQLiteQuery): Int

fun deleteNotInList(
    ids: List<String>
) {
    deleteNotInListRaw(
        SimpleSQLiteQuery(
            "DELETE FROM table WHERE id NOT IN (${ids.joinToString(prefix = "'", postfix = "'", separator = "','")})"
        )
    )
}

在这种情况下,查询使用游标并在删除完成后将其关闭,仍然锁定数据库,但不在 GC 阶段和专用线程上。

P.S. 可能有更稳定的长期解决方案,但它们需要在 Room / SqlCipher 方面实施。

在给定状态下,SqlCipher 可以重构为在 GC 阶段不锁定数据库。有一个悬而未决的问题:https://github.com/sqlcipher/android-database-sqlcipher/issues/537

Room 应该修复代码生成器并使用查询构建器并生成语句关闭行,那里没有未解决的问题,但我稍后会仔细检查这个想法并将其作为问题提出给他们。

对我们来说,这似乎完全解决了这个问题。

请注意,所有 DELETE / UPDATE 查询都使用 IN / NOT IN 运算符,可能还有一些其他运算符会阻止 Room 预编译查询(由于运行时参数)导致这个。您可以检查代码生成器以验证生成的代码调用 cursor.close()statement.release()