解析 SQLiteOpenHelper

「SQLiteOpenHelper」 是一個用來管理數據庫的建立和版本管理的輔助類。它是一個抽象類,要使用它必須建立一個子類繼承 SQLiteOpenHelper,並實現 onCreate,onUpgrade 這兩個抽象方法。這樣,若是數據庫存在,它就會打開;若是不存在,就會建立這個數據庫,而且若是必要的話會自動升級數據庫。爲了確保數據庫始終處於一個合理的狀態,它會使用事務。它便於 ContentProvider 實如今第一次使用以前延遲打開和升級數據庫,以免數據庫升級時間過長阻塞應用的啓動。javascript

構造方法

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,會使用一個默認的。java

從 5 個參數的構造方法中,能夠看到 version 不能小於 1 ,不然會拋出非法參數異常。數據庫

onCreate

public abstract void onCreate(SQLiteDatabase db);複製代碼

很明顯這是一個抽象方法,前面咱們也說過了,它是繼承 SQLiteOpenHelper 必需要實現的方法之一。緩存

第一次建立數據庫時調用,在這個方法中建立表和表的初始化。併發

onUpgrade

public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);複製代碼

這也是一個抽象方法,它也是繼承 SQLiteOpenHelper 必需要實現的方法之一。ide

當數據庫須要升級時,會調用這個方法。應該使用這個方法來實現刪除表、添加表或者作一些須要升級新的策略版本的事情。此方法在事務中執行。若是拋出異常,全部更改將自動回滾。優化

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異常。這個方法是在事務中執行的。若是有異常被拋出,全部的改變都會被回滾。ui

這裏我要說一下,這種狀況是怎麼發生的,好比用戶當前數據庫版本號爲 2,可是Ta又覆蓋安裝了一箇舊版本(版本號爲1),這個時候數據庫版本就從 2 降到了 1,就會觸發 onDowngrade 方法了。this

getDatabaseName

public String getDatabaseName() {
    return mName;
}複製代碼

這個方法再簡單不過了,就是返回數據庫的名字,它和構造方法中傳入的名字是同樣的。spa

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) 的使用方法能夠借鑑一下,這行代碼從邏輯上是無關緊要的,它的妙處在於減小了沒必要要的代碼執行,提升了效率(若是設置的值和當前值相等,就不做任何操做)。

那麼這個預寫日誌究竟是什麼鬼呢?

從第 五、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方法中作這些事情。

簡書地址:www.jianshu.com/p/842609bfe…
CSDN:blog.csdn.net/xiaomai9498…

相關文章
相關標籤/搜索