技术原理Android 高质量开发 — 存储优化

Android 存储选项之 SQLiteDatabase 创建过

2019-10-14  本文已影响0人  godliness
闪存
Android 存储优化系列专题

Android 之不要滥用 SharedPreferences
Android 之不要滥用 SharedPreferences(2)— 数据丢失

《Android 存储选项之 ContentProvider 启动过程源码分析》
《Android 存储选项之 ContentProvider 深入分析》

Android 对象序列化之你不知道的 Serializable
Android 对象序列化之 Parcelable 深入分析
Android 对象序列化之追求完美的 Serial

《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》

Android 存储选项之 SQLiteDatabase 创建过程源码分析
Android 存储选项之 SQLiteDatabase 源码分析
数据库连接池 SQLiteConnectionPool 源码分析
SQLiteDatabase 启用事务源码分析
SQLite 数据库 WAL 模式工作原理简介
SQLite 数据库锁机制与事务简介
SQLite 数据库优化那些事儿


存储优化系列专题中,先后为大家介绍了 SharedPreferences、ContentProvider、对象/数据序列化等存储和优化方法。先回顾下前面介绍的存储方法的使用场景,少量的 key:value 数据可以直接使用 SharedPreferences,稍微复杂一些的数据类型也可以通过序列化成 JSON 或者 Protocol Buffers 保存,并且在开发中获取或者修改数据也很简单。

不过这几种方法并不能覆盖所有的存储场景,数据量在几百上千条这个量级时它们的性能还可以接受,但如果是几万条的数据记录时,而且如何实现快速地对某几条数据做增删改查呢?

对于大数据的存储场景,我们需要考虑稳定性、性能和可扩展性,这个时候就轮到该篇文章的“主角”数据库登场了。

SQLite 简介

虽然市面上有很多的数据库,但受限于库体积和存储空间,适合移动端使用的还真不多。当然最广泛的还是我们今天要介绍的 SQLite。但同样还是有一些其它不错的选择,例如创业团队的 Realm、Google 的 LevelDB 等。

SQLite 是一款完全由 C 语言开发的开源、嵌入式关系型数据库,最早发布于 2000 年,它没有独立运行进程,它与所服务的应用程序在应用程序进程空间内共生共存。它的代码与应用程序代码也是在一起的,或者说嵌入其中,作为托管它的程序的一部分。

数据库这个主题内容非常多,而且相信有很多小伙伴学习数据库都是从入门到放弃,本人也是从最开始懵懂到不断的摸索探究,总算是对它有了进一步的认识。

在 Android 平台,系统已经内置了 SQLite 数据库作为数据持久存储的一种方案。Android 为 SQLite 数据库提供的第一个也是最重要的类就是 SQLiteOpenHelper,该类可以供开发人员扩展实现对数据库的创建、打开或使用数据库的重要任务和行为。

该篇文章主要是通过源码角度分析 Android 平台提供的 SQLiteDatabase 的创建过程源码分析。有关 SQLiteDatabase 更详细的内容请参考后续文章《Android 存储选项之 SQLiteDatabase 源码分析》和《Android 存储选项之 SQLite 优化那些事儿》。

1. SQLiteOpenHelper

在 Android 中如果要使用 SQLite 数据库,那 SQLiteOpenHelper 可能就是我们第一个接触的类,从它的命名其实我们也可以看出它属于数据库操作的辅助类,实际上它内部也是围绕 SQLiteDatabase 完成一系列任务和行为。

下面就从一个例子开始分析 SQLiteDatabase 的创建过程:

public final class SampleDBHelper extends SQLiteOpenHelper{

    public SampleHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建数据库表
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //升级
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //降级
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
        //数据库被打开
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        //配置相关
    }
}

在 SampleDBHelper 示例中,我们重写 SQLiteOpenHelper 的相关方法,其中 onCreate 和 onUpgrade 是必须要实现的;其余方法则是在数据库满足相关状态时触发。那它们的调用时机分别是怎么样的呢?

先看下 SQLiteOpenHelper 的构造方法:

public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,
            @Nullable CursorFactory factory, int version) {
    //context 上下文
    //name 数据库名称
    //factory 是Cursor工厂,用于自定义指定Cursor
    //version 当前版本,决定了onCreate、onUpgrade、onDowngrade的调用时机
    //errorHandler 用于指定发生异常时回调
    this(context, name, factory, version, null);
}

Cursor 是完成对数据查询后的数据获取接口封装。

另外 SQLiteOpenHelper 中还提供 close 关闭 SQLiteOpenHelper 中的 SQLite 数据库。数据库被关闭后,任何对数据库的操作都是不允许的。
getReadableDatabase 和 getWriteableDatabase 方法行为相似,从它们的名字也可以看出,它们只有一点不同 getReadableDatabase 以只读方式打开 SQLiteOpenHelper 对象中指定的数据库,也就说任何想要修改数据库的行为都是不允许的。getWriteableDatabase 也是打开数据库,但它允许数据库正常的读/写操作。

如果因为某种原因,数据库不能进行写操作,那么 getWriteableDatabase 将以只读方式打开数据库,并抛出一个类型为 SQLiteException 的异常。可以利用 isReadOnly 方法测试数据库是否可写。然后可以使用 getWriteableDatabase 方法重新打开一个只读的数据库,使它能够正常读写。

2. SQLiteDatabase

SQLiteOpenHelper 是在 Android 平台处理 SQLite 数据库相关的第一个类,那接下来说的 SQLiteDatabase 则是最关键的类了。SQLiteDatabase 在概念上很容易理解,与 SQLite C API 的底层数据库对象相似,实际可能比想象的复杂的多。

但实际上 SQLiteDatabase 内部的每一个方法都有自己的用途,而且大部分方法都是为完成一个简单的数据库任务,比如表的选择、插入、更新和删除语句。这里只是列举一些重要的方法,不过这部分内容并不是我们今天要讨论的,你可以参考下一篇《Android 存储选项之 SQLiteDatabase 源码分析》一文。

通过 SQLiteOpenHelper 的 getReadableDatabase / getWriteableDatabase 方法分别获得只读 / 可读写的 SQLiteDatabase 实例。

//获得一个只读的数据库
public SQLiteDatabase getReadableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(false);
    }
}

//获得一个可读写的数据库
public SQLiteDatabase getWritableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(true);
    }
}

它们最终调用 getDatabaseLocked 方法,SQLiteOpenHelper 中默认会对应一个 SQLiteDatabase 实例:

private SQLiteDatabase getDatabaseLocked(boolean writable) {
    //SQLiteDatabase作为SQLiteOpenHelper的成员
    if (mDatabase != null) {
        //SQLiteOpenHelper中默认保存当前SQLiteDatabase实例
        if (!mDatabase.isOpen()) {
            //如果数据库已经关闭,需要重新打开
            mDatabase = null;
        } else if (!writable || !mDatabase.isReadOnly()) {
            //如果数据库没有关闭
            //只要是该数据库是可写则直接返回
            //否则当前申请是只读则直接返回
            return mDatabase;
        }
    }

    if (mIsInitializing) {
        //避免重复的初始化阶段
        throw new IllegalStateException("getDatabase called recursively");
    }

    SQLiteDatabase db = mDatabase;
    try {
        mIsInitializing = true;

        if (db != null) {
            if (writable && db.isReadOnly()) {
                //但是当前数据是只读状态,需要获取可写的数据库
                //此时需要根据配置选项SQLiteDatabaseConfiguration重新打开数据库
                db.reopenReadWrite();
            }
        } else if (mName == null) {
            //数据库名称为null,为Memory Db
            db = SQLiteDatabase.createInMemory(mOpenParamsBuilder.build());
        } else {
            //获取数据库保存路径
            final File filePath = mContext.getDatabasePath(mName);
            SQLiteDatabase.OpenParams params = mOpenParamsBuilder.build();
            try {
                //创建数据库操作SQLiteDatabase实例
                db = SQLiteDatabase.openDatabase(filePath, params);
                // Keep pre-O-MR1 behavior by resetting file permissions to 660
                setFilePermissionsForDb(filePath.getPath());
            } catch (SQLException ex) {
                if (writable) {
                    throw ex;
                }
                Log.e(TAG, "Couldn't open " + mName
                        + " for writing (will try read-only):", ex);
                params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build();
                db = SQLiteDatabase.openDatabase(filePath, params);
            }
        }
        //onConfigure回调,配置数据库相关
        onConfigure(db);
        //获取当前数据库版本,默认为0
        final int version = db.getVersion();
        //mNewVersion是自行设置的版本号,一般默认第一个版本为1
        //如果首次创建传递0,此时将不再执行
        if (version != mNewVersion) {
            if (db.isReadOnly()) {
                //此时如果是只读状态,将无法对数据库做任何更改
                throw new SQLiteException("Can't upgrade read-only database from version " +
                        db.getVersion() + " to " + mNewVersion + ": " + mName);
            }

            //例如当前数据库version==3,而mMinimumSupportedVersion==5,此时
            //该数据不再不支持,将其删除
            if (version > 0 && version < mMinimumSupportedVersion) {
                File databaseFile = new File(db.getPath());
                //这个被@hide了
                onBeforeDelete(db);
                db.close();
                if (SQLiteDatabase.deleteDatabase(databaseFile)) {
                    mIsInitializing = false;
                    //删除成功重新创建数据库
                    return getDatabaseLocked(writable);
                } else {
                    //删除失败要抛出异常
                    throw new IllegalStateException("Unable to delete obsolete database "
                            + mName + " with version " + version);
                }
            } else {
                //开启事务
                db.beginTransaction();
                try {
                    //数据库第一次创建执行onCreate
                    //默认数据库第一次创建时version==0
                    if (version == 0) {
                        onCreate(db);
                    } else {
                        if (version > mNewVersion) {
                            //降级
                            onDowngrade(db, version, mNewVersion);
                        } else {
                            //升级
                            onUpgrade(db, version, mNewVersion);
                        }
                    }
                    //设置当前最后版本
                    db.setVersion(mNewVersion);
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            }
        }

        //onOpen被回调
        onOpen(db);

        if (db.isReadOnly()) {
            Log.w(TAG, "Opened " + mName + " in read-only mode");
        }
        //赋值给当前成员
        mDatabase = db;
        return db;
    } finally {
        mIsInitializing = false;
        if (db != null && db != mDatabase) {
            db.close();
        }
    }

快速浏览下该方法,是否发现前面在 SampleDBHelper 示例中重写的相关方法都会在这里体现出来。下面就具体分析下它们的执行过程。

首先方法的开始部分,SQLiteOpenHelper 中会持有 SQLiteDatabase 实例,根据当前打开数据库模式是否符合操作需要,则直接返回。

如果数据库已经打开 Read-Only(只读) 状态,此时 reopenReadWrite 方法更新成可写的。

如果数据库的名称 name == null,此时 SQLiteDatabase.createInMemory 方法创建一个 in-memory 类型 Database。

真正创建数据库操作是 SQLiteDatabase.openDatabase 方法,这里将 SQLiteDatabase.OpenParams 作为参数,先简单看下它的构造方法,其内部保存了数据库创建的相关配置项。

private OpenParams(int openFlags, CursorFactory cursorFactory,
            DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount,
            long idleConnectionTimeout, String journalMode, String syncMode) {
        //打开模式
        mOpenFlags = openFlags;
        //Cursor工厂
        mCursorFactory = cursorFactory;
        //DatabaseErrorHandler
        mErrorHandler = errorHandler;
        mLookasideSlotSize = lookasideSlotSize;
        mLookasideSlotCount = lookasideSlotCount;
        //空闲连接超时时间
        mIdleConnectionTimeout = idleConnectionTimeout;
        //日志模式,3.12.0之后为WAL模式
        mJournalMode = journalMode;
        //同步模式,提交数据之后采取的同步会数据库文件的策略
        mSyncMode = syncMode;
    }

其实 OpenParams 主要是保存了当前数据库 SQLiteDatabase 打开时相关配置选项,这在后面内容会验证到该部分。

继续向下分析,我们看下在上面示例中重写的相关方法的回调过程:

创建数据库 SQLiteDatabase 成功(SQLiteDatabase.openDatabase 方法正确返回),此时 onConfigure 方法被回调,它的原型是一个空方法:

public void onConfigure(SQLiteDatabase db) {}

在该方法我们可以对数据库做相关配置操作,例如开启 WAL,设置 PageSize 等信息。

如果数据库第一次创建,此时默认版本号为 0( db.getVersion() ),而我们实现 SQLiteOpenHelper 后默认第一个版本一般为 1。此时 onCreate 方法被回调。

另外源码中也可以看出,如果当前版本号小于最小支持的版本 mMinimumSupportedVersion,此时要执行删除数据库任务。

//删除数据库文件
SQLiteDatabase.deleteDatabase(databaseFile)

如果数据库被设置过版本号,并且大于当前传递版本,表示降级操作,此时 onDowngrade 方法被回调:

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

注意,当出现数据库降级时,该方法必须被实现,否则默认抛出异常。

与 onDowngrade 方法正好相反,表示当前升级数据库操作。最后数据库会保存当前最后一次版本号。

以上执行过程全部结束后,onOpen 方法被回调表示数据库已经打开。

大家是否有注意到,整个数据表创建、升/降级操作过程,系统默认为我们开启了事务,以提高执行效率。

事务使用的标准格式一般如下:

...省略

db.beginTransaction();

try{

    // do something ...
    
   db.setTrainsactionSuccessful();
}finally{
    db.endTrainsaction();
}

另外还有一些其它需要说明的地方

我们可以直接使用 SQLiteDatabase.createDatabase 单独指定数据库创建位置,但默认使用 SQLiteOpenHelper 时如下:

 //获取数据库保存路径
 final File filePath = mContext.getDatabasePath(mName);

这里实际调用到 ContextImpl 的 getDatabasePath 方法

@Override
public File getDatabasePath(String name) {
    File dir;
    File f;

    if (name.charAt(0) == File.separatorChar) {
        //如果name自定义存储路径:/storage/emulated/0/database/test.db
        String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar));
        //自定义路径:/storage/emulated/0/database
        dir = new File(dirPath);
        //数据库名称:test.db
        name = name.substring(name.lastIndexOf(File.separatorChar));
        f = new File(dir, name);

        if (!dir.isDirectory() && dir.mkdir()) {
            FileUtils.setPermissions(dir.getPath(),
                FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
                -1, -1);
        }
    } else {
        //否则默认/data/data/packageName/databases/test.db
        dir = getDatabasesDir();
        f = makeFilename(dir, name);
    }

    return f;
}

系统默认数据库保存路径为 /data/data/packeageName/database/name.db;但是我们也可以指定自定义数据库文件路径。这样做的唯一好处是不会随着应用数据清除或应用删除后数据库文件也被删除。

  1. SQLiteDatabase 创建

通过上面的分析,也证实了 SQLiteOpenHelper 的命名的含义,它的主要工作就是完成对 SQLiteDatabase 的创建、关闭、升/降级、配置等相关辅助管理。

接下来我们从 SQLiteDatabase.openDatabase 方法开始真正分析 SQLiteDatabase 的创建过程。

//创建数据库操作SQLiteDatabase实例
db = SQLiteDatabase.openDatabase(filePath, params);

//最终会调用如下
private static SQLiteDatabase openDatabase(@NonNull String path,
        @NonNull OpenParams openParams) {
    Preconditions.checkArgument(openParams != null, "OpenParams cannot be null");
    //创建SQLiteDatabase实例
    //前面提到OpenParams此时作为参数
    //将OpenParams配置参数传入SQLiteDatabase
    SQLiteDatabase db = new SQLiteDatabase(
            path, 
            openParams.mOpenFlags,
            openParams.mCursorFactory, 
            openParams.mErrorHandler,
            openParams.mLookasideSlotSize, 
            openParams.mLookasideSlotCount,
            openParams.mIdleConnectionTimeout,
            openParams.mJournalMode, 
            openParams.mSyncMode);
    //调用SQLiteDatabase的open
    db.open(); 
    return db;
}

直接创建 SQLiteDatabase 实例并调用它的 open 方法。SQLiteDatabase 构造方法如下:

private SQLiteDatabase(final String path, final int openFlags,
        CursorFactory cursorFactory, DatabaseErrorHandler errorHandler,
        int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs,
        String journalMode, String syncMode) {
    mCursorFactory = cursorFactory;
    mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
    //SQLiteDatabase的创建模式,这个对于数据库的并发访问非常重要
    //主要影响数据库连接池打开模式
    mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
    mConfigurationLocked.lookasideSlotSize = lookasideSlotSize;
    mConfigurationLocked.lookasideSlotCount = lookasideSlotCount;
    // Disable lookaside allocator on low-RAM devices
    if (ActivityManager.isLowRamDeviceStatic()) {
        //如果是低内存设备,系统好像规定低于512MB为低内存设备
        //不过发展到现在就算是2G都算是小内存了
        mConfigurationLocked.lookasideSlotCount = 0;
        mConfigurationLocked.lookasideSlotSize = 0;
    }
    long effectiveTimeoutMs = Long.MAX_VALUE;
    // Never close idle connections for in-memory databases
    if (!mConfigurationLocked.isInMemoryDb()) {
        //不是内存级数据库,如果name传递null,此时为inMemory
        if (idleConnectionTimeoutMs >= 0) {
            //空闲连接超时时间后关闭
            effectiveTimeoutMs = idleConnectionTimeoutMs;
        } else if (DEBUG_CLOSE_IDLE_CONNECTIONS) {
            //DEBUG_CLOSE_IDLE_CONNECTIONS 默认为false,表示空闲连接不会被关闭
            effectiveTimeoutMs = SQLiteGlobal.getIdleConnectionTimeout();
        }
    }
    //配置到SQLiteDatabaseConfiguration中
    mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs;
    //日志模式
    mConfigurationLocked.journalMode = journalMode;
    //同步模式
    mConfigurationLocked.syncMode = syncMode;

    if (!SQLiteGlobal.isCompatibilityWalSupported() || (
            SQLiteCompatibilityWalFlags.areFlagsSet() && !SQLiteCompatibilityWalFlags
                    .isCompatibilityWalSupported())) {
        //不支持兼容性WAL
        //则禁用掉兼容性WAL
        mConfigurationLocked.openFlags |= DISABLE_COMPATIBILITY_WAL;
    }
}

