Sqlite在Android上的一个Bug
SQLiteCantOpenDatabaseException: unable to open database file
先上log
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: com.company.product.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file (code 14)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:913)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:819)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:159)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:147)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.AbstractCursor.moveToPosition(AbstractCursor.java:218)
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.AbstractCursor.moveToFirst(AbstractCursor.java:258)先给出结论,
这是sqlite在Android系统上的一个bug,在需要建立索引的sql语句频繁执行时,会发生这个异常。
(如果你是在SQLiteDatabase执行open()时看到的这个exception,那应该是线程冲突的问题,跟这篇文章讲的不是同一个)
根本原因是sqlite临时文件目录不可用。
解决方案是第一次建立连接时设置临时文件目录。
在项目里遇到了这样一个奇怪的crash,长期占据各个版本crash上报榜首,但在开发中一直不能重现。
在许多查DB的代码路径里,都会在moveToFirst(),getCount()等需要执行fillWindow的地方出现这个crash。
网络上的解决方案:
谷歌搜索SQLiteCantOpenDatabaseException,多是一些执行SQLiteDatabase open()时线程冲突的问题,与我们这个问题不同。
跟这个问题相关的回答屈指可数,一直没找到解决方案,最相关的两种回答来自github:
https://github.com/Raizlabs/DBFlow/issues/380
https://github.com/dxopt/OpenFilesLeakTest/blob/master/bugs-show/AbstractCursor.moveToFirst.md
第一个链接与我们的情况相符,但是没有根本的解决方案,只有try – catch
第二个链接讲的是FD泄露导致打不开文件,于是我排查了app中各种泄露的地方,并且写了一个计算文件句柄数的上报工具,发现用户发生此类crash时,FD都不超过256,低于系统对单个进程默认FD数量1024的限制。排除这个可能。
(但有些时候也有可能是由这个问题引发的,可以用StrictMode detectLeak去排查)
于是先尝试在一些可能触发这个Exception的地方try-catch
再分析用户日志,发现try – catch住这个Exception后是可以继续执行一些DB查询的,
于是全都上了try – catch
重现路径
分析用户日志,发现用户的一些共性,由于业务保密限制这里总结一下,共性是DB中数据量很大,并且查询中有大量的子查询。
于是尝试重现这个问题:
在数据量很大的情况下,多次查询就会重现。
可以重现的话就可以开始打log了。
为了在sqlite native层打log,编译sqlite,使用sqlite3_log来输出自己想观察的信息。
首先我们可以看到sqlite的log
可以看到是打开一个”./etilqs_3P2SKRP0Ge6cj3T”的文件时打开失败。
先查查这个临时文件是什么鬼,
在sqlite3.c搜索前缀etilqs_里可以看到这样的注释:
总之就是临时文件就对了。
临时文件源码追踪
然后找找这个东西在哪里用的,
这里可以留意到一个神奇的东西
zDir = unixTempFileDir();
if( zDir==0 ) zDir = "."; 我们的文件是 ./etilqs_3P2SKRP0Ge6cj3T
所以unixTempFileDir()确实是返回了0
那再看下unixTempFileDir();
azDirs[0]是sqlite3_temp_directory,我们没有设置过,
azDirs[1]和[2]是环境变量,用sqlite3_log打出来是
即环境变量里没有设置这两个值,
而另外三个目录/var/tmp,/usr/tmp,/tmp在Android系统里都是应用不可写的,
所以会返回0给unixGetTemp,
于是unixGetTemp使用了”.”作为临时文件的目录,
那”.”是哪个目录呢?
使用
结果是:
这特么是根目录!当前工作目录是根目录我也是醉了。。。
所以在根目录创建临时文件一定会失败!
etilqs临时文件创建时机
那为什么平时使用都是正常的呢?
找一找这个临时文件的创建时机:
在unixGetTempname函数里,人为地造一个crash,通过crash堆栈配合addr2line来查看调用栈:
使用addr2line –C –f –e 加上面14个pc地址,结果:

懒得看图的童鞋还是听我说吧,
先看sqlite的architecture

因为我们crash的地方是查DB的地方,所以拿query操作来解释这个architecture是怎么运行的
先用SQL Command Processor解析sql语句,变成类似汇编的命令给Virtual Machine执行,
我们可以用explain plan select …. 这样的语句来查看virtual machine要执行的命令,比如
对应的命令是:
可以看到其中需要建立索引,IdxInsert,于是在sqlite3VdbeExec中会进入
OP_IdxInsert分支,然后
会调用sqlite3BtreeInsert,向B树中插入一个节点,
此时如果pPage满了,会执行balance平衡B树,
在这里面就会btreeGetPage去获取可用的page,
获取page的过程最终会执行sqlite3_malloc,为page分配空间,一旦分配失败,就会在fetch处触发pBase == 0的条件,
于是执行sqlite3PcacheFetchStress,在其中调用pager_write_pagelist时触发pPager->fd == 0的条件(因为page在前面没有分配到空间),
于是触发pagerOpenTemp,往下执行调用unixGetTempname,得到上面所说的那个不正确的文件路径,
执行sqlite3Osopen时就会失败。
从上面的分析看出,触发这个路径需要几个条件:
执行的sql语句需要建立索引,
B树不平衡
没有设置过环境变量
分配的内存不足以新建新的page
所以触发条件还是比较严格的。
在unixOpenTempname执行时用一个变量计算临时文件的打开次数,也可以发现确实是一打开这样的文件就会失败(在打开第一个的时候就失败)。
解决方案(Solution)
那么最重要的事情来了,怎么修复呢?
既然是临时文件的目录没有写权限,那就改目录吧!
翻了翻sqlite的一些资料,找到了这样一个programa
http://www.sqlite.org/c3ref/temp_directory.html
这个东西仅对当前SqliteConncetion有效,
在第一次建立sqlite连接的时候(我是重写了getReadabelDatabase()方法),设置一下临时文件目录,like this:
然后再去执行那些繁重的查询,你会发现问题消失了,
并且sqlite3会在不需要这个临时文件时自动删除它,所以你不需要做一套清理逻辑。
于是问题解决!
最后更新于
这有帮助吗?