基本操作的部分,大家都很熟悉了,這里根據個人切身經驗,總結了一些經常遇到的,也需要注意的一些問題,與大家分享,水平有限,不妥或者錯誤的地方還望指出。
我們可以得知SQLite是文件級別的鎖:多個線程可以同時讀,但是同時只能有一個線程寫。Android提供了SqliteOpenHelper類,加入Java的鎖機制以便調用。
如果多線程同時讀寫(這里的指不同的線程用使用的是不同的Helper實例),后面的就會遇到android.database.sqlite.SQLiteException: database is locked這樣的異常。
對於這樣的問題,解決的辦法就是keep single sqlite connection,保持單個SqliteOpenHelper實例,同時對所有數據庫操作的方法添加synchronized關鍵字。
如下所示:
Android為我們提供了SqliteOpenHelper類,我們可以通過getWritableDatabase或者getReadableDatabase拿到SQLiteDatabase對象,然后執行相關方法。這2個方法名稱容易給人誤解,我也在很長的一段時間內想當然的認為getReadabeDatabase就是獲取一個只讀的數據庫,可以獲取很多次,多個線程同時讀,用完就關閉,實際上getReadableDatabase先以讀寫方式打開數據庫,如果數據庫的磁盤空間滿了,就會打開失敗,當打開失敗后會繼續嘗試以只讀方式打開數據庫。
在多線程中,如果第一個線程先調用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,只不過要保證打開之前,數據庫要創建或者升級好,這樣讀操作就不會互斥寫操作,效率相對更高。
關於數據庫關閉的問題,在下面好的習慣中會專門說明。
使用事務對於批量更新有極大的好處,因為單次更新會頻繁的調用數據庫,曾經我同步過聯系人,沒使用事務之前,300個聯系人寫入自己的數據庫大概需要3~5秒鍾的時間,引入事務后,讀取聯系人的時間沒有減少,但是所有更新的時間降為200ms級,提升極為明顯。
實際上多次數據庫變動的升級是很痛苦的事情,要考慮每一個舊的版本,理論上用戶可以從任何一個舊的版本直接升級到最新版本,我們需要考慮每一種情況。在onUpgrade方法中,針對每一種版本號,先把舊的臨時數據保存下來,刪去舊的表,創建新表,然后將數據根據情況插入到新表中,不需要的字段可以丟棄,新增字段填默認值,數據可以臨時存放到一個數組中,或者可以臨時cache到文件中,最后將臨時文件清空。
更新操作可以使用事務提高效率,另外需要知道的是I/O操作時耗時的,如果數據量較大,還需要放到單獨的線程中處理,防止阻塞UI。
解決這個問題有4個方法:
1.改名稱(最簡單):
aapt工具在打包apk文件時,會將資源文件壓縮以減小安裝包大小(raw文件夾下的資源則不受影響)。但是可以通過修改文件成下面的擴展名,逃避檢查。
2.壓縮:
如果原文件能壓縮到1M一下,可以先壓縮成zip或者rar格式,然后解壓將數據庫文件釋放到相應位置。
3.分割文件:
大的數據,分割成多個小數據文件,info1.dat,info2.dat…,分別讀取這些文件數據插入數據庫。
4.網絡:
上面的幾種方法都是將初始化數據放在安裝包中,這樣無疑會增加安裝包大小,如果必要情況下,可以將數據放到服務器上,創建數據庫后,通過HTTP請求,獲取JSON,XML數據或者數據庫文件,然后經過處理入庫。
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。
- 多線程讀寫
我們可以得知SQLite是文件級別的鎖:多個線程可以同時讀,但是同時只能有一個線程寫。Android提供了SqliteOpenHelper類,加入Java的鎖機制以便調用。
如果多線程同時讀寫(這里的指不同的線程用使用的是不同的Helper實例),后面的就會遇到android.database.sqlite.SQLiteException: database is locked這樣的異常。
對於這樣的問題,解決的辦法就是keep single sqlite connection,保持單個SqliteOpenHelper實例,同時對所有數據庫操作的方法添加synchronized關鍵字。
如下所示:
復制內容到剪貼板
代碼:
1 public class DatabaseHelper extends SQLiteOpenHelper { 2 public static final String TAG = "DatabaseHelper"; 3 private static final String DB_NAME = "practice.db"; 4 private static final int DB_VERSION = 1; 5 6 private Context mContext; 7 private static DatabaseHelper mInstance; 8 9 private DatabaseHelper(Context context) { 10 super(context, DB_NAME, null, DB_VERSION); 11 } 12 13 public synchronized static DatabaseHelper getInstance(Context context) { 14 if (mInstance == null) { 15 mInstance = new DatabaseHelper(context); 16 } 17 return mInstance; 18 } 19 20 @Override 21 public void onCreate(SQLiteDatabase db) { 22 // TODO Auto-generated method stub 23 24 } 25 26 @Override 27 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 28 // TODO Auto-generated method stub 29 30 } 31 public synchronized void queryMethod() { 32 SQLiteDatabase readableDatabase = getReadableDatabase(); 33 //read operation 34 } 35 36 public void updateMethod() { 37 SQLiteDatabase writableDatabase = getWritableDatabase(); 38 //update operation 39 } 40 }
Android為我們提供了SqliteOpenHelper類,我們可以通過getWritableDatabase或者getReadableDatabase拿到SQLiteDatabase對象,然后執行相關方法。這2個方法名稱容易給人誤解,我也在很長的一段時間內想當然的認為getReadabeDatabase就是獲取一個只讀的數據庫,可以獲取很多次,多個線程同時讀,用完就關閉,實際上getReadableDatabase先以讀寫方式打開數據庫,如果數據庫的磁盤空間滿了,就會打開失敗,當打開失敗后會繼續嘗試以只讀方式打開數據庫。
復制內容到剪貼板
代碼:
1 public synchronized SQLiteDatabase getReadableDatabase() { 2 if (mDatabase != null && mDatabase.isOpen()) { 3 return mDatabase; // The database is already open for business 4 } 5 6 if (mIsInitializing) { 7 throw new IllegalStateException("getReadableDatabase called recursively"); 8 } 9 10 try { 11 return getWritableDatabase(); 12 } catch (SQLiteException e) { 13 if (mName == null) throw e; // Can't open a temp database read-only! 14 Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e); 15 } 16 17 SQLiteDatabase db = null; 18 try { 19 mIsInitializing = true; 20 String path = mContext.getDatabasePath(mName).getPath(); 21 db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY); 22 if (db.getVersion() != mNewVersion) { 23 throw new SQLiteException("Can't upgrade read-only database from version " + 24 db.getVersion() + " to " + mNewVersion + ": " + path); 25 } 26 27 onOpen(db); 28 Log.w(TAG, "Opened " + mName + " in read-only mode"); 29 mDatabase = db; 30 return mDatabase; 31 } finally { 32 mIsInitializing = false; 33 if (db != null && db != mDatabase) db.close(); 34 } 35 }
在多線程中,如果第一個線程先調用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,只不過要保證打開之前,數據庫要創建或者升級好,這樣讀操作就不會互斥寫操作,效率相對更高。
關於數據庫關閉的問題,在下面好的習慣中會專門說明。
- 事務
復制內容到剪貼板
代碼:
db.beginTransaction();
try {
...
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
使用事務對於批量更新有極大的好處,因為單次更新會頻繁的調用數據庫,曾經我同步過聯系人,沒使用事務之前,300個聯系人寫入自己的數據庫大概需要3~5秒鍾的時間,引入事務后,讀取聯系人的時間沒有減少,但是所有更新的時間降為200ms級,提升極為明顯。
- 升級
實際上多次數據庫變動的升級是很痛苦的事情,要考慮每一個舊的版本,理論上用戶可以從任何一個舊的版本直接升級到最新版本,我們需要考慮每一種情況。在onUpgrade方法中,針對每一種版本號,先把舊的臨時數據保存下來,刪去舊的表,創建新表,然后將數據根據情況插入到新表中,不需要的字段可以丟棄,新增字段填默認值,數據可以臨時存放到一個數組中,或者可以臨時cache到文件中,最后將臨時文件清空。
更新操作可以使用事務提高效率,另外需要知道的是I/O操作時耗時的,如果數據量較大,還需要放到單獨的線程中處理,防止阻塞UI。
- 數據初始化
解決這個問題有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數據或者數據庫文件,然后經過處理入庫。
- 除此之外要有幾點要注意:
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。
-
SqlitePractice.rar
(669.88 KB)
-
2012-12-18 10:19, 下載次數: 3048
