解决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);
        }
    }
posted @ 2019-09-18 15:17  ming3  阅读(2141)  评论(0编辑  收藏  举报