场景
Jni下编译SQLite源码作为数据库,在测试手机:型号(Redmi Note 2) Android版本(5.0.2 LRX22G)系统下使用,尝试写数据库的时候,返回错误信息:attempt to write a readonly database
原因
SQLite源码定义的存储节点编号数据类型由于类型长度不足以描述真实的存储节点数据类型,导致在对比存储节点长度的时候,出现截断的问题,从而引发只读数据库问题
解决
在sqlite.c文件中查找
ino_t ino; /* Inode number */
修改为
unsigned long long ino; /* Inode number */
错误说明
Store inodes in unsigned long long In 32 bit ABIs, ino_t is a 32 bit type, while the st_ino field in struct stat is 64 bits wide in both 32 and 64 bit processes. This means that struct stat can expose inode numbers that are truncated when stored in an ino_t. The SDCard fuse daemon (/system/bin/sdcard) uses raw pointer values as inode numbers, so on 64 bit devices, we're very likely to observe inodes that need > 32 bits to represent. The fileHasMoved function in sqlite compares the stored inode value with a new one from stat, and when the stored value is truncated, this check will falsely indicate that the file has been moved. When the fileHasMoved function triggers, other functions start returning errors indicating that the database is in read-only mode. NOTE: Bionic differs from glibc in that struct stat's st_ino is *always* 64 bits wide, and not the same width as ino_t.
32位应用程序二进制接口使用unsigned long long存储磁盘节点,ino_t是32位比特数据类型,在结构体stat字段st_ino在32位还是64位处理器都是64比特长度。这意味着结构体stat能够完整描述节点编号,但是ino_t存储节点由于长度问题,会导致截断的情况。SDCard守护进程(/system/bin/sdcard)使用原始指针值作为节点编号,所以在64比特设备,我们需要大于32比特的数据类型描述节点sqlite源码中的fileHasMoved函数对比从stat获取的值跟存储的节点值,如果存储的节点值被截断,检测就会失败,返回的值说明文件已经被移除。当fileHasMoved函数触发的时候,其他函数开始返回错误,提示数据库处于只读状态。
注意:Bionic库不同于glibc库,结构体stat中st_ino字段是64比特长度跟ino_t不是同一个长度类型
参考
http://www.jianshu.com/p/30139ef31230
解决过程:
1 怀疑是读写权限的问题,但是其他文件也有读写,明显不成立,并且已经提供了读写的权限:
<uses-permission android:name="android.permission.INTERNET" />
2 怀疑是使用MTP模式共享机身存储到电脑,导致两边同时编辑,通过关闭MTP媒体共享,不成立
Redmi Note 2手机请勿关闭MTP媒体设备,否则需要恢复出厂设置才能够重新共享到电脑。
3 拷贝test.db和test.db-journal文件到Windows系统,并且使用SQLite控制台进行修改,没有任何的
问题。当前使用的是相同的SQLite源码编译的控制台程序。进行数据的插入过程中,会自动删除journal归档文件,并没有提示只读。另外单独拷贝test.db,直接操作,也没有任何的问题。
4 尝试关闭Java层对数据库的读写访问,只是允许NDK层直接操作数据库,防止多线程访问数据库,还是出现同样的结果,当然不知道是否是sqlite3_open与sqlite3_open_v2的接口是否会产生不同的结果,目前采用第一种方法访问数据库。
5 尝试获取Android系统中SQLite的版本,调用getVersion返回1,没有实质上的帮助。
6 是否是其他的Java层获取数据库的连接,没有关闭导致问题的出现?SQliteOpenHelper内部只缓存一个数据库的连接,在多线程的使用过程中,不要频繁的调用close,而应该保存一个唯一的一个访问实例,但是对于多进程之间的访问,也带来问题http://blog.sina.com.cn/s/blog_5de73d0b0102w0g0.html
7 虽然NDK无法修改数据,但是可以读取数据,而Java层却可以轻松的完成数据库的修改操作。
8 尝试调用beginTransaction和endTransaction对数据库的操作进行事务加锁还是没有任何的效果
9 怀疑是Android目录的读取问题,NDK层的SQLite读取数据库文件,发现有问题,但是没有办法找到journal归档文件,因此认为是只读,也是合理的
总结
经过上述种种的研究分析,得到如下的猜测:NDK层的SQLite实例在系统开机启动之后,会检查是否存在journal档案。如果是通过程序自动删除该journal档案。但是如果程序无法自动删除,目前怀疑就是因为版本之间的不对称,导致无法删除journal,SQLite数据库因此数据库变成只读的状态。
折中方案
NDK层只是负责只读数据,如果有任何的修改,都需要调用Java的接口进行修改数据,虽然有SQLite的源码,但是NDK层不好调试。