Android 中 SQLite 性能優化
數據庫是應用開發中常用的技術,在Android應用中也不例外。Android默認使用了SQLite數據庫,在應用程序開發中,我們使用最多的無外乎增刪改查。縱使操作簡單,也有可能出現查找數據緩慢,插入數據耗時等情況,如果出現了這種問題,我們就需要考慮對數據庫操作進行優化了。本文將介紹一些實用的數據庫優化操作,希望可以幫助大家更好地在開發過程中使用數據庫。
建立索引
很多時候,我們都聽說,想要查找快速就建立索引。這句話沒錯,數據表的索引類似于字典中的拼音索引或者部首索引。
索引的解釋
重溫一下我們小時候查字典的過程:
- 對于已經知道拼音的字,比如中這個字,我們只需要在拼音索引里面找到zhong,就可以確定這個字在詞典中的頁碼。
- 對于不知道拼音的字,比如欗這個字,我們只需要在部首索引里面查找這個字,就能找到確定這個字在詞典中的頁碼。 </ul>
- 對于增加,更新和刪除來說,使用了索引會變慢,比如你想要刪除字典中的一個字,那么你同時也需要刪除這個字在拼音索引和部首索引中的信息。
- 建立索引會增加數據庫的大小,比如字典中的拼音索引和部首索引實際上是會增加字典的頁數,讓字典變厚的。
- 為數據量比較小的表建立索引,往往會事倍功半。 </ul>
- 編譯sql語句獲得SQLiteStatement對象,參數使用?代替
- 在循環中對SQLiteStatement對象進行具體數據綁定,bind方法中的index從1開始,不是0 </ul>
沒錯,索引做的事情就是這么簡單,使得我們不需要查找整個數據表就可以實現快速訪問。
建立索引
創建索引的基本語法如下
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也比較簡單,基本如下:
請參考如下簡單的使用代碼
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