Android 中 SQLite 效能優化

技術小黑屋發表於2015-12-15

資料庫是應用開發中常用的技術,在Android應用中也不例外。Android預設使用了SQLite資料庫,在應用程式開發中,我們使用最多的無外乎增刪改查。縱使操作簡單,也有可能出現查詢資料緩慢,插入資料耗時等情況,如果出現了這種問題,我們就需要考慮對資料庫操作進行優化了。本文將介紹一些實用的資料庫優化操作,希望可以幫助大家更好地在開發過程中使用資料庫。

建立索引

很多時候,我們都聽說,想要查詢快速就建立索引。這句話沒錯,資料表的索引類似於字典中的拼音索引或者部首索引。

索引的解釋

重溫一下我們小時候查字典的過程:

  • 對於已經知道拼音的字,比如中這個字,我們只需要在拼音索引裡面找到zhong,就可以確定這個字在詞典中的頁碼。
  • 對於不知道拼音的字,比如欗這個字,我們只需要在部首索引裡面查詢這個字,就能找到確定這個字在詞典中的頁碼。

沒錯,索引做的事情就是這麼簡單,使得我們不需要查詢整個資料表就可以實現快速訪問。

建立索引

建立索引的基本語法如下

CREATE INDEX index_name ON table_name;

建立單列索引

CREATE INDEX index_name ON table_name (column_name);

索引真的好麼

毋庸置疑,索引加速了我們檢索資料表的速度。然而正如西方諺語 “There are two sides of a coin”,索引亦有缺點:

  • 對於增加,更新和刪除來說,使用了索引會變慢,比如你想要刪除字典中的一個字,那麼你同時也需要刪除這個字在拼音索引和部首索引中的資訊。
  • 建立索引會增加資料庫的大小,比如字典中的拼音索引和部首索引實際上是會增加字典的頁數,讓字典變厚的。
  • 為資料量比較小的表建立索引,往往會事倍功半。

所以使用索引需要考慮實際情況進行利弊權衡,對於查詢操作量級較大,業務對要求查詢要求較高的,還是推薦使用索引的。

編譯SQL語句

SQLite想要執行操作,需要將程式中的sql語句編譯成對應的SQLiteStatement,比如select * from record這一句,被執行100次就需要編譯100次。對於批量處理插入或者更新的操作,我們可以使用顯式編譯來做到重用SQLiteStatement。

想要做到重用SQLiteStatement也比較簡單,基本如下:

  • 編譯sql語句獲得SQLiteStatement物件,引數使用?代替
  • 在迴圈中對SQLiteStatement物件進行具體資料繫結,bind方法中的index從1開始,不是0

請參考如下簡單的使用程式碼

 private void insertWithPreCompiledStatement(SQLiteDatabase db) {
    String sql = "INSERT INTO " + TableDefine.TABLE_RECORD + "( " + TableDefine.COLUMN_INSERT_TIME + ") VALUES(?)";
    SQLiteStatement  statement = db.compileStatement(sql);
    int count = 0;
    while (count < 100) {
        count++;
        statement.clearBindings();
        statement.bindLong(1, System.currentTimeMillis());
        statement.executeInsert();
    }
}

顯式使用事務

在Android中,無論是使用SQLiteDatabase的insert,delete等方法還是execSQL都開啟了事務,來確保每一次操作都具有原子性,使得結果要麼是操作之後的正確結果,要麼是操作之前的結果。

然而事務的實現是依賴於名為rollback journal檔案,藉助這個臨時檔案來完成原子操作和回滾功能。既然屬於檔案,就符合Unix的檔案範型(Open-Read/Write- Close),因而對於批量的修改操作會出現反覆開啟檔案讀寫再關閉的操作。然而好在,我們可以顯式使用事務,將批量的資料庫更新帶來的journal檔案開啟關閉降低到1次。

具體的實現程式碼如下:

 private void insertWithTransaction(SQLiteDatabase db) {
    int count = 0;
    ContentValues values = new ContentValues();
    try {
        db.beginTransaction();
        while (count++ < 100) {
            values.put(TableDefine.COLUMN_INSERT_TIME, System.currentTimeMillis());
            db.insert(TableDefine.TABLE_RECORD, null, values);
        }
          db.setTransactionSuccessful();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        db.endTransaction();
    }
}

上面的程式碼中,如果沒有異常丟擲,我們則認為事務成功,呼叫db.setTransactionSuccessful();確保操作真實生效。如果在此過程中出現異常,則批量資料一條也不會插入現有的表中。

查詢資料優化

對於查詢的優化,除了建立索引以外,有以下幾點微優化的建議

按需獲取資料列資訊

通常情況下,我們處於自己省時省力的目的,對於查詢使用類似這樣的程式碼

 private void badQuery(SQLiteDatabase db) {
    db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ;
}

其中上面方法的第二個引數型別為String[],意思是返回結果參考的colum資訊,傳遞null表明需要獲取全部的column資料。這裡建議大家傳遞真實需要的字串資料物件表明需要的列資訊,這樣做效率會有所提升。

提前獲取列索引

當我們需要遍歷cursor時,我們通常的做法是這樣

 private void badQueryWithLoop(SQLiteDatabase db) {
    Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
    while (cursor.moveToNext()) {
        long insertTime = cursor.getLong(cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME));
    }
}

但是如果我們將獲取ColumnIndex的操作提到迴圈之外,效果會更好一些,修改後的程式碼如下:

 private void goodQueryWithLoop(SQLiteDatabase db) {
    Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
    int insertTimeColumnIndex = cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME);
    while (cursor.moveToNext()) {
        long insertTime = cursor.getLong(insertTimeColumnIndex);
    }
    cursor.close();
}

ContentValues的容量調整

SQLiteDatabase提供了方便的ContentValues簡化了我們處理列名與值的對映,ContentValues內部採用了 HashMap來儲存Key-Value資料,ContentValues的初始容量是8,如果當新增的資料超過8之前,則會進行雙倍擴容操作,因此建議對ContentValues填入的內容進行估量,設定合理的初始化容量,減少不必要的內部擴容操作。

及時關閉Cursor

使用資料庫,比較常見的就是忘記關閉Cursor。關於如何發現未關閉的Cursor,我們可以使用StrictMode,詳細請戳這裡 Android效能調優利器StrictMode

耗時非同步化

資料庫的操作,屬於本地IO,通常比較耗時,如果處理不好,很容易導致ANR,因此建議將這些耗時操作放入非同步執行緒中處理,這裡推薦一個單執行緒 + 任務佇列形式處理的HandlerThread實現非同步化。

原始碼下載

示例原始碼,存放在Github,地址為 AndroidSQLiteTuningDemo

相關文章