【问题标题】:Recommended design pattern for writing to SQLite database in Android在 Android 中写入 SQLite 数据库的推荐设计模式
【发布时间】:2012-07-18 06:09:56
【问题描述】:

伙计们,

我正在寻找一种设计模式,它使 UI 线程能够与客户端 SQLite 数据库进行交互,该数据库可能具有批量插入(需要 10 秒)、快速插入和读取,并且不会阻塞 UI线。

我想知道我是否为此使用了最佳设计模式,因为我最近一直在调试死锁和同步问题,我对我的最终产品不是 100% 有信心。

现在所有数据库访问都通过单例类成为瓶颈。这是显示我如何在我的单例 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 发生,从不在主 线程
  • 下一步:所有写操作都同步并包装在 开始交易
  • 下一步:读操作是 非同步的,因为它们在写入期间不需要阻塞
  • 最后:读操作的结果缓存在主 onPostExecute 中的线程

这是否代表了将潜在的大量数据写入 SQLite 数据库同时最大限度地减少对 UI 线程的影响的 Android 最佳实践?您在上面看到的伪代码是否存在任何明显的同步问题?

更新

我上面的代码有一个明显的bug,具体如下:

DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");

该行获取数据库上的锁。然而,它是一个 DEFERRED 锁,所以在写入发生之前,other clients can both read and write

DataManager.this.dbWriteMessage(args[0]);

那行实际上修改了数据库。此时该锁为RESERVED锁,其他客户端不可写入。

请注意,在第一次 dbWriteMessage 调用之后,可能会有更昂贵的数据库写入。假设每个写操作都发生在受保护的同步方法中。这意味着在 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 同时执行,并且 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的设计模式

【问题讨论】:

  • 如果我可以在性能部分提出一些建议:我注意到的一件事是,一旦您编写了新消息,您似乎正在阅读数据库中的 整个 消息并设置mCachedMessages。取而代之的是,您可以简单地将新添加的消息添加到 mCachedMessages 列表中?这将节省大量的数据库读取。
  • @AswinKumar 这是真的;我的意思是这是一个简化的示例,以代表围绕同步的最佳实践。性能优化确实是简单地添加到已经存在的缓存列表中。

标签: android multithreading sqlite synchronization


【解决方案1】:

关于同步/死锁部分,我不能说太多,这在很大程度上取决于您的其余代码。由于DataManager 类并不真正与 UI 交互,您可能希望使用服务 (IntentService) 而不是 AsyncTask。您可以在完成同步后显示通知。如果您不调用 UI 代码,则实际上不需要 onPostExecute()

【讨论】:

  • 假设 UI 确实与 DataManager 交互 - 即当 Activity 启动、恢复或接收到有可用更新的新消息时,UI 线程将调用 dataManager.readMessages()。
  • 重要的是DataManager是否做任何直接涉及UI线程的事情,即更新TextView上的文本等。如果某些活动方法通过DataManager访问全局缓存数据,则不算数与 UI 交互 :)
  • 我正在寻找一种设计模式,它使 UI 线程能够与可能具有批量插入、快速插入和读取的客户端 SQLite 数据库进行交互,并且不会阻塞 UI 线程.虽然通过 IntentService 访问所有 SQLite 数据库当然是可能的,但这似乎有点矫枉过正。
  • 再次,我不知道您的系统是如何设置的,但您似乎正在这样做:在启动时将数据从 db 读取到缓存中,并使用活动中的缓存。我建议您可能希望将填充缓存向上/同步移动到服务中。所有来自活动的直接读取(如果有)都可以而且应该按需AsyncTasks。
【解决方案2】:

您可能需要考虑来自 SDK (http://developer.android.com/reference/android/os/AsyncTask.html)

在首次引入时,AsyncTask 是在单个 后台线程。从 DONUT 开始,这变成了一个池 允许多个任务并行运行的线程。从...开始 HONEYCOMB,任务在单线程上执行,避免常见 并行执行导致的应用程序错误。

如果你真的想要并行执行,你可以调用 executeOnExecutor(java.util.concurrent.Executor, Object[]) 与 THREAD_POOL_EXECUTOR。

【讨论】:

    【解决方案3】:

    仅供参考,在 SQLite 上运行的每个 SQL 语句都在一个事务下运行,即使您没有指定一个。

    如果您在 SQLite 中执行 批量插入,请检查以下线程:

    1. Android Database Transaction
    2. SQLite Bulk Insert

    【讨论】:

    • 谢谢 - 第一个链接很好地介绍了使用事务,第二个链接有助于提高性能。我正在寻找一种对处理批量插入、短插入和读取严格正确且不会阻塞 UI 线程的设计模式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多