咱們你們都知道Android平臺提供給咱們一個數據庫輔助類來建立或打開數據庫,這個輔助類繼承自SQLiteOpenHelper類,在該類的構造器中,調用Context中的方法建立並打開一個指定名稱的數據庫對象。繼承和擴展SQLiteOpenHelper類主要作的工做就是重寫如下兩個方法。
onCreate(SQLiteDatabase db) : 當數據庫被首次建立時執行該方法,通常將建立表等初始化操做在該方法中執行。
onUpgrade(SQLiteDatabse dv, int oldVersion,int new Version):當打開數據庫時傳入的版本號與當前的版本號不一樣時會調用該方法。
除了上述兩個必需要實現的方法外,還能夠選擇性地實現onOpen 方法,該方法會在每次打開數據庫時被調用。
SQLiteOpenHelper 類的基本用法是:當須要建立或打開一個數據庫並得到數據庫對象時,首先根據指定的文件名建立一個輔助對象,而後調用該對象的getWritableDatabase 或 getReadableDatabase方法 得到SQLiteDatabase 對象。
調用getReadableDatabase 方法返回的並不老是隻讀數據庫對象,通常來講該方法和getWriteableDatabase 方法的返回狀況相同,只有在數據庫僅開放只讀權限或磁盤已滿時纔會返回一個只讀的數據庫對象 java
SQLite實質上是將數據寫入一個文件,一般狀況下,在應用的包名下面都能找到xxx.db的文件,擁有root權限的手機,能夠經過adb shell,看到data/data/packagename/databases/xxx.db這樣的文件。
咱們能夠得知SQLite是文件級別的鎖:多個線程能夠同時讀,可是同時只能有一個線程寫。Android提供了SqliteOpenHelper類,加入Java的鎖機制以便調用。
若是多線程同時讀寫(這裏的指不一樣的線程用使用的是不一樣的Helper實例),後面的就會遇到android.database.sqlite.SQLiteException: database is locked這樣的異常。
對於這樣的問題,解決的辦法就是keep single sqlite connection,保持單個SqliteOpenHelper實例,同時對全部數據庫操做的方法添加synchronized關鍵字。 android
public class DatabaseHelper extends SQLiteOpenHelper { public static final String TAG = "DatabaseHelper"; private static final String DB_NAME = "practice.db"; private static final int DB_VERSION = 1; private Context mContext; private static DatabaseHelper mInstance; private DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } public synchronized static DatabaseHelper getInstance(Context context) { if (mInstance == null) { mInstance = new DatabaseHelper(context); } return mInstance; } @Override public void onCreate(SQLiteDatabase db) { // TODO Auto-generated method stub } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // TODO Auto-generated method stub } public synchronized void queryMethod() { SQLiteDatabase readableDatabase = getReadableDatabase(); //read operation } public void updateMethod() { SQLiteDatabase writableDatabase = getWritableDatabase(); //update operation } }
Android爲咱們提供了SqliteOpenHelper類,咱們能夠經過getWritableDatabase或者getReadableDatabase拿到SQLiteDatabase對象,而後執行相關方法。這2個方法名稱容易給人誤解,我也在很長的一段時間內想固然的認爲getReadabeDatabase就是獲取一個只讀的數據庫,能夠獲取不少次,多個線程同時讀,用完就關閉,實際上getReadableDatabase先以讀寫方式打開數據庫,若是數據庫的磁盤空間滿了,就會打開失敗,當打開失敗後會繼續嘗試以只讀方式打開數據庫。 git
public synchronized SQLiteDatabase getReadableDatabase() { if (mDatabase != null && mDatabase.isOpen()) { return mDatabase; // The database is already open for business } if (mIsInitializing) { throw new IllegalStateException("getReadableDatabase called recursively"); } try { return getWritableDatabase(); } catch (SQLiteException e) { if (mName == null) throw e; // Can't open a temp database read-only! Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e); } SQLiteDatabase db = null; try { mIsInitializing = true; String path = mContext.getDatabasePath(mName).getPath(); db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY); if (db.getVersion() != mNewVersion) { throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to " + mNewVersion + ": " + path); } onOpen(db); Log.w(TAG, "Opened " + mName + " in read-only mode"); mDatabase = db; return mDatabase; } finally { mIsInitializing = false; if (db != null && db != mDatabase) db.close(); } }
在多線程中,若是第一個線程先調用getWritableDatabase,後面線程再次調用,或者第一個線程先調用getReadableDatabase,後面的線程調用getWritableDatabase,那麼後面的這個方法是會失敗的,由於數據庫文件打開後會加鎖,必須等前面的關閉後後面的調用才能正常執行,正是由於這個緣由,能夠1 Write+Many Read(有可能產生衝突,由於第一個getReadableDatabase有可能先於getWritableDatabase執行,致使後面的失敗),也能夠Many Read,可是不可能Many Write。因此使用單例加上同步的數據庫操做方法,就不會出現死鎖的問題,這部分例子請參照附件,多線程能夠運行的很好,另外關於Sqlite database locking collisions example,網上有很不錯的一個例子,能夠這裏去下載。
其實我以爲理論上能夠修改getReadableDatabase方法,打開的數據庫都是Read Only的,這樣就能同時1 Write+Many Read,只不過要保證打開以前,數據庫要建立或者升級好,這樣讀操做就不會互斥寫操做,效率相對更高。
關於數據庫關閉的問題,在下面好的習慣中會專門說明。
github
接觸過數據庫的人,對事務這個概念必定不陌生,它是原子性的,要麼執行成功,執行一半失敗後會回滾,這樣就能保證數據的完整性。SQLiteDatabase也提供了Transaction的相關方法,常見用法: sql
db.beginTransaction(); try { ... db.setTransactionSuccessful(); } finally { db.endTransaction(); }使用事務對於批量更新有極大的好處,由於單次更新會頻繁的調用數據庫,曾經我同步過聯繫人,沒使用事務以前,300 個聯繫人寫入本身的數據庫大概須要3~5 秒鐘的時間,引入事務後,讀取聯繫人的時間沒有減小,可是全部更新的時間降爲200ms 級,提高極爲明顯。
在應用迭代多個版本後,隨着功能的增長和改變,數據庫改變是很常見的事情,因爲數據庫中的數據通常是須要永久保存的,應用更新後,用戶不但願數據丟失,特別是若是應用有幾十萬,百萬級的用戶量,若是很粗魯的丟棄舊版本數據庫中數據,對用戶體驗是很很差的,若是你沒有提供雲端備份的方案,就須要爲用戶保留舊的數據,即使數據庫結構要發生變化。
實際上屢次數據庫變更的升級是很痛苦的事情,要考慮每個舊的版本,理論上用戶能夠從任何一箇舊的版本直接升級到最新版本,咱們須要考慮每一種狀況。在onUpgrade方法中,針對每一種版本號,先把舊的臨時數據保存下來,刪去舊的表,建立新表,而後將數據根據狀況插入到新表中,不須要的字段能夠丟棄,新增字段填默認值,數據能夠臨時存放到一個數組中,或者能夠臨時cache到文件中,最後將臨時文件清空。
更新操做可使用事務提升效率,另外須要知道的是I/O操做時耗時的,若是數據量較大,還須要放到單獨的線程中處理,防止阻塞UI。
shell
咱們也常常會遇到數據庫中須要初始化數據,好比城市,機場,號碼歸屬地等信息,若是數據量不是很大,咱們能夠處理後放到asset或者raw文件下,建立數據庫後導入進去,而且在2.3之前,asset中文件有大小限制,文件大小不能超過1M,不然AssetManager或Resources classes方法來獲取InputStream,將拋出DEBUG/asset(1123): Data exceeds UNCOMPRESS_DATA_MAX的java.io.IOException異常。
解決這個問題有4個方法:
1.更名稱(最簡單):
aapt工具在打包apk文件時,會將資源文件壓縮以減少安裝包大小(raw文件夾下的資源則不受影響)。可是能夠經過修改文件成下面的擴展名,逃避檢查。 數據庫
/* these formats are already compressed, or don't compress well */ static const char* kNoCompressExt[] = { ".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg", ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", ".amr", ".awb", ".wma", ".wmv" };2.壓縮: