解決Android數據庫異步操作的大問題


前言

相信大家在開發過程中,也遇到過下面的這種異常:

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

模板代碼貼完了,下面具體說明下這種方案:

  1. 創建一個數據庫操作對象。這一步大家都懂(我就不班門弄斧了😝)
  2. 自定義一個幫助類且使用單例模式,並加鎖避免多線程同時操作的問題。整個項目中獲取 SQLiteDatabase 對象,都通過該幫助類獲取。該類又引入了:AtomicInteger對象(AtomicInteger具體的介紹,大家可以自行搜索哈~這里就不過的描述了),使用該對象自身特點,可以達到控制 SQLiteDatabase對象"真正"的開 or 關。(后面還有分析呢~請勿捉急😁)
  3. 通過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);
        }
    }


免責聲明!

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



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