前言
相信大家在開發過程中,也遇到過下面的這種異常:
java.lang.IllegalStateException:
attempt to re-open an already-closed object: SQLiteDatabase:
異常的解釋:就是當你嘗試打開一個可讀可寫的數據庫時,該數據庫已經被關閉,打開失敗就會拋出該異常~
異常的原因:在我們開發過程中,會有很多數據需要在本地存儲(像我們公司做的是教育軟件,用戶會產生大量的做題數據!)。如果需要操作大量的數據,SQLite肯定是首選,而且數據庫的讀寫操作並不一定是按順序去執行的。肯定會存在讀和寫並存的情況:有的線程在操作寫入,有的線程在操作讀取;假如有一條線程執行完后關閉了數據庫,那么另一條線程就會拋出異常,因為數據庫被關閉了。
在開發中,這種異步操作數據庫的情況非常多,所以如果沒有一個好的解決方法,項目的崩潰或異常數據會非常多。可能有些小伙伴會使用"try...catch..."處理,把異常都捕獲,盡量避免了崩潰,但是這種情況會造成數據丟失(數據寫入失敗或數據讀取失敗)。那么,我們該如何解決這種問題呢?
下面就講述下我要和大家說的這種解決方案:使用"AtomicInteger"控制數據庫(SQLiteDatabase)的開和關~
先上代碼(后面再詳解)
首先:創建一個數據庫操作對象
//創建一個本地數據庫操作對象(SQLiteOpenHelper是android提供的一個幫助類,便於操作數據庫)
public class SQLiteDBHelper extends SQLiteOpenHelper {
public SQLiteDBHelper(Context context) {
//@ 參數2 name 數據庫的文件名稱
//@ 參數3 factory to use for creating cursor objects, or null for the default
//@ 參數4 version 數據庫版本的控制 number of the database (starting at 1) Android4.0版本之后只能升不能降
super(context, "user.db", null, 1);
}
//數據庫第一次創建的時候,會執行 onCreate 方法(本地數據庫刪除后再創建也屬於新建)
@Override
public void onCreate(SQLiteDatabase db) {
createTables(db);
}
//數據庫版本發生改變的時候,會執行 onUpgrade 方法
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
createTables(db);
}
//創建表
private void createTables(SQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS user(id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT,age INTEGER,sex INTEGER,number INTEGER,address TEXT);");
}
}
其次:創建一個數據開&關的幫助類
/**
* 數據庫打開關閉幫助類
* 針對本地自建的數據庫
*/
public class DBOpenManager {
private static DBOpenManager dbOpenManager;
private SQLiteDatabase database;
private AtomicInteger atomicInteger;
//用AtomicInteger來解決數據表異步操作的問題
private SQLiteDBHelper dbHelper;
//私有化構造器
private DBOpenManager() {
initData();
}
//初始化基本數據
private void initData() {
if (dbHelper == null) {
dbHelper = new SQLiteDBHelper(MyApplication.getAppContext());
}
if (atomicInteger == null) {
atomicInteger = new AtomicInteger();
}
}
//單例模式獲取操作類對象(懶漢式)
public static DBOpenManager getInstance() {
if (dbOpenManager == null) {
synchronized (DBOpenManager.class) {
if (dbOpenManager == null) {
dbOpenManager = new DBOpenManager();
}
}
}
return dbOpenManager;
}
//打開數據庫. 返回數據庫操作對象
public synchronized SQLiteDatabase openDatabase() {
initData();
//查看當前 AtomicInteger 中的 value 值
Log.e("AtomicInteger", "開前:" + atomicInteger.get());
if (atomicInteger.incrementAndGet() == 1) {
try { //獲取一個可讀可寫的數據庫操作對象
database = dbHelper.getWritableDatabase();
dbHelper.getReadableDatabase();
} catch (Exception e) {
atomicInteger.set(0);
e.printStackTrace();
}
}
Log.e("AtomicInteger", "開后:" + atomicInteger.get());
return database;
}
//關閉數據庫
public synchronized void closeDatabase() {
//查看當前 AtomicInteger 中的 value 值
Log.e("AtomicInteger", "關前:" + atomicInteger.get());
if (atomicInteger.decrementAndGet() <= 0) {//避免關閉多次后數據庫產生異常
atomicInteger.set(0);
Utils.closeCloseable(database);
database = null;
}
Log.e("AtomicInteger", "關后:" + atomicInteger.get());
}
}
Utils 里面的方法 closeCloseable 如下:
public static void closeCloseable(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
測試
//new 一個線程延遲1, 打開數據庫
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SQLiteDatabase openDatabase = DBOpenManager.getInstance().openDatabase();
boolean isOpen = openDatabase.isOpen();
System.out.println("AtomicInteger +++++++++++++++++++++ " + isOpen);
}
}).start();
//Handler 延遲2秒,關閉數據庫
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
DBOpenManager.getInstance().closeDatabase();
}
}, 2000);
//new 一個線程也延遲2秒,再次打開數據庫
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SQLiteDatabase openDatabase = DBOpenManager.getInstance().openDatabase();
boolean isOpen = openDatabase.isOpen();
System.out.println("AtomicInteger --------------------- " + isOpen);
}
}).start();
ok
模板代碼貼完了,下面具體說明下這種方案:
- 創建一個數據庫操作對象。這一步大家都懂(我就不班門弄斧了😝)
- 自定義一個幫助類且使用單例模式,並加鎖避免多線程同時操作的問題。整個項目中獲取 SQLiteDatabase 對象,都通過該幫助類獲取。該類又引入了:AtomicInteger對象(AtomicInteger具體的介紹,大家可以自行搜索哈~這里就不過的描述了),使用該對象自身特點,可以達到控制 SQLiteDatabase對象"真正"的開 or 關。(后面還有分析呢~請勿捉急😁)
- 通過new多條線程,模擬對數據庫的操作,核實問題是否可以有效的避免(假如多條線程直接操作一個 SQLiteDatabase 對象,很容易拋出異常。舉例如下)
//attempt to re-open an already-closed object: SQLiteDatabase 異常拋出
final SQLiteDBHelper dbHelper = new SQLiteDBHelper(this);
final SQLiteDatabase database = dbHelper.getWritableDatabase();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
database.close();
}
}, 1000);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
database.beginTransaction();
}
}, 1100);
AtomicInteger 如何解決問題的
AtomicInteger 源碼中 定義了一個 value,默認值是:0
private static final long VALUE;
每當調用:openDatabase() 嘗試打開數據庫的時候,會先調用 AtomicInteger的方法 incrementAndGet()對value值修改,value值+1並返回。所以判斷:
if (atomicInteger.incrementAndGet() == 1) 是true的情況下才會執行
database = dbHelper.getWritableDatabase();獲取一個可讀可寫的SQLiteDatabase對象,否則返回的都是之前打開的SQLiteDatabase對象。
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
每當調用:closeDatabase() 嘗試關閉數據庫的時候,會先調用AtomicInteger 的方法decrementAndGet()對value值修改,value值-1並返回。所以判斷:
if (atomicInteger.decrementAndGet() <= 0) 是true的情況下才會執行
對SQLiteDatabase對象的關閉,否認並不會真的關閉SQLiteDatabase對象。
/**
* Atomically decrements by one the current value.
*
* @return the updated value
*/
public final int decrementAndGet() {
return U.getAndAddInt(this, VALUE, -1) - 1;
}
getWritableDatabase() 和 getReadableDatabase() 區別
getReadableDatabase()
- 該方法:首先以讀寫方式嘗試打開數據庫,若磁盤空間已滿,會再次嘗試以只讀的方式打開數據庫,打開失敗會拋出異常!
- 底層源代碼
public SQLiteDatabase getReadableDatabase() {
synchronized (this) {
return getDatabaseLocked(false);
}
}
getWritableDatabase()
- 直接以讀寫的方式打開數據庫,若磁盤空間已滿,則會拋出異常!
- 底層源代碼
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}