大家好,
我正在寻找一种设计模式,使UI线程能够与客户端SQLite数据库进行交互,该数据库可能有大量插入(需要10秒以上),快速插入和读取,并且不会阻塞UI线程。
我想征求意见,是否使用了最佳的设计模式,因为我最近一直在调试死锁和同步问题,对我的最终产品没有100%的信心。
现在,所有DB访问都通过一个单例类进行瓶颈控制。以下是我在单例DataManager中处理写入的伪代码:
public class DataManager {
private SQLiteDatabase mDb;
private ArrayList<Message> mCachedMessages;
public ArrayList<Message> readMessages() {
return mCachedMessages;
}
public void writeMessage(Message m) {
new WriteMessageAsyncTask().execute(m);
}
protected synchronized void dbWriteMessage(Message m) {
this.mDb.replace(MESSAGE_TABLE_NAME, null, m.toContentValues());
}
protected ArrayList<Message> dbReadMessages() {
// SQLite query for messages
}
private class WriteMessageAsyncTask extends AsyncTask<Message, Void, ArrayList<Messages>> {
protected Void doInBackground(Message... args) {
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
DataManager.this.dbWriteMessage(args[0]);
// More possibly expensive DB writes
DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
ArrayList<Messages> newMessages = DataManager.this.dbReadMessages();
return newMessages;
}
protected void onPostExecute(ArrayList<Message> newMessages) {
DataManager.this.mCachedMessages = newMessages;
}
}
}
亮点:
- 首先:所有公共写操作(writeMessage)都是通过AsyncTask进行的,永远不会在主线程上执行
- 接下来:所有写操作都是同步的,且被包装在BEGIN TRANSACTIONS中
- 接下来:读操作是非同步的,因为它们在写操作期间无需阻塞
- 最后:读操作的结果在onPostExecute中缓存在主线程中
这是否代表了在最小化对UI线程的影响时将大量数据写入SQLite数据库的Android最佳实践?你在上面看到的伪代码中是否有任何明显的同步问题?
更新
我的代码中存在一个重大错误,如下所示:
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
这行代码获取了数据库的锁。然而,这是一种延迟锁,因此在发生写操作之前,其他客户端可以读取和写入。
DataManager.this.dbWriteMessage(args[0]);
那行代码实际上修改了数据库。此时,锁是一个“保留”锁,因此没有其他客户端可以写入。
请注意,在第一个dbWriteMessage调用之后,可能会有更多的昂贵的DB写操作。假设每个写操作都发生在受保护的同步方法中。这意味着在DataManager上获取锁,进行写操作,然后释放锁。如果WriteAsyncMessageTask是唯一的写入者,那么这就没问题了。
现在假设还有另一个任务也进行写操作,但不使用事务(因为它是快速写入)。它可能看起来像这样:
private class WriteSingleMessageAsyncTask extends AsyncTask<Message, Void, Message> {
protected Message doInBackground(Message... args) {
DataManager.this.dbWriteMessage(args[0]);
return args[0];
}
protected void onPostExecute(Message newMessages) {
if (DataManager.this.mCachedMessages != null)
DataManager.this.mCachedMessages.add(newMessages);
}
}
在这种情况下,如果WriteSingleMessageAsyncTask正在执行,并且WriteMessageAsyncTask已经执行了至少一次写操作,则WriteSingleMessageAsyncTask可能会调用dbWriteMessage,在DataManager上获取锁,但由于保留锁而被阻塞无法完成写操作。WriteMessageAsyncTask重复获取和释放DataManager上的锁,这是一个问题。
要点:将事务和单例对象级别的锁定组合在一起可能会导致死锁。确保在开始事务之前拥有对象级别的锁。
对于我原来的WriteMessageAsyncTask类的修复:
synchronized(DataManager.this) {
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
DataManager.this.dbWriteMessage(args[0]);
// More possibly expensive DB writes
DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
}
更新2
请查看来自Google I/O 2012的视频: http://youtu.be/gbQb1PVjfqM?t=19m13s
该视频提出了一种设计模式,利用内置的独占事务,然后使用yieldIfContendedSafely函数。