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 ,否则 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


转自:http://bbs.51cto.com/thread-990260-1.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值