Android SQLite 报错详解:unable to open database file (code 14)

QuibblerQuibbler 2020-01-18 约 15 分钟 197 次阅读

Android SQLite 报错详解:unable to open database file (code 14)

在 Android 上用 SQLite,有一个"神出鬼没"的崩溃:SQLiteCantOpenDatabaseException: unable to open database file(错误码 code 14)。它往往不是数据库本身坏了,而是 SQLite 临时文件写不进去导致的。本文从异常现象、SQLite 的临时文件机制讲起,给出排查步骤与几种根治方案,帮你彻底解决这个 "SQL 在安卓上的 Bug"。

1、现象:code 14 是什么

报错通常长这样——执行查询或写入时突然抛异常,堆栈一路追到 native 执行 SQL 的位置:

W/System.err: SQLiteCantOpenDatabaseException: unable to open database file (code 14)
W/System.err:     at ...SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
W/System.err:     at ...SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:913)
W/System.err:     at ...SQLiteSession.executeForCursorWindow(SQLiteSession.java:819)
W/System.err:     at ...SQLiteQuery.fillWindow(SQLiteQuery.java:62)
W/System.err:     at ...SQLiteCursor.getCount(SQLiteCursor.java:147)

这里的 code 14 = SQLITE_CANTOPEN,是 SQLite 的原生错误码,字面意思是"无法打开数据库文件"。但它不一定指主数据库文件——很多时候数据库本身能打开,反而是 SQLite 在执行过程中要创建的"临时文件"打不开,于是抛出同一个错误码。这个"误区"正是排查这道题的第一道坎。

2、根因:SQLite 的临时文件机制

要理解为什么会"打不开",得先知道 SQLite 在跑事务时会偷偷创建一堆临时文件:

// SQLite 默认的回滚日志模式(journal mode = DELETE/PERSIST/TRUNCATE)
//   事务开始 → 在 db 文件旁创建临时文件 .db-journal,记录修改前的副本
//   事务提交 → 删除/清空该 journal
//   一旦提交过程中崩溃,靠 journal 回滚保证原子性

// WAL 模式(Write-Ahead Logging)
//   事务写入 .db-wal + .db-shm 两个辅助文件,定期 checkpoint 合并回主库

// 此外:临时表、临时索引、大查询的中间结果,也都会用到临时文件

关键点:这些临时文件默认就建在"数据库文件所在的目录"(或由 `PRAGMA temp_store_directory` 指定的临时目录)。所以一旦那个目录不可写,事务一启动、journal 创建失败,立刻报 SQLITE_CANTOPEN——表现就是"查询/写入时偶发崩溃",且常常时有时无(和临时文件的创建、清理时机有关)。

3、为什么 Android 上会触发

在 Android 上,触发"目录不可写"的常见情形有这些:

// 情形1:把数据库放到了外部存储 / sdcard
//   /sdcard/xxx/app.db  —— Android 6.0+ 需运行时申请存储权限,
//   Android 10+ 引入 scoped storage,外存权限大幅收紧,
//   临时文件(.db-journal)很可能写不进去
//
// 情形2:自定义数据库路径,但父目录不存在或不可写
//   例如直接指向某个未 mkdir 的目录,或受 SELinux 策略限制的路径
//
// 情形3:磁盘空间不足
//   journal/wal 需要落盘,磁盘满则创建失败
//
// 情形4:多进程并发访问同一 db 文件
//   journal 文件锁竞争,或主从库交替持有 journal
//
// 情形5:数据库被置为只读挂载,或所在目录被系统限制为只读

这类问题的共性是:主 db 文件本身能读,但"它旁边/它依赖的目录"写不了。这就是为什么崩溃常出现在"执行 SQL 期间"而不是"打开数据库时"——打开主库成功,执行到一半要 journal 才发现写不进。

4、排查步骤

遇到 code 14,按这个顺序定位最快:

// 1. 确认数据库路径:是私有目录还是外存?
val path = context.getDatabasePath("app.db")?.absolutePath
// 正确做法应在:/data/data/<package>/databases/app.db

// 2. adb 进去检查目录是否存在、是否可写、父目录是否齐全
adb shell run-as <package> ls -l databases/
adb shell run-as <package> ls -ld databases      // 目录本身权限
// 重点看 .db-journal / .db-wal / .db-shm 是否能被创建

// 3. 检查磁盘空间
adb shell df /data

// 4. 确认是否多进程(AndroidManifest 里 activity/provider 是否 android:process)
// 多进程共库是 code 14 的高发场景

// 5. 看崩溃是否与"外存/sdcard/特定机型"强相关,缩小范围

一个判断口诀:"刚装上没事、用着用着才崩"多为 journal 目录问题;"特定机型/系统版本集中"多为权限或 SELinux 问题;"偶发且与并发相关"多进程冲突嫌疑大"。

5、解决方案一:回到私有目录

最根本、最稳的解法:把数据库放回应用私有目录。SQLiteOpenHelper 的默认行为就是这样,私有目录 `/data/data/<package>/databases/` 始终可写、不受外存权限波动影响:

// 推荐:用 SQLiteOpenHelper(或 Room),数据库自动建在私有目录
class DbHelper(context: Context) :
    SQLiteOpenHelper(context, "app.db", null, DB_VERSION) {
    // context 这里传 applicationContext,库名给纯名字,路径交给框架
}

// 如果曾经手动把库放到了外存,迁移回私有目录
val from = File(Environment.getExternalStorageDirectory(), "xxx/app.db")
val to   = context.getDatabasePath("app.db")     // 私有目录
to.parentFile?.mkdirs()                          // 确保目录存在
if (from.exists()) from.copyTo(to, overwrite = true)

// 切忌:不要为了"共享"把 db 放 sdcard,那是 code 14 的温床

绝大多数线上 case,迁回私有目录后立即消失。只有"必须跨应用共享数据库"时才考虑外存,而那也该用 ContentProvider 暴露,而不是裸放文件。

6、解决方案二:改临时目录 / WAL 模式

如果业务上确实要把数据库留在某个"主库可读但 journal 目录紧张"的位置,可以单独把临时文件目录指到一处稳定可写的地方,从根源上消除"journal 写不进":

// 方案 A:设置 SQLite 的临时文件目录到应用缓存目录(一定可写)
val tempDir = context.cacheDir.absolutePath + "/sqlite_temp"
File(tempDir).mkdirs()
db.execSQL("PRAGMA temp_store_directory = '$tempDir'")

// 方案 B:启用 WAL 模式(Android 较新版本很多库已默认开)
//   WAL 把回滚日志换成 -wal/-shm,对"主库目录可写"的要求依旧,
//   但并发读写更友好、崩溃风险更低
// SQLiteDatabase:启用 WAL(返回 Boolean)
db.enableWriteAheadLogging()
// 或 SQLiteOpenHelper(API 16+):
//   helper.setWriteAheadLoggingEnabled(true)

// 方案 C:临时数据纯内存(仅适合可丢失的中间结果)
db.execSQL("PRAGMA temp_store = MEMORY")   // 临时表/索引放内存,减少落盘

7、多进程并发场景

另一个高频诱因是多进程。AndroidManifest 里给组件加了 `android:process` 后,多个进程会各自创建 SQLiteOpenHelper、同时操作同一份 db 文件,journal 锁与文件并发极易触发 code 14 或锁定类错误:

// 错误:多进程各自 new 一个 SQLiteOpenHelper 操作同一文件
//   → journal 冲突、code 14、SQLITE_BUSY 频发

// 正确:把数据库访问收敛到一个进程,其他进程通过 ContentProvider 调用
//   (ContentProvider 默认运行在 :main 或指定进程,天然串行化 db 访问)
class DbProvider : ContentProvider() {
    override fun onCreate(): Boolean { /* 在此进程持有 SQLiteOpenHelper 单例 */ }
    // query/insert/update/delete 都走这一个 Helper
}

