SQLite多線程讀寫實踐及常見問題總結


 

基本操作的部分,大家都很熟悉了,這里根據個人切身經驗,總結了一些經常遇到的,也需要注意的一些問題,與大家分享,水平有限,不妥或者錯誤的地方還望指出。

  • 多線程讀寫
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關鍵字。
如下所示:
復制內容到剪貼板
代碼:
 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,只不過要保證打開之前,數據庫要創建或者升級好,這樣讀操作就不會互斥寫操作,效率相對更高。
關於數據庫關閉的問題,在下面好的習慣中會專門說明。

  • 事務
接觸過數據庫的人,對事務這個概念一定不陌生,它是原子性的,要么執行成功,執行一半失敗后會回滾,這樣就能保證數據的完整性。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。

  SqlitePractice.rar  (669.88 KB)

2012-12-18 10:19, 下載次數: 3048


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM