解析 SQLiteOpenHelper

你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher

“SQLiteOpenHelper” 是一个用来管理数据库的创建和版本管理的辅助类。它是一个抽象类,要使用它必须创建一个子类继承 SQLiteOpenHelper,并实现 onCreate,onUpgrade 这两个抽象方法。这样,如果数据库存在,它就会打开;如果不存在,就会创建这个数据库,并且如果必要的话会自动升级数据库。为了确保数据库始终处于一个合理的状态,它会使用事务。它便于 ContentProvider 实现在第一次使用之前延迟打开和升级数据库,以避免数据库升级时间过长阻塞应用的启动。

构造方法

public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
        this(context, name, factory, version, null);
}

public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
        DatabaseErrorHandler errorHandler) {
    if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);

    mContext = context;
    mName = name;
    mFactory = factory;
    mNewVersion = version;
    mErrorHandler = errorHandler;
}

可以看到,第一个 4 个参数的构造方法,会调用第二个 5 个参数的构造方法,但 4 个参数的构造方法会很快的返回。构造方法的作用是创建一个辅助对象来创建、打开、管理一个数据库。需要注意的是如果没有调用过 getWriteableDatabase 或者 getReadableDatabase 数据库不会自动创建或者打开。构造方法中的参数除了context 和 version 之外,其他参数可以为 null。当 name 为 null 时,会在内存中创建数据库;如果CursorFactory 为 null,会使用一个默认的。

从 5 个参数的构造方法中,可以看到 version 不能小于 1 ,否则会抛出非法参数异常。

onCreate

public abstract void onCreate(SQLiteDatabase db);

很明显这是一个抽象方法,前面我们也说过了,它是继承 SQLiteOpenHelper 必须要实现的方法之一。

第一次创建数据库时调用,在这个方法中创建表和表的初始化。

onUpgrade

public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);

这也是一个抽象方法,它也是继承 SQLiteOpenHelper 必须要实现的方法之一。

当数据库需要升级时,会调用这个方法。应该使用这个方法来实现删除表、添加表或者做一些需要升级新的策略版本的事情。此方法在事务中执行。如果抛出异常,所有更改将自动回滚。

onDowngrade

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

当数据库需要降级时调用。这个方法与 onUpgrade(SQLiteDatabase, int, int) 方法非常相似,它是在数据库之前版本比当前的版本新的时候,才会被调用。但是这个方法不是抽象的,因此它不强制要求重写。如果这个方法没有被重写,默认的实现会拒绝数据库版本降级,并抛出SQLiteException异常。这个方法是在事务中执行的。如果有异常被抛出,所有的改变都会被回滚。

这里我要说一下,这种情况是怎么发生的,比如用户当前数据库版本号为 2,但是Ta又覆盖安装了一个旧版本(版本号为1),这个时候数据库版本就从 2 降到了 1,就会触发 onDowngrade 方法了。

getDatabaseName

public String getDatabaseName() {
    return mName;
}

这个方法再简单不过了,就是返回数据库的名字,它和构造方法中传入的名字是一样的。

setWriteAheadLoggingEnabled

public void setWriteAheadLoggingEnabled(boolean enabled) {
    synchronized (this) {
        if (mEnableWriteAheadLogging != enabled) {
            if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
                if (enabled) {
                    mDatabase.enableWriteAheadLogging();
                } else {
                    mDatabase.disableWriteAheadLogging();
                }
            }
            mEnableWriteAheadLogging = enabled;
        }
    }
}

这个方法的作用是:启用或禁用数据库的预写日志。

预写日志不能用于只读的数据库,因此如果数据库是以只读的方式被打开,这个标记值会被忽略。

源代码的第 3 行 if (mEnableWriteAheadLogging != enabled) 的使用方法可以借鉴一下,这行代码从逻辑上是可有可无的,它的妙处在于减少了不必要的代码执行,提高了效率(如果设置的值和当前值相等,就不作任何操作)。

那么这个预写日志到底是什么鬼呢?

从第 5、6 行代码可以看到如果 enabled 的值为 true,就会调用 mDatabase.enableWriteAheadLogging();

调用此方法后,只要数据库保持打开,则并行执行查询操作。用于并行执行查询的连接的最大数目取决于设备内存和其他属性。如果一个查询是事务的一部分,那么它就在同一个数据库句柄上执行。

它通过打开数据库的多个连接并为每个查询使用不同的数据库连接来实现在同一数据库中并行执行多个线程的查询,同时数据库的日志模式也被更改以启用写入与读取同时进行。

当预写日志没有启用时(默认状态),同一时间在数据库上读取和写入是不可能的。因为写入线程会在修改数据库之前隐式地获取数据库上的互斥锁,以防止写入操作完成之前其他线程读取访问数据库。

相反,当启用预写日志时,写入操作会在分离的日志文件中进行,该文件允许读取同时进行。当数据库正在进行写入操作时,其他线程上的读取操作将在写入开始前会感知数据库的状态,当写入操作完成后,其他线程上的读取操作将感知数据库的新状态。

当数据库同时被多个线程同时访问和修改时,启用预写日志是一个好办法。然而,开启预写日志功能,会比普通日记使用更多的内存,因为同一个数据库有多个连接。因此,如果一个数据库只有一个线程使用,或者优化并发不是很重要,那么应该禁用预写日志功能。

getWritableDatabase 和 getReadableDatabase

public SQLiteDatabase getWritableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(true);
    }
}

这个方法用于创建或打开用于读取和写入的数据库。第一次被调用时数据库将被打开,onCreate、onUpgrade、onOpen 将被调用。一旦成功打开,数据库将被缓存,因此每次需要写入数据库时,都可以调用此方法(确保当不再需要对这个数据库进行操作时,调用 close方法关闭)。权限问题或磁盘已满等问题可能会导致这个方法打开数据库失败,但如果问题是固定的,多次尝试可能会成功。数据库升级可能需要很长时间,不应该从应用程序主线程调用此方法。

public SQLiteDatabase getReadableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(false);
    }
}

这个方法用于创建或打开数据库。它和 getWriteableDatabase() 方法返回的是同一个对象,但是因为它只需要返回只读的数据库,所以不会有磁盘已满等问题。

这两个方法的代码都很简单,调用了 getDatabaseLocked(true/false);下面来看一下这个方法。

getDatabaseLocked

private SQLiteDatabase getDatabaseLocked(boolean writable) {
    if (mDatabase != null) {
        if (!mDatabase.isOpen()) {
            // Darn!  The user closed the database by calling mDatabase.close().
            mDatabase = null;
        } else if (!writable || !mDatabase.isReadOnly()) {
            // The database is already open for business.
            return mDatabase;
        }
    }

    if (mIsInitializing) {
        throw new IllegalStateException("getDatabase called recursively");
    }

    SQLiteDatabase db = mDatabase;
    try {
        mIsInitializing = true;

        if (db != null) {
            if (writable && db.isReadOnly()) {
                db.reopenReadWrite();
            }
        } else if (mName == null) {
            db = SQLiteDatabase.create(null);
        } else {
            try {
                if (DEBUG_STRICT_READONLY && !writable) {
                    final String path = mContext.getDatabasePath(mName).getPath();
                    db = SQLiteDatabase.openDatabase(path, mFactory,
                            SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                } else {
                    db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
                            Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
                            mFactory, mErrorHandler);
                }
            } catch (SQLiteException ex) {
                if (writable) {
                    throw ex;
                }
                Log.e(TAG, "Couldn't open " + mName
                        + " for writing (will try read-only):", ex);
                final String path = mContext.getDatabasePath(mName).getPath();
                db = SQLiteDatabase.openDatabase(path, mFactory,
                        SQLiteDatabase.OPEN_READONLY, mErrorHandler);
            }
        }

        onConfigure(db);

        final int version = db.getVersion();
        if (version != mNewVersion) {
            if (db.isReadOnly()) {
                throw new SQLiteException("Can't upgrade read-only database from version " +
                        db.getVersion() + " to " + mNewVersion + ": " + mName);
            }

            db.beginTransaction();
            try {
                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(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();
        }
    }
}

首先这是一个私有的方法。在这个方法中会根据各种情况创建或者打开一个数据库并赋值给局部变量 db;之后会调用 onConfigure(db) 这个方法下面会介绍,默认是一个空方法,我们可以重写此方法,对 db 进行配置;之后会对当前数据库的版本与最新版本进行对比,如果 db 是只读的,会抛出异常(不能对只读数据库进行版本的更新);如果 db 可写,就开启一个事务,进行版本更新;版本更新完毕后,会调用 onOpen(db) 方法,同样这个方法下面会介绍,也是一个空方法,可以重写此方法;最后,会关闭数据库。

onConfigure

在配置数据库连接时调用,从上面 getDatabaseLocked 方法中,我们可以看出 onConfigure() 在 onCreate(SQLiteDatabase), onUpgrade(SQLiteDatabase, int, int), onDowngrade(SQLiteDatabase, int, int), onOpen(SQLiteDatabase) 这些方法之前调用。在这个方法中不应修改数据库,除非根据需要配置数据库连接,启用预写日志或外键支持等功能。

onOpen

数据库被打开时,会调用这个方法。在升级数据库之前,这个方法的实现应该调用 isReadOnly() 方法检查数据库是否是只读的。

此方法在配置数据库连接和创建数据库模式、升级或必要的降级之后调用。如果数据库连接的配置必须以某种方式创建,升级,或降级,那么在onConfigure方法中做这些事情。

你可能感兴趣的:(解析 SQLiteOpenHelper)