SQLiteDatabase 构造方法内容有点多,但是内容却比较单一,就是将前面保存相关创建配置的 OpenParams 信息保存到 SQLiteDatabaseConfiguartion 中,在后续创建数据库连接池和开启 WAL 模式等相关内容时都会到该配置信息。

SQLiteDatabase 的 open 方法如下:

private void open() {
    try {
        try {
            //调用onpenInner
            openInner();
        } catch (SQLiteDatabaseCorruptException ex) {
            //这里回调DatabaseErrorHandler
            onCorruption();
            openInner();
        }
    } catch (SQLiteException ex) {
        Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
        close();
        throw ex;
    }
}

//这里直接调用了 openInner 方法
private void openInner() {
    //mLock是ConnectionPoolLocked
    synchronized (mLock) {
        assert mConnectionPoolLocked == null;
        //该mConfigurationLocked就是在构造方法中创建的SQLiteDatabaseConfiguartion
        //打开数据库连接池,ConfigurationLocked作为参数
        mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
        mCloseGuardLocked.open("close");
    }

    synchronized (sActiveDatabases) {
        //缓存当前SQLiteDatabase实例
        //sActiveDatabases是WeakHashMap
        sActiveDatabases.put(this, null);
    }
}

openInner 中开始创建当前 SQLiteDatabase 的数据库连接池 SQLiteConnectionPool,并将 SQLiteDatabaseConfiguration 作为参数:

public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) {
    if (configuration == null) {
        throw new IllegalArgumentException("configuration must not be null.");
    }

    //创建数据库连接池
    SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration);
    //调用open 可能抛出异常
    pool.open();
    return pool;
}

一样的调用套路,直接创建 SQLiteConnecitonPool,并调用其 open 方法:

SQLiteConnectionPool 的构造方法如下:

private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
    //copyOnWrite模式吧
    mConfiguration = new SQLiteDatabaseConfiguration(configuration);
    //设置连接池的大小
    setMaxConnectionPoolSizeLocked();
    // If timeout is set, setup idle connection handler
    // In case of MAX_VALUE - idle connections are never closed
    //在Long.MAX_VALUE下永远不会关闭连接
    if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
        setupIdleConnectionHandler(Looper.getMainLooper(),
                mConfiguration.idleConnectionTimeoutMs);
    }
}

将数据库配置 configuration 保存,不过这里并不是直接赋值,而是数据 copy。

设置数据库连接池大小:

//Android提供的SQLiteDatabase开启连接池与WAL模式是一回事
private void setMaxConnectionPoolSizeLocked() {
    if (!mConfiguration.isInMemoryDb()
            && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) {
        //如果不是memoryDb,与打开模式是ENABLE_WRITE_AHEAD_LOGGING
        //在这里其实可以注意到,SQLiteDatabase并没有给我们提供设置连接池大小的API,
        //这里仍然取决于系统的配置
        mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize();
    } else {
        // We don't actually need to always restrict the connection pool size to 1
        // for non-WAL databases.  There might be reasons to use connection pooling
        // with other journal modes. However, we should always keep pool size of 1 for in-memory
        // databases since every :memory: db is separate from another.
        // For now, enabling connection pooling and using WAL are the same thing in the API.
        mMaxConnectionPoolSize = 1;
    }
}

这里不知道是否还记得在 SQLiteOpenHelper 的构造方法,如果传入的数据库名称 name 为 null,此时就会满足 isMemoryDb()。

openFlags & ENABLE_WRITE_AHEAD_LOGGING != 0

表示开启 WAL 预写模式。关于 WAL 模式工作原理你可以参考这里

其实 Android 并没有提供设置数据库连接池大小的 API(一些大型应用一般会集成自己的数据库源码,来修改相关内容),此时需要我们开启 WAL 模式,实际上 Android 提供的开启 WAL 模式与数据库连接池就是一回事,看下系统默认连接池的大小设置:

public static int getWALConnectionPoolSize() {
    int value = SystemProperties.getInt("debug.sqlite.wal.poolsize",
            Resources.getSystem().getInteger(
            com.android.internal.R.integer.db_connection_pool_size));
    //从这里可以得出结论:连接池默认最小为2,最大取决于系统配置
    //不过在我个人这台机器上测试发现得到的value==4.
    return Math.max(2, value);
}

从这里可以看出,当开启 WAL 模式后,系统默认连接池最小为 2。最大取决于系统配置。
ps:在我个人这台测试机上系统配置大小为 4。

关于连接池的作用补充说明:SQLite 支持多线程并发模式,需要开启下面的配置,当然 Android 系统默认开启了多线程 Multi-thread 模式。

PRAGMA SQLITE_THREADSAFE = 2