// 或使用 Room + WAL,并尽量减少跨进程写入;
// 切记:多进程下不要让"每个进程自己开数据库"

8、最佳实践与预防

       - 默认就把数据库放在私有目录(SQLiteOpenHelper / Room 的默认行为),不要乱改路径

       - 自定义路径时,务必先 mkdirs() 确保父目录存在,并确认可写

       - 多进程共库走 ContentProvider,别让每个进程各开一份

       - 关注磁盘空间,低存储设备上加容量监控与异常兜底

       - 升级用 Room/官方组件,少自己拼 SQLite,减少踩坑面

       - 线上捕获 SQLiteCantOpenDatabaseException 时,把"db 路径 + 可写性 + 剩余空间"一并上报,定位最快

9、总结

`SQLiteCantOpenDatabaseException: unable to open database file (code 14)` 在 Android 上几乎都不是"数据库文件坏了",而是SQLite 的临时文件(journal / WAL)写不进它该写的目录。根因常见于"数据库被放到外存/不可写路径""父目录缺失""磁盘满""多进程并发"。

关键要点:

       - code 14 = SQLITE_CANTOPEN,常指临时文件打不开而非主库

       - SQLite 事务依赖 journal/WAL 临时文件,默认建在 db 所在目录

       - 根治:数据库放私有目录;必要时改 temp_store_directory 或开 WAL

       - 多进程共库用 ContentProvider 收敛访问,避免 journal 锁冲突

       - 排查口诀:看路径、看权限、看空间、看进程

对于 Android 开发者而言,SQLite 这道"code 14"看似玄学,实则是"文件系统可写性"问题的集中爆发。把"数据库放私有目录 + 临时目录可写 + 多进程收敛访问"这三条做到位,绝大多数偶发崩溃都会销声匿迹。下次再撞见这个报错,别急着怀疑数据,先去查"它的临时文件想往哪儿写、写不写得进"——问题往往就在那一个目录上。

学习资料:

        SQLiteCantOpenDatabaseException unable to open db-journal · DBFlow #380

        SQLiteCantOpenDatabaseException: unable to open database file(CSDN)

        Sqlite在Android上的一个Bug —— 临时文件写策略(简书)

相关推荐

SQLite数据库浏览器:DB Browser for SQLite
数据库

SQLite数据库浏览器:DB Browser for SQLite

SQLite数据库浏览器:DB Browser for SQLite为了方便查看SQLite中存储的数据,需要一款好用的数据库查看工具。最好用的是DB Browser for SQLite。DB Browser for SQLite (DB4S) 是一款面向开发者的高质量的,可视化的开源的工具,它可以创建,设计,以及修改SQlite数据库。官网:http://www.sqlitebrowser.o

3.7k
MySQL:Access denied for user 'root'@'localhost'
数据库

MySQL:Access denied for user 'root'@'localhost'

Access denied for user 'root'@'localhost'这周双休闲的没事又开始折腾了。先是要把电脑折腾成Linux,遇到各种坑爹问题,未遂。接着又准备维护维护服务器,好久没有更新服务端。先更新一波:接着一看剩余内存,本来就是2G内存的“入门”级服务器,运行了小半年估计内存已经满了一看剩余内存还剩不到200M果断重启服务器,预料之中,果然都打不开了。博客站点、音乐API服务

2.2k
Android数据库调试助手:Glance
数据库

Android数据库调试助手:Glance

Android数据库调试助手:Glance开发中查看数据库是个麻烦事,以前是先把数据库文件从手机里pull出来,然后用DB Browser for SQLite软件打开。后来用过一个数据库调试工具:Android Debug Database。今天再介绍一个数据库调试工具库:Glance。此Glance非彼Glance,Jetpack中也有一个名为Glance的库,目的是用Compose样式的 A

2.1k