代码改变世界

Android SQLite总结

2017-02-13 17:33  soar.  阅读(3459)  评论(0编辑  收藏  举报

  SQLite在Android一般应用中还是比较常用,早期的时候碰到过不少坑,其中最烦的就是多线程并发读写问题,今天正好整理一下,做个笔记,也欢迎指正、讨论和补充。

  一、查询优化

  1、wal模式

  开启wal模式,可以实现并发读,且读写不阻塞,当然写与写之间仍然阻塞,该模式需要android3.0+才支持。

  当开启了wal模式更新数据时,会先将数据写入到*.db-wal文件中,而不是直接修改数据库文件,当执行checkpoint时或某个时间点才会将数据更新到数据库文件(执行endTransaction时会提交checkpoint)。当出现rollback也只是清除wal日志文件,而ROLLBACK JOURNAL模式,也就是关闭wal模式时,当数据有更新时,先将需要修改的数据备份到journal文件中,然后修改数据库文件,当发生rollback,从journal日志中取出数据,并修改数据库文件,然后清除journal日志。 从以上流程来看wal在数据更新上I/O量要小,所以写操作要快。由于在读取数据时也需要读取wal日志验证数据的正确性,所以读取数据相对要慢,但使用wal还是提高了读取的并发性。

  开启wal模式后,一定要使用beginTransactionNonExclusive来提交事务。db.beginTransaction()相当于execSQL("BEGIN EXCLUSIVE;"),在当前事务没有结束之前任何其他线程或进程都无法对数据库进行读写操作。当开启wal模式时,使用db.beginTransactionNonExclusive(),相当于execSQL("BEGIN IMMEDIATE;"),只会限制其他线程对数据库的写操作,不会阻塞读操作。

  2、建立索引,推荐看这个文章,足够了解索引的简单使用和优点了http://www.trinea.cn/android/database-performance/,总而言之,索引会增加SQLite体积,且增删改时也要维护索引,会对增删改性能存在一定影响,如果数据量不大,不建议使用。使用时一定要根据需求建立合适的索引,勿滥用。

  3、当某张表可预见数据量很大时,可以适当的进行表的细化、后期可以分表分库,查询时也可以使用异步查询。

 

  二、批量插入优化

  1、事务提交

  批量插入,包括更新删除,一定要加事务,如果不加事务,则默认会为每一次插入开启一个事务并自动提交,是非常慢的。

  2、开启wal模式,参见上文中解释;

  3、SQLiteStatement优化

  我们每次执行的sql语句最终会转化为一个SQLiteStatement对象来进行处理,可以预先使用db.compileStatement方法获取SQLiteStatement对象并重用,而不是让系统每次insert都构造一个对应的SQLiteStatement对象,这样能够提高内存的使用率。

  补充:网上有人解释“比如insert into xxx,一般情况下执行多少次,就要编译多少次”,关于这点,首先我认为不对,我阐述一下我自己的分析:SQLite想要执行操作,需要将程序中的sql语句进行“预编译”。例如批量插入,我们可以使用“显式预编译”来做到重用SQLiteStatement,也就是使用compileStatement方法。其实重点在于SQLiteStatement对象在new时,会通过SQLiteSession获取连接池中某个SQLiteConnection,通过调用SQLiteConnection的prepare方法,会从PreparedStatement链表中获取,如果没有可重用的则会创建一个PreparedStatement对象,其中会做一些native操作,例如给PreparedStatement的mStatementPtr赋值,通过注释,我们可以了解到这个mStatementPtr就是一个指向sqlite3_stmt类型的指针,而sqlite3_stmt是sqlite自己内部的数据结构,用来记录“sql语句”,这个sql语句是解析后的,也就是“预编译”后的。

  一句话,就是new SQLiteStatement会对sql做预编译,如果已经预编译过,会直接从缓存链表中拿。

  其实从上面分析,我们知道每个SQLiteConnection都包含一个链表结构的PreparedStatemnt对象集合,每次获取SQLiteConnection都会优先找到包含sql预编译的PreparedStatement实例的数据库连接,这样就不会每次都去预编译sql。所以除非这个connection刚好被其他线程拿去用了,否则都获取相同的connection,不用重复预编译。也就是说在已经执行过一次预编译(生成PreparedStatement实例)的SQLiteConnection中,不会再反复预编译,即使你inser into n次,而导致你需要重新预编译sql的情况是SQLiteConnection恰巧被其他线程使用,就会重新acquirePreparedStatement。

private PreparedStatement acquirePreparedStatement(String sql) {
    PreparedStatement statement = mPreparedStatementCache.get(sql);
    boolean skipCache = false;
    if (statement != null) {
        if (!statement.mInUse) {
            return statement;
        }
        // The statement is already in the cache but is in use (this statement appears
        // to be not only re-entrant but recursive!).  So prepare a new copy of the
        // statement but do not cache it.
        skipCache = true;
    }

    final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
    try {
        final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
        final int type = DatabaseUtils.getSqlStatementType(sql);
        final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
        statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
        if (!skipCache && isCacheable(type)) {
            mPreparedStatementCache.put(sql, statement);
            statement.mInCache = true;
        }
    } catch (RuntimeException ex) {
        // Finalize the statement if an exception occurred and we di

 

  4、分多个db来实现并发写;

 

  三、getReadableDatabase()和getWritableDatabase()

  getReadableDatabase()和getWritableDatabase()首先都会尝试以读写方式打开数据库。其中getReadableDatabase()如果因为磁盘空间已满等原因导致以读写方式打开数据库失败,会改以只读方式打开,而getWritableDatabase()会抛异常。若只需要一个只读的数据库,可以使用SQLiteDatabase.OPEN_READONLY标志,通过SQLiteDatabase#openDatabase(String, CursorFactory, int)方法手动打开。

  这两个方法成功返回后,会回调onOpen()方法,且OpenHelper会缓存该数据库实例。这两个方法调用时,如果因为数据库文件不存在需要创建会触发SQLiteOpenHelper#onCreate()回调,如果因为数据库版本不一致升或降会触发SQLiteOpenHelper#onUpgrade()、SQLiteOpenHelper#onDowngrade()回调。

 

  差不多就这些,欢迎大家补充和指正。最后是个人写的SQLite辅助管理类,维护一个SQLiteOpenHelper实例,并提供全局SQLiteDatabase实例打开和关闭,避免因为多线程操作或重复打开关闭导致的database is locked、reopen and already closed等异常,并且支持wal模式。因为比较简单,就不做单独讲解了。

 

public class SQLiteDbManager
{
    private SQLiteDbManager()
    {
    }

    private AtomicInteger mOpenCounter = new AtomicInteger();

    private static SQLiteDbManager mDatabaseHelper;
    private static SQLiteOpenHelper mSQLiteDbMaintain;
    private SQLiteDatabase mDatabase;

    private boolean mEnableWAL;

    public static void initializeInstance(SQLiteOpenHelper dbMaintain, boolean enableWAL)
    {
        if (mDatabaseHelper == null)
        {
            mDatabaseHelper = new SQLiteDbManager();
            mDatabaseHelper.mEnableWAL = enableWAL;

            mSQLiteDbMaintain = dbMaintain;
        }
    }

    public static SQLiteDbManager getInstance()
    {
        return mDatabaseHelper;
    }

    public synchronized SQLiteDatabase openDatabase()
    {
        if (mOpenCounter.incrementAndGet() == 1)
        {
            try
            {
                mDatabase = mSQLiteDbMaintain.getReadableDatabase();

                // 并发读
                if (mEnableWAL && Build.VERSION.SDK_INT >= 11)
                {
                    mDatabase.enableWriteAheadLogging();
                }
            }
            catch (SQLiteException ex)
            {
                mOpenCounter.decrementAndGet();
                mDatabase = null;
                Logger.getInstance().error(ex.toString());
            }
        }
        return mDatabase;
    }

    public void beginTransaction()
    {
        if (Build.VERSION.SDK_INT >= 11 && mEnableWAL)
        {
            mDatabase.beginTransactionNonExclusive();
            return;
        }

        mDatabase.beginTransaction();
    }

    public void beginTransactionWithListener(SQLiteTransactionListener listener)
    {
        if (Build.VERSION.SDK_INT >= 11 && mEnableWAL)
        {
            mDatabase.beginTransactionWithListenerNonExclusive(listener);
            return;
        }

        mDatabase.beginTransactionWithListener(listener);
    }

    public synchronized void closeDatabase()
    {
        if (mOpenCounter.decrementAndGet() == 0)
        {
            mDatabase.close();
        }
    }
}

 

参考链接:

http://www.trinea.cn/android/database-performance/

http://blog.csdn.net/efeics/article/details/18995433