SQLite 锁的粒度都是数据库文件级别,并没有实现表级甚至行级的锁,还有需要说明的,同一个句柄同一时间只有一个线程在操作,这个时候我们需要打开连接池 Connection Pool。关于 SQLite 进程与线程并发你可以参考《Android 存储选项之 SQLite 优化那些事儿》。

回到 SQLiteConnectionPool 的构造方法,创建空闲连接超时管理机制,需要说明的是,处于长时间不使用的数据库连接也是在浪费系统资源,此时通过延迟关闭机制释放那些长时间处于空闲状态的数据库连接

public void setupIdleConnectionHandler(Looper looper, long timeoutMs) {
    synchronized (mLock) {
        //创建IdleConnectionHandler,超时管理的Handler
        mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs);
    }
}

IdleConnectionHandler 继承自 Handler:

private class IdleConnectionHandler extends Handler {
    //保存配置的空闲连接超时时间
    private final long mTimeout;

    IdleConnectionHandler(Looper looper, long timeout) {
        super(looper);
        mTimeout = timeout;
    }

    @Override
    public void handleMessage(Message msg) {
        // Skip the (obsolete) message if the handler has changed
        synchronized (mLock) {
            if (this != mIdleConnectionHandler) {
                return;
            }
            if (closeAvailableConnectionLocked(msg.what)) {
                //关闭空闲超时的SQLiteConnection
                
                //... 省略
            }
        }
    }

    void connectionReleased(SQLiteConnection con) {
        //根据连接id(ConnectionId),发送超时消息
        sendEmptyMessageDelayed(con.getConnectionId(), mTimeout);
    }

    void connectionAcquired(SQLiteConnection con) {
        // Remove any pending close operations
        removeMessages(con.getConnectionId());
    }

    void connectionClosed(SQLiteConnection con) {
        //移除当前超时机制,说明连接被重新使用
        removeMessages(con.getConnectionId());
    }
}

从源码中我们很容易看出,通过发送延迟消息来关闭处于空闲状态的数据库连接,当某个连接重新被使用时此时移除该超时关闭消息。超时时间就是在 OpenParams 中配置的 mIdleConnectionTimeout。它的默认值是 Long 的最大值,可以理解成”永不“释放。

看下数据库连接空闲超时关闭机制,处理如下:

public SQLiteConnection acquireConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
    //获取一个SQLiteConnection
    SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal);
    synchronized (mLock) {
        if (mIdleConnectionHandler != null) {
            //移除该连接的超时关闭消息
            mIdleConnectionHandler.connectionAcquired(con);
        }
    }
    return con;
}

每当我们从连接池获取到一个 SQLiteConnection 后,首先会根据该连接的 connectionId 移除当前的回收 Message,mIdleConnectionHandler.connectionAcquired 方法表示当前数据库连接重新被使用。

当该连接使用完成被 SQLiteConnectionPool 缓存后,又重新对该连接开启超时关闭机制:

public void releaseConnection(SQLiteConnection connection) {
    synchronized (mLock) {
        if (mIdleConnectionHandler != null) {
            //重新加入超时关闭机制
            mIdleConnectionHandler.connectionReleased(connection);
        }
    // ... 省略
}

SQLiteConnectionPool 构造方法一系列任务完成后,接着看 SQLiteConnection 的 open 方法如下:

// Might throw
private void open() {
    // Open the primary connection.
    // This might throw if the database is corrupt.
    //创建数据库主链接
    //只有主连接才可以写数据库,每一个数据库连接池默认仅有一条主连接
    mAvailablePrimaryConnection = openConnectionLocked(mConfiguration,
            true /*primaryConnection*/); // might throw
    // Mark it released so it can be closed after idle timeout
    synchronized (mLock) {
        if (mIdleConnectionHandler != null) {
            //由于此时还没有任何使用,故开启空闲超时关闭机制
            mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection);
        }
    }

    // Mark the pool as being open for business.
    mIsOpen = true;
    mCloseGuard.open("close");
}

数据库连接池被创建后,会默认创建一条主连接 SQLiteConnection,每一个数据库连接池默认仅有一条主连接,只有主连接可以对数据库写操作,另外还包含若干条非主连接

// Might throw.
private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration,
        boolean primaryConnection) {
    //当前连接的唯一id
    final int connectionId = mNextConnectionId++;
    //创建连接
    return SQLiteConnection.open(this, configuration,
            connectionId, primaryConnection); // might throw
}

connectionId 标识当前数据库连接的唯一 connectionId,该 id 就是用于在连接处于空闲状态时的唯一标志。 SQLiteConnection 静态方法 open 如下:

// Called by SQLiteConnectionPool only.
static SQLiteConnection open(SQLiteConnectionPool pool,
        SQLiteDatabaseConfiguration configuration,
        int connectionId, boolean primaryConnection) {
    //创建SQLiteConnection,每个SQLiteConnection对一个native层一个操作句柄
    SQLiteConnection connection = new SQLiteConnection(pool, configuration,
            connectionId, primaryConnection);
    try {
        //调用自己的open方法
        connection.open();
        return connection;
    } catch (SQLiteException ex) {
        connection.dispose(false);
        throw ex;
    }
}

一样的 API 套路,直接创建 SQLiteConnection,并调用其 open 方法。SQLiteConnection 的构造方法:

private SQLiteConnection(SQLiteConnectionPool pool,
        SQLiteDatabaseConfiguration configuration,
        int connectionId, boolean primaryConnection) {
    //持有数据库连接池
    mPool = pool;
    //操作日志
    mRecentOperations = new OperationLog(mPool);
    //也持有数据库配置信息
    mConfiguration = new SQLiteDatabaseConfiguration(configuration);
    //该链接的唯一id
    mConnectionId = connectionId;
    //是否是主链接
    mIsPrimaryConnection = primaryConnection;
    //是否是只读模式
    mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
    //缓存预编译后的SQL语句,内部采用LRU算法
    mPreparedStatementCache = new PreparedStatementCache(
            mConfiguration.maxSqlCacheSize);
    mCloseGuard.open("close");
}

SQLiteConnection 同样保存了当前数据库配置信息 SQLiteDatabaseConfiguration、还包括当前连接的唯一 connectionId、是否是主连接等。

这里需要说明的是 PreparedStatementCache 本质是一个 LRU 缓存,它用于缓存编译 SQL 语句后的 PreparedStatement 对象,一个 SQLiteConnecion 包含多个 PreparedStatement,可以使用它多次高效的执行同一个 SQL 语句。

SQLiteConnection 的 open 方法如下:

private void open() {
    //创建数据库操作句柄
    //同一个句柄同一时间只能有同一个线程在操作
    //SQLiteDatabase使用ThreadLocal解决多线程操作问题
    mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
            mConfiguration.label,
            SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME,
            mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount);
    //设置页缓存大小
    setPageSize();
    setForeignKeyModeFromConfiguration();
    //根据Configuration设置WAL模式
    setWalModeFromConfiguration();
    //设置日志限制大小
    setJournalSizeLimit();
    //设置检查点信息
    setAutoCheckpointInterval();
    setLocaleFromConfiguration();

    // Register custom functions.
    final int functionCount = mConfiguration.customFunctions.size();
    for (int i = 0; i < functionCount; i++) {
        SQLiteCustomFunction function = mConfiguration.customFunctions.get(i);
        nativeRegisterCustomFunction(mConnectionPtr, function);
    }
}

这里通过 nativeOpen 方法获取一个数据库操作连接(native 层 SQLiteConnection),每个 Java 层 SQLiteConnection 都会对应一个 native 层 SQLiteConnection 数据库连接。每个 native 层 SQLiteConnection 都会持有一个数据库操作句柄:

static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags,
115        jstring labelStr, jboolean enableTrace, jboolean enableProfile) {
116    int sqliteFlags;
117    if (openFlags & SQLiteConnection::CREATE_IF_NECESSARY) {
118        sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
119    } else if (openFlags & SQLiteConnection::OPEN_READONLY) {
120        sqliteFlags = SQLITE_OPEN_READONLY;
121    } else {
122        sqliteFlags = SQLITE_OPEN_READWRITE;
123    }
124
125    const char* pathChars = env->GetStringUTFChars(pathStr, NULL);
126    String8 path(pathChars);
127    env->ReleaseStringUTFChars(pathStr, pathChars);
128
129    const char* labelChars = env->GetStringUTFChars(labelStr, NULL);
130    String8 label(labelChars);
131    env->ReleaseStringUTFChars(labelStr, labelChars);
132
       //数据库操作句柄
133    sqlite3* db;
       //打开一个数据库
134    int err = sqlite3_open_v2(path.string(), &db, sqliteFlags, NULL);
135    if (err != SQLITE_OK) {
           //是否正确打开
136        throw_sqlite3_exception_errcode(env, err, "Could not open database");
137        return 0;
138    }
139
140    // Check that the database is really read/write when that is what we asked for.
141    if ((sqliteFlags & SQLITE_OPEN_READWRITE) && sqlite3_db_readonly(db, NULL)) {
           //如果打开数据库模式与当前不匹配
142        throw_sqlite3_exception(env, db, "Could not open the database in read/write mode.");
143        sqlite3_close(db);
144        return 0;
145    }
146
147    // Set the default busy handler to retry automatically before returning SQLITE_BUSY.
148    err = sqlite3_busy_timeout(db, BUSY_TIMEOUT_MS);
149    if (err != SQLITE_OK) {
           //设置默认超时机制
150        throw_sqlite3_exception(env, db, "Could not set busy timeout");
151        sqlite3_close(db);
152        return 0;
153    }
154
155    // Register custom Android functions.
156    err = register_android_functions(db, UTF16_STORAGE);
157    if (err) {
158        throw_sqlite3_exception(env, db, "Could not register Android SQL functions.");
159        sqlite3_close(db);
160        return 0;
161    }
162
163    // Create wrapper object.
       //创建数据库连接,内部持有数据库操作句柄
164    SQLiteConnection* connection = new SQLiteConnection(db, openFlags, path, label);
165
166    // Enable tracing and profiling if requested.
167    if (enableTrace) {
168        sqlite3_trace(db, &sqliteTraceCallback, connection);
169    }
170    if (enableProfile) {
171        sqlite3_profile(db, &sqliteProfileCallback, connection);
172    }
173
174    ALOGV("Opened connection %p with label '%s'", db, label.string());
175    return reinterpret_cast<jlong>(connection);
176}

SQLiteConnection 是数据库操作真正开始的地方,每一个 SQLiteConnection 内部持有一个数据库操作句柄。同一个句柄同一时间只有一个线程在操作,这也是我们要开启数据库连接池 Connection Pool 提高数据库并发访问的目的。

SQLiteConnection 中持有对应 native 层的匿名内存描述符 mConnectionPtr。以数据库查询为例:

public int executeForCursorWindow(String sql, Object[] bindArgs,
        CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
        CancellationSignal cancellationSignal) {

        // ... 省略
        
        try {
            //获取复用编译SQL语句后的对象
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                try {
                    //mConnectionPtr对应native层SQLiteConnection
                    //mStatementPtr对应nativePrepardStatement
                    //mWindowPtr对应native的CursorWindow,存放查询结果集
                    final long result = nativeExecuteForCursorWindow(
                            mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
                            startPos, requiredPos, countAllRows);
                    actualPos = (int)(result >> 32);
                    countedRows = (int)result;
                    filledRows = window.getNumRows();
                    window.setStartPosition(actualPos);
                    return countedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
           //...省略
        }
        
        // ... 省略
}

调用 SQLiteConnection 的 native 方法 nativeExecuteForCursorWindow 完成数据库操作过程。

static jlong nativeExecuteForCursorWindow(JNIEnv* env, jclass clazz,
666        jlong connectionPtr, jlong statementPtr, jlong windowPtr,
667        jint startPos, jint requiredPos, jboolean countAllRows) {
       //转换成native层SQLiteConnection
668    SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
       //转换成对应native层Statement
669    sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
       //对应native层CursorWindow
670    CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
671
672    status_t status = window->clear();
673    if (status) {
674        String8 msg;
675        msg.appendFormat("Failed to clear the cursor window, status=%d", status);
676        throw_sqlite3_exception(env, connection->db, msg.string());
677        return 0;
678    }
 //... 省略
}

SQLiteDatabase 的操作最后都交由 SQLiteConnection 来完成;SQLiteConnection 表示一条数据库操作连接,是真正执行数据库操作开始的地方。

本文只是重点介绍了 Android 提供的数据库框架 SQLiteDatabase 的创建过程源码分析,关于这部分更详细分析可以继续阅读文章开头给出的 SQLite 存储系列的其他文章。

总结

Android 系统为我们提供了 SQLiteOpenHelper 辅助完成 SQLiteDatabase 的创建,包括数据表创建、升/降级、打开、关闭等操作。通过 getReadableDatabase / getWriteableDatabase 分别获取只读或可读/写的 SQLiteDatabase 对象。

SQLiteConnectionPool 主要为提高数据库并发访问性能;SQLiteDatabase 根据配置信息 SQLiteDatabaseConfiguration 创建 SQLiteConnectionPool 连接池,包括连接池大小、WAL 模式、空闲连接超时等。SQLiteConnectionPool 缓存所有数据库操作连接 SQLiteConnection。

数据库连接池 SQLiteConnectionPool 被创建后,会默认创建一个数据库主连接 SQLiteConnection。每个 Java 层 SQLiteConnection 对应一个 native 层 SQLiteConnection,每个 SQLiteConnection 中又包含一个数据库操作句柄。

简单整理下整个关系

SQLiteDatabase创建过程

以上便是个人在学习 SQLiteDatabase 数据库创建过程的体会和总结,文中如有不妥或有更好的分析结果,欢迎大家指出。

如果文章对你有帮助,就请留个赞吧!

上一篇 下一篇

猜你喜欢

热点阅读