2

在我的 android 应用程序中,我使用 Room 和 sqlcipher 库进行加密/解密。我经常在 Crashlytic 中看到以下崩溃:

java.util.concurrent.TimeoutException:net.sqlcipher.database.SQLiteCompiledSql.finalize() 在 java.util.concurrent.locks.LockSupport.park(LockSupport 的 sun.misc.Unsafe.park(Native Method) 处的 10 秒后超时.java:190) 在 java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:902) 在 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) 在 net.sqlcipher.database.SQLiteDatabase.lock(SQLiteDatabase.java:567) 在 net.sqlcipher.database.SQLiteCompiledSql。releaseSqlStatement(SQLiteCompiledSql.java:104) 在 net.sqlcipher.database.SQLiteCompiledSql.finalize(SQLiteCompiledSql.java:146) 在 java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:289) 在 java.lang.Daemons$FinalizerDaemon .runInternal(Daemons.java:276) 在 java.lang.Daemons$Daemon.run(Daemons.java:137) 在 java.lang.Thread.run(Thread.java:929)

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

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

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

4

1 回答 1

2

我已经在 Crashlytics 中看到了很长一段时间,并且似乎最终能够完全解决这个问题,我将在这里发布完整的研究。

问题

对于这样的查询:

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

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

在此处输入图像描述

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

在此处输入图像描述

问题是数据库可能被锁定超过 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 阶段和专用线程上。

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

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

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

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

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

于 2021-03-17T22:57:15.567 回答