@walldeskoo
2014-09-09T12:20:24.000000Z
字数 5406
阅读 1025
Beta 0.1(Wallace Zeng)
众所周知Android平台默认就支持了SQLite,只要引用SDK相关类就可以轻松使用。但是使用过程中有非常多的坑,为了造福苦逼的程序员们,于是有了这篇文章。
基本介绍:
Android平台上的SQLite是基于文件系统的轻量级数据库,其主要架构 (详见《The Definitive Guide to SQLite》), 需要关注的是Compiler和Backend两个模块。正因为有一个虚拟机的存在,所以才有了Compiled Statement的价值,因为它可以减少前置的编译时间,直接到VDBE上执行。而Backend端的Pager,则是重要数据管理者,真正决定者数据操作的性能,以及内存占用。
SQLite以Page为单位存储数据,默认一个Page有1024字节,然后通过B- Tree组织起来(Table使用B+ Tree组织):
Lookaside则是SQLite应用的内存管理的技术,优化了内存的使用效率,主要思想是先分配一整块内存, 分成若干个slots,然后SQLite再按需使用。这和许多小内存分配器的思想是一样的。再解释一下Page Cache Overflow, 主要是在一个Page中的记录的数据无法刚好放在一个Page内,还要使用额外的另一个Page空间, 这就是Overflow Page。
Android还有个万能dumpsys, 使用dumpsys meminfo可以查看到一个进程的SQLite使用的内存信息。如:
SQL
heap: 265 MEMORY_USED: 265
PAGECACHE_OVERFLOW: 73 MALLOC_SIZE: 46
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 60 17 199/114/1 webviewCache.db
1/541/1 (pooled # 1) webviewCache.db
使用SQLite的PRAGMA可以直接获取一些通过SQLiteDatabase获取不到信息,当然如果SQLite不支持,也会抛异常出来
最佳实践一:什么业务场景使用SQLite?
首先任何方案都有其适用场景,没有万能的最优,基于SQLite设计初衷,大部分手机应用的各种数据业务管理需求都是比较合适的。相对于Java环境的其他Model方案,一个非常突出的优势就是SQLite用的是Native的内存,大大缓解大数据的内存占用导致的虚拟机OOM问题。
但具体如下场景,不建议使用:
最佳实践二:数据库什么时候关闭?
相信大家使用过SQLite,一定都思考这个问题。系统文件的读写,我们一般都建议用的时候open,用完要及时close。但是对于SQLite,我的建议是,一段业务时间内,保持open状态便可,等进入相对长时间不使用数据库连接时才关闭。
原因有如下:
最佳实践三:是否使用系统的SQLiteOpenHelper?
我的建议是使用最新的源代码,而不是直接用sdk的api类,因为SQLiteOpenHelper有很多bug,尤其是使用低版本的SDK,运行高版本的系统时,在从高版本的数据库降级为低版本的数据库,会稳定崩溃。因为高版本的Helper封装了onDowngrade方法,如果数据库的文件版本比当前的运行版本高,就会抛出异常。Google设计的初衷是希望使用者重载这个onDowngrade,自己实现降级的逻辑。
另外,2.x,3.x,4.x 各个版本的实现都略有不同,兼容问题会直接让人崩溃。
最佳实践四:数据库升级策略
数据库升级是一个不得不重视的问题,越早处理得当,后期付出的代价就越少。那么数据库升级,有什么比较好的策略呢?
1 数据库在建表的时候,要预留一些空字段,方便未来扩展,避免频繁升级版本号
2 数据库修改的方案,不能删除旧有字段或者表信息,避免降级导致旧版本无法使用。
3 升级时,一级一级升级,避免中间版本过于复杂的升级逻辑问题。
最佳实践五:存储路径问题
默认存储路径是当前app数据路径的databases目录下,但如果数据量大,建议继承SQLiteOpenHelper,重载构造方法,使得数据库保存在SD目录下。另外需要检查一下SD目录的有效性,因为像小米手机在没有sd时,“sdcard”目录是不可用的,会导致很多异常(类构造阶段的异常)。
最佳实践六:如何处理4.0x系列系统的创建数据库时栈溢出问题?
4.0.x系统有个bug,在创建数据库时,假如发生数据异常,他会在异常的时候,再次调用创建方法,导致栈溢出。问题代码如下:
private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler, short connectionNum) {
SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum);
try {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.i(TAG, "opening the db : " + path);
}
// Open the database.
db.dbopen(path, flags);
db.setLocale(Locale.getDefault());
if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
db.enableSqlTracing(path, connectionNum);
}
if (SQLiteDebug.DEBUG_SQL_TIME) {
db.enableSqlProfiling(path, connectionNum);
}
return db;
} catch (SQLiteDatabaseCorruptException e) {
db.mErrorHandler.onCorruption(db);
return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler);
} catch (SQLiteException e) {
Log.e(TAG, "Failed to open the database. closing it.", e);
db.close();
throw e;
}
}
我遇到的问题是db.setLocale时抛出异常,所以解决的办法就是不让它抛出异常。我们通过创建时增加一个flag,避免这个问题。
private SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) {
// 4.0.x 使用特殊方式创建资源 Android 4.0.3, 4.0.4 15 ; 4.0, 4.0.1, 4.0.2 14
if (android.os.Build.VERSION.SDK_INT == 14 || android.os.Build.VERSION.SDK_INT == 15) {
return SQLiteDatabase.openDatabase(path, factory, SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
} else {
return mContext.openOrCreateDatabase(path, SQLiteDatabase.OPEN_READWRITE, factory);
}
}
最佳实践七:如何处理数据库的多线程访问?
在使用数据库的时候,我们很多时候会起一个新的线程来处理写操作,然后在主线程读数据。但是无论我们怎么优化,在一些低端机器上,总是出现写数据时间比较长的时候,甚至出现ANR。SQLite多线程的特性如下:
同一个SQLiteDatabase的多线程操作都会被锁住,直到上一个操作完成,才能执行下一个操作。
1 多个线程并发读必须是使用多个SQLiteDatabase对象。
2 多个线程并发写就不用想了,SQLite不支持,会抛出异常。
3 至于多个读,一个写的情况,默认是不支持的。需要调用api enableWriteAheadLogging(3.0以上支持)
而我的最佳实践就是,在SQLiteOpenHelper创建一个读,一个写的SQLiteDatabase对象。在调用getWritableDatabase或者getReadableDatabase时,直接创建两个对象(必须先创建Write对象,因为第一次调用,会调用onCreate等升级接口,需要写权限才行),然后再返回各自需要的。当然这样做,会产生一定的内存消耗以及查询的时候会变慢,因为数据库为了实现这个功能创建了两个文件,一个写,一个读,只有在空闲时才会合并两个文件。在合并前,查询会访问两个文件。
最佳实践八:是不是业务逻辑直接调用数据库获取数据?
很多初学者在使用数据库时,都会什么时候都直接访问数据库。我认为这样是不对的,不管数据库怎么优化, 都是一个相对复杂的系统,查询数据是比较耗时的。虽然我们不需要再把所有的数据都加载到了内存,但是缓存一些数据也是非常有必要的,能够大大加快速度,并且减轻卡顿现象。但是要注意及时清理,尤其是数据写到数据库后,要及时更新内存数据。
最佳实践九:多用事务?
是的,我非常建议大家多用事务。一方面是保证数据的原子性,使得一个业务数据得以完整的方式落地。另外一方面可以大大提升性能,尤其是插入效率。
最佳实践十:如何判断元素的数量,譬如表,或者表里的数据?
很多人会使用sql的查询返回的Cursor来获取,这里有一个更快的方式,就是select count,有几倍性能的提升,如下:
String sql = "select count(*) as c from " + CatalogTableElement.TABLE_NAME;
cursor = db.rawQuery(sql, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0);
}
最佳实践十一:是使用SQLiteDatabase的API(insert)还是直接直接用Sql语句实现功能?
我的建议是尽量用Sql语句,他们在灵活性和性能不是一个级别的。
最佳实践十二:使用索引?
查询性能的提升是我们使用数据库非常需要注意的,而很多时候字符串比较花了我们查询的语句的大部分执行时间,因此引入必要的索引提高速度是非常必要的。
如果没有在数据库使用索引,当你在一个没有排序的数据表中使用映射查询(projection query)搜索时, 无可避免的要执行一个全序列查找。这种情况通常并不是什么问题,每种数据库,包括SQLite都会为数据集执行索引来降低查找时间。
索引维护着一个表中某一列或某几列的顺序,这样就可以快速定位到一组值,而不用扫遍全表。所有的索引信息会被保存在一个独立的索引表中,所以会产生额外的空间占用,不过绝对物超所值,特别是当你会在数据库中进行大量的读及搜索操作时。
SQLite会自动为每一个UNIQUE栏位创建索引,包括主键(Primary Key)栏位,另外也可以通过CREATE
INDEX进行显示地创建
最佳实践十三:在Where分支中使用限定符?
如果以字串拼接出SQL语句的Where,还不如使用SQLite的query操作带上'?'来编译查询。以下是它的好处:
a. 有利于SQLite缓存这些查询及索引。
b. 可以避免达到SQLite缓存的上限。使用字串拼接Where的查询,每一个都被视为不同的查询,这就容易达到缓存的上限。
c. 可以避免非法的SQL注入。