[关闭]
@walldeskoo 2014-09-09T12:20:24.000000Z 字数 5406 阅读 1025

Android SQLite 最佳实践

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问题。

但具体如下场景,不建议使用:

  1. 由于SQLite是基于文件的数据库方案,所以跨进程的并发数据访问不建议使用。
  2. 对性能,内存有特定要求的场景不合适,毕竟数据库启动就要花费不少时间和内存。尤其类似于崩溃标志等轻量级数据的高速读写,不建议使用数据库。
  3. 类似图片类的二进制数据,不建议存到数据库。可以通过存入其本地路径,在需要的时候再获取。

最佳实践二:数据库什么时候关闭?
相信大家使用过SQLite,一定都思考这个问题。系统文件的读写,我们一般都建议用的时候open,用完要及时close。但是对于SQLite,我的建议是,一段业务时间内,保持open状态便可,等进入相对长时间不使用数据库连接时才关闭。

原因有如下:

  1. 数据库的open和close都需要消耗比较多的时间
  2. 快速的调用open和close,在一些rom上,会出现一些诡异异常,就是open返回的对象是上一次close的对象,导致单次读写失败。大家可以尝试自己写测试代码,for循环快速open和close,把open到的SQLiteDatabase对象打印出来。
  3. 当然保持连接需要消耗一定,所以要在合适的时间关闭,譬如切到后台,或者退出业务的功能等。

最佳实践三:是否使用系统的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注入。


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注