1.寫在前面的話
前面寫過一篇關于Sqlite基本操作的文章,今天我們來學習Android中如何使用Sqlite以及性能優化。
2.Android平臺下數據庫相關類
SQLiteOpenHelper 抽象類:通過從此類繼承實現用戶類,來提供數據庫打開、關閉等操作函數。
SQLiteDatabase 數據庫訪問類:執行對數據庫的插入記錄、查詢記錄等操作。
SQLiteCursor 查詢結構操作類:用來訪問查詢結果中的記錄。
3.Android Sqlite的使用
(1)創建數據庫
Android下要使用Sqlite首先要寫一個SQLiteOpenHelper的實現類,該類的構造函數如下:
private MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
需要傳入的參數解釋如下:
name:數據庫的名稱,用這個名稱來打開創建或打開相應的數據庫。
factory:用來創建cursor,通常情況下我們傳入null,使用默認的就行。
version:數據庫的版本,從1開始,可以修改版本號,來除法數據庫的更新操作。
對于SQLiteOpenHelper我們般會設計成單例。
private static MyDBHelper myDBHelper;
public static synchronized MyDBHelper getInstance(Context context) {
if (myDBHelper == null) {
Context applicationContext = context.getApplicationContext();
myDBHelper = new MyDBHelper(applicationContext);
}
return myDBHelper;
}
private MyDBHelper(Context context) {
this(context, DB_NAME, null, VERSION);
}
當我們首次使用MyDBHelper來獲取數據庫時,即調用getWritableDatabase或getReadableDatabase方法時,變會觸發DBHelper中的onCreate方法,這時我們可以在數據庫中創建表:
@Override
public void onCreate(SQLiteDatabase db) {
StringBuilder sql = new StringBuilder();
sql.append("create table ");
sql.append(TAB_PERSON + "(");
sql.append("id integer,");
sql.append("name char(8),");
sql.append("age int");
sql.append(");");
db.execSQL(sql.toString());
}
以上代碼實際執行了一個sql語句create table person(id integer,name char(10), age int);創建了一張person表。
(2)更新數據庫
通過修改數據庫的版本我們可以觸發數據庫的更新。這里我們修改數據庫的version為2,并在更新時添加一個新的developer表。和創建一樣,onUpgrade會在調用getWritableDatabase或getReadableDatabase時觸發。
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
StringBuilder sql = new StringBuilder();
sql.append("create table ");
sql.append(TAB_DEVLOPER + "(");
sql.append("id integer,");
sql.append("position char(20),");
sql.append(");");
db.execSQL(sql.toString());
}
(3)增刪查改
對于增刪查改,我們可以分別調用數據庫的insert、delete、query、update方法傳入參數來進行操作,當然也可以直接用execSQL方法來執行Sql來進行操作。比如我們要在在person表中插入一條記錄,我們可以在MyDBHelper中創建一個inser方法,如下:
public boolean insert(int id, String name, int age) {
ContentValues values = new ContentValues();
values.put("name", name);
values.put("age", age);
long insert = getWritableDatabase().insert(TAB_PERSON, null, values);
return insert >= 1;
}
4.Sqlite性能優化
(1)編譯SQL語句
Sqlite想要執行操作,需要將程序中的sql語句編譯成對應的SQLiteStatement,比如select * from record這一句,被執行100次就需要編譯100次。對于批量處理插入或者更新的操作,我們可以使用顯示編譯來做到重用SQLiteStatement。
想要做到重用SQLiteStatement也比較簡單,基本如下:
編譯sql語句獲得SQLiteStatement對象,參數使用?代替
在循環中對SQLiteStatement對象進行具體數據綁定,bind方法中的index從1開始,不是0
如下向person表中插入100條數據:
public void insertBatchPreCompile() {
long start = SystemClock.currentThreadTimeMillis();
String sql = "insert into " + TAB_PERSON + " values (?,'test','1');";
SQLiteStatement sqLiteStatement = getReadableDatabase().compileStatement(sql);
int count = 0;
while (count < 100) {
count++;
sqLiteStatement.clearBindings();
sqLiteStatement.bindLong(1, count);
sqLiteStatement.executeInsert();
}
Log.e(TAG, "insert recompile use time " + (SystemClock.currentThreadTimeMillis() - start));
}
(2)顯示使用事務
在Android中,無論是使用SQLiteDatabase的insert,delete等方法還是execSQL都開啟了事務,來確保每一次操作都具有原子性,使得結果要么是操作之后的正確結果,要么是操作之前的結果。
然而事務的實現是依賴于名為rollback journal文件,借助這個臨時文件來完成原子操作和回滾功能。既然屬于文件,就符合Unix的文件范型(Open-Read/Write- Close),因而對于批量的修改操作會出現反復打開文件讀寫再關閉的操作。然而好在,我們可以顯式使用事務,將批量的數據庫更新帶來的journal文件打開關閉降低到1次。
具體的實現代碼如下:
public void insertWithTransaction() {
long start = SystemClock.currentThreadTimeMillis();
int count = 0;
try {
getWritableDatabase().beginTransaction();
while (count++ < 100) {
insert(count, "test", 1);
}
getWritableDatabase().setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
getWritableDatabase().endTransaction();
}
Log.e(TAG, "insert traceaction use time " + (SystemClock.currentThreadTimeMillis() - start));
}
使用這兩種方式分別優化,對比效果如下:
從圖中可以看到在插入100條的情況下,使用預編譯的方式可以稍微提升性能,但是使用事務,能夠使性能提升大概8倍,所以可以看出頻繁的IO操作還是比較耗時的。同時使用兩種方式進行優化,可以提升17倍,優化效果非常明顯。
(3)建立索引
a.索引的概念
索引,使用索引可快速訪問數據庫表中的特定信息。索引是對數據庫表中一列或多列的值進行排序的一種結構。
在關系數據庫中,索引是一種與表有關的數據庫結構,它可以使對應于表的SQL語句執行得更快。索引的作用相當于圖書的目錄,可以根據目錄中的頁碼快速找到所需的內容。當表中有大量記錄時,若要對表進行查詢,第一種搜索信息方式是全表搜索,是將所有記錄一一取出,和查詢條件進行一一對比,然后返回滿足條件的記錄,這樣做會消耗大量數據庫系統時間,并造成大量磁盤I/O操作;第二種就是在表中建立索引,然后在索引中找到符合查詢條件的索引值,最后通過保存在索引中的ROWID(相當于頁碼)快速找到表中對應的記錄。
索引是一個單獨的、物理的數據庫結構,它是某個表中一列或若干列值的集合和相應的指向表中物理標識這些值的數據頁的邏輯指針清單。
索引提供指向存儲在表的指定列中的數據值的指針,然后根據您指定的排序順序對這些指針排序。數據庫使用索引的方式與您使用書籍中的索引的方式很相似:它搜索索引以找到特定值,然后順指針找到包含該值的行。
b.建立索引
創建索引的基本語法:
CREATE INDEX index_name ON table_name;
創建單列索引
CREATE INDEX index_name ON table_name;
c.索引的利弊
毋庸置疑,索引加速了我們檢索數據表的速度。然而正如西方諺語 “There are two sides of a coin”,索引亦有缺點:
對于增加,更新和刪除來說,使用了索引會變慢,比如你想要刪除字
- 列表內容典中的一個字,那么你同時也需要刪除這個字在拼音索引和部首索引中的信息。
- 建立索引會增加數據庫的大小,比如字典中的拼音索引和部首索引實際上是會增加字典的頁數,讓字典變厚的。
- 為數據量比較小的表建立索引,往往會事倍功半。
所以使用索引需要考慮實際情況進行利弊權衡,對于查詢操作量級較大,業務對要求查詢要求較高的,還是推薦使用索引的。
(4)查詢數據優化
按需獲取列信息
db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ;
其中上面方法的第二個參數類型為String[],意思是返回結果參考的colum信息,傳遞null表明需要獲取全部的column數據。如果我們不需要所有列的信息,最好指定一下需要的列。
提前獲取索引
例如下面的代碼,我們可以把獲取列索引的代碼cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME)放到循環外,這樣不需要每次獲取。
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));
}
}
(5)ContentValues的容量調整
SQLiteDatabase提供了方便的ContentValues簡化了我們處理列名與值的映射,ContentValues內部采用了 HashMap來存儲Key-Value數據,ContentValues的初始容量是8,如果當添加的數據超過8之前,則會進行雙倍擴容操作,因此建議對ContentValues填入的內容進行估量,設置合理的初始化容量,減少不必要的內部擴容操作。
(6)及時關閉Cursor
(7)耗時異步化
數據庫的操作,屬于本地IO,通常比較耗時,如果處理不好,很容易導致ANR,因此建議將這些耗時操作放入異步線程中處理。
本文Demo下載地址SqliteDemo