Android#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關鍵字。

如下所示:

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,否則AssetManagerResources classes方法來獲取InputStream,將拋出DEBUG/asset(1123): Data exceeds UNCOMPRESS_DATA_MAXjava.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,其他地方,我們則需要用完關閉,以前需要CursorAdapter則需要在changeCursor時判斷關閉old cursor,在ActivityonDestory方法中關閉cursor
2.關閉DatabaseHelper
在上述單例Helper例子中,其實一直沒有關閉數據庫,但是我們閱讀getReadabeDatabasegetWritableDatabas的方法,他們會關閉Old SQLiteDatabase的,我們只需要在ApplicationonTerminal方法中關閉即可,這樣也能避免多線程中,一個線程關閉了數據庫,導致其他線程使用的時候失敗的問題。
實質上,數據庫是一個文件引用,單例模式下,不關閉也不會出現問題,讓它保持隨單例的生命週期關閉就好了。
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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章