Android数据库源码分析(1)-getReadableDatabase和getWritableDatabase

本系列主要关注安卓数据库的线程行为,分为四个部分:
(1)SQLiteOpenHelper的getReadableDatabase和getWritableDatabase
(2)SQLiteDatabase操作在多线程下的行为
(3)连接缓存池SQLiteConnectionPool
(4)SQLiteDatabase多线程实践

安卓数据库的线程行为在安卓的文档中少有提及,因此编写时不免产生一些疑问。本系列旨在扫除这些疑惑。

本篇主要关注SQLiteOpenHelper中两个方法的行为:getReadableDatabasegetWritableDatabase

先上结论:
(1)检查是否有已经打开的SQLiteDatabase。如果已经打开的SQLiteDatabase可写,则直接返回之。如果已经打开的SQLiteDatabase不可写并且需要一个可写的对象,则尝试重新将其打开为可写。
(2)如果没有可用的SQLiteDatabase,则尝试创建一个可写的对象。如果创建成功,则返回之。如果创建失败,且需要可写,则抛出SQLiteException。如果创建失败,且只需要可读,则尝试创建一个只读的SQLiteDatabase对象并返回。

废话不多说,看源码

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

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

这里看起来很简单,即加锁,然后调用一个核心逻辑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;
    }
}

mDatabase是一个成员变量,事实上就是上一次缓存的SQLiteDatabase对象。如果不为空,会倾向于返回同一个对象,但是要进行一定的条件判断。具体地,先检查是否关闭,关闭的丢弃;再进行可写检查。如果只需要只读数据库,那么直接返回就可以了。如果需要可写数据库,那么检查一下mDatabase是否可写,如果为真才会返回。

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

这一步看信息是为了防止递归调用getXXXDatabase。一般来说进不到这里,是一个保护性的代码。如果在重载的onConfigureonCreateonOpen这些方法里调用getReadableDatabasegetWritableDatabase或者close的话,就会触发这里的Exception。

接下来是一大段try ... catch,我这里直接在代码里加注释了。

SQLiteDatabase db = mDatabase;
try {
    mIsInitializing = true;  //上面判断用的那个判断标记
    if (db != null) {  //这里不为空的情况,应该是“拥有一个可读数据库,但需要一个可写数据库”
        if (writable && db.isReadOnly()) {
            db.reopenReadWrite();  //看名字,把它重打开成可写的
        }
    } else if (mName == null) {
        db = SQLiteDatabase.create(null);  //mName是SQLiteOpenHelper构造器里的数据库名参数初始化的。这个条件下数据库名为空,数据库名会采用魔法字符串":memory:",事实上这个数据库会被创建到内存里。
    } else {  //确实需要创建磁盘数据库了
        try {
            if (DEBUG_STRICT_READONLY && !writable) {  //DEBUG_STRICT_READONLY条件下,会尽量创建不可写的数据库
                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) {  //创建失败了,fallback
            if (writable) {  //没办法了,把Exception抛出
                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);  //fallback为不可写的数据库
        }
    }
    onConfigure(db);  //一个hook
    final int version = db.getVersion();
    if (version != mNewVersion) {  //要升级
        if (db.isReadOnly()) {  //然而不可写,扔Exception
            throw new SQLiteException("Can't upgrade read-only database from version " +
                    db.getVersion() + " to " + mNewVersion + ": " + mName);
        }
        db.beginTransaction();  //这里开了事务了。所以,在onCreate和onUpgrade里面不要自己开事务,一般来说没意义。也不要对数据库执行close,会挂掉。
        try {
            if (version == 0) {  //没有过这个数据库,调用onCreate。这里可以看出来,不能把数据库的版本号设为0,不然每次打开都会onCreate。
                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);  //又一个hook
    if (db.isReadOnly()) {
        Log.w(TAG, "Opened " + mName + " in read-only mode");
    }
    mDatabase = db;
    return db;
} finally {  //善后
    mIsInitializing = false;
    if (db != null && db != mDatabase) {//这是什么情况呢?mDatabase的修改除了这个方法之外,还有一个SQLiteOpenHelper.close()。不过这个close也是加锁的。所以这只是保护性代码?
        db.close();
    }
}

与这两个方法具有同一个锁的还有一个,也值得一看。这个方法就简单了:

public synchronized void close() {
    //还是这个判断,防递归
    if (mIsInitializing) throw new IllegalStateException("Closed during initialization");

    if (mDatabase != null && mDatabase.isOpen()) {  //关闭并置空
        mDatabase.close();
        mDatabase = null;
    }
}

由上文可知,在数据库未关闭的情况下,getReadableDatabasegetWritableDatabase是会倾向于每次返回同一个对象的。开发时如果在多线程下频繁进行数据库打开和关闭的话,就可能在一个线程中关闭了数据库,而在另一个线程中继续使用它,这时就会造成应用崩溃。

解决的方法一是对每个操作加锁;二是做一个引用计数来决定是否关闭数据库。显然第二种方法性能更好。而对数据库的具体操作,安卓是保证了线程安全性的。这个问题会在下一篇探讨。

你可能感兴趣的:(Android数据库源码分析(1)-getReadableDatabase和getWritableDatabase)