多線程讀寫
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關鍵字。
如下所示:
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先以讀寫方式打開數據庫,如果數據庫的磁盤空間滿了,就會打開失敗,當打開失敗後會繼續嘗試以只讀方式打開數據庫。
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,只不過要保證打開之前,數據庫要創建或者升級好,這樣讀操作就不會互斥寫操作,效率相對更高。
關於數據庫關閉的問題,在下面好的習慣中會專門說明。
事務
接觸過數據庫的人,對事務這個概念一定不陌生,它是原子性的,要麼執行成功,執行一半失敗後會回滾,這樣就能保證數據的完整性。SQLiteDatabase也提供了Transaction的相關方法,常見用法:
db.beginTransaction(); try { ... db.setTransactionSuccessful(); } finally { db.endTransaction(); }
使用事務對於批量更新有極大的好處,因爲單次更新會頻繁的調用數據庫,曾經我同步過聯繫人,沒使用事務之前,300個聯繫人寫入自己的數據庫大概需要3~5秒鐘的時間,引入事務後,讀取聯繫人的時間沒有減少,但是所有更新的時間降爲200ms級,提升極爲明顯。
升級
在 應用迭代多個版本後,隨着功能的增加和改變,數據庫改變是很常見的事情,由於數據庫中的數據一般是需要永久保存的,應用更新後,用戶不希望數據丟失,特別 是如果應用有幾十萬,百萬級的用戶量,如果很粗魯的丟棄舊版本數據庫中數據,對用戶體驗是很不好的,如果你沒有提供雲端備份的方案,就需要爲用戶保留舊的 數據,即便數據庫結構要發生變化。
實際上多次數據庫變動的升級是很痛苦的事情,要考慮每一箇舊的版本,理論上用戶可以從任何一箇舊的版本直接升級到最新版本,我們需要考慮每一種情況。在onUpgrade方法中,針對每一種版本號,先把舊的臨時數據保存下來,刪去舊的表,創建新表,然後將數據根據情況插入到新表中,不需要的字段可以丟棄,新增字段填默認值,數據可以臨時存放到一個數組中,或者可以臨時cache到文件中,最後將臨時文件清空。
更新操作可以使用事務提高效率,另外需要知道的是I/O操作時耗時的,如果數據量較大,還需要放到單獨的線程中處理,防止阻塞UI。
數據初始化
我們也經常會遇到數據庫中需要初始化數據,比如城市,機場,號碼歸屬地等信息,如果數據量不是很大,我們可以處理後放到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.壓縮:
如果原文件能壓縮到1M一下,可以先壓縮成zip或者rar格式,然後解壓將數據庫文件釋放到相應位置。
3.分割文件:
大的數據,分割成多個小數據文件,info1.dat,info2.dat…,分別讀取這些文件數據插入數據庫。
4.網絡:
上面的幾種方法都是將初始化數據放在安裝包中,這樣無疑會增加安裝包大小,如果必要情況下,可以將數據放到服務器上,創建數據庫後,通過HTTP請求,獲取JSON,XML數據或者數據庫文件,然後經過處理入庫。
除此之外要有幾點要注意:
1.關閉Cursor
Cursor如果不關閉,雖然不會導致出錯,但是Log中會有錯誤提示,還是嚴謹點,Activity中有startManagingCursor的方法,Activity會在生命週期結束時關閉這些Cursor,其他地方,我們則需要用完關閉,以前需要Cursor的Adapter則需要在changeCursor時判斷關閉old cursor,在Activity的onDestory方法中關閉cursor。
2.關閉DatabaseHelper
在上述單例Helper例子中,其實一直沒有關閉數據庫,但是我們閱讀getReadabeDatabase和getWritableDatabas的方法,他們會關閉Old SQLiteDatabase的,我們只需要在Application的onTerminal方法中關閉即可,這樣也能避免多線程中,一個線程關閉了數據庫,導致其他線程使用的時候失敗的問題。
實質上,數據庫是一個文件引用,單例模式下,不關閉也不會出現問題,讓它保持隨單例的生命週期關閉就好了。
3.在循環外面獲取ColumnIndex,如果表中列不是很多,每次查詢又返回所有列的話,可以將列的index定義到TABLE_COLUMNS中去,這樣每次獲取指定列數據的話,就不用去查找index了。
4.數據庫存放的數據類型
Android提供了多種數據存儲的方法,文件,數據庫,SharePreference,網絡等,要根據情況選擇合適的方式,不要把什麼東西都往數據庫中塞。
下面的幾種情況就不適合放到數據庫中:
1)圖片等二進制數據:如果是圖片的話,可以將文件名稱或者路徑保存到數據庫中,真正的文件可以作爲緩存文件保存在文件系統中。
2)臨時數據:定位獲取到的Location,登錄的Session等。
3)日誌數據:可以寫入文件中,通常是log_xxxx.txt。
原文出處:http://bbs.51cto.com/thread-990260-1.html