【问题标题】:Implementing a robust persistent undo/redo feature实现强大的持久撤消/重做功能
【发布时间】:2011-05-01 14:47:22
【问题描述】:

我正在编写一个位图编辑器,我在其中使用命令模式来表示将转换文档的操作。我将到目前为止执行的所有命令保存在一个列表中,为了实现撤消,我将文档恢复到其初始状态,然后重播除最后一个命令之外的所有命令。

我希望我的撤消/重做系统具有以下功能:当用户关闭编辑器并返回时,文档,包括可用的撤消和重做命令,应该恢复到状态用户离开时它在里面。

我正在为 Android 实现此功能,在这种情况下,您的应用程序在从内存中清除之前几乎不会收到通知,例如:用户接到电话。另外,我的一些命令是例如用户绘制的所有 x,y 坐标的列表,因此这些可能需要一些时间才能保存到磁盘。

我目前的想法如下:

  1. 执行新操作时,命令对象将添加到需要保存到磁盘的命令的列表 S 中。
  2. 使用了一个后台线程,它将不断地从列表 S 中获取命令并将它们保存到磁盘。使用的文件名的后缀将按顺序编号。例如,如果用户填满屏幕然后画了 2 个圆圈,则命令文件可能称为 FillCommand1.cmd、DrawCircleCommand2.cmd、DrawCircleCommand3.cmd。
  3. 我们会定期保存一个“检查点”命令,其目的是存储完整的文档状态,这样即使 .cmd 文件之一损坏,我们也可以恢复该文档的最新版本。
  4. 当用户退出应用程序时,后台线程会尝试完成它可以保存的所有命令(但它可能会被杀死)。
  5. 在启动时,我们会查找代表可以成功加载的检查点的最新 .cmd 文件。在此之后我们可以加载的所有 .cmd 文件(即某些文件可能已损坏)进入重做命令列表,我们可以在第一个加载的检查点和我们可以加载的最旧检查点之间加载的所有 .cmd 文件进入撤消列表.

我希望撤消限制为大约 20 或 30 个命令,因此我需要额外的逻辑来丢弃命令、删除 .cmd 文件,并且我必须担心多线程行为。这个系统看起来相当复杂,需要进行大量测试以确保它不会出错。

Java 或 Android 中是否有任何东西可以帮助简化此操作?我在任何地方都在重新发明轮子吗?也许数据库会更好?

【问题讨论】:

  • “这个系统看起来相当复杂,需要进行大量测试以确保它不会出错。”欢迎来到现实世界中的应用程序。函数式编程风格可能会有所帮助(保留旧值——不能解决应用程序退出/持久性问题),但您可能会遇到内存使用问题。 “......一个数据库......”这可以帮助提高速度,但它不会从根本上使它更容易,我不相信。除非你有一个内置历史的类 git 数据库。
  • 大多数位图编辑器操作都是破坏性的,所以在我看来,函数式编程风格的方法没有多大帮助。
  • 对。但是,如果您的代码是返回 NewBitmap 的 Execute(Bitmap, Action) ,那么您将拥有自己的状态。当然,这会强制复制您可能不想要的位图。仅仅因为典型的方法是破坏性的,并不意味着没有其他方法(即使在大多数情况下,手工挑选的破坏性可能更好)。您选择要存储的状态的方法可能是您想要的。
  • 函数式编程的另一个方面是丰富的持久数据结构集,它们共享组件,因此实际上不必显式复制状态。比较命令式方法:破坏性更新的哈希表(字典、键值存储)与更新后的映射共享数据的有限映射(实现为某种树)。再一次,它可能无法解决您特定的位图级问题,但数据结构本身是持久的或共享的(或保留自己的历史记录)的想法值得考虑,例如,由具有特定粒度的图块组成的位图。

标签: android design-patterns mobile undo


【解决方案1】:

与其恢复原始然后执行所有操作,不如考虑使命令可逆。这样,如果您决定增加撤消历史记录的大小,您将不会在撤消时引入滞后的可能性。或者,正如 Jared Updike 所指出的,您的应用程序可能会在不久的过去和将来从缓存渲染结果中受益。

我认为您使用基于文件系统的解决方案使事情变得过于复杂。如果您想维护当前工作文档的整个历史记录的备份,您应该只在附加模式下保持一个无缓冲的日志打开,并将操作记录到它。日志应该与正在编辑的应用程序和文件的特定实例相关联,因此您不必担心另一个线程会踩到您的脚趾。从该日志加载应该与从普通保存文件加载非常相似。每当您遇到撤消操作时,只需丢弃上次读取的操作。

【讨论】:

  • “考虑使命令可逆。”对于位图编辑器,这意味着存储例如画笔绘制的矩形的位图。如果用户快速绘制并且我没有足够的内存将位图保存在 RAM 中,直到它们被写入磁盘,我无法将它们足够快地保存到磁盘。 “你应该只在附加模式下保持一个无缓冲的日志打开,并将操作记录到它。”谢谢,这看起来确实简单多了。如果我的应用程序在我可以干净地关闭日志之前被杀死,我是否不必担心文件损坏?
  • @RichardNewton:这只是一个想法。无论如何,如果要关闭所有日志,则不需要执行尽可能多的关闭操作,并且如果每当您写入句柄时缓冲区就会被刷新,即使应用程序没有,我也无法想象会出现什么问题不要干净地退出。如果现代操作系统不关闭进程已死的文件,那确实是一件坏事!
【解决方案2】:

好吧,您的代码本质上可能是命令式的,其中应用程序的状态由用户的操作就地修改。这可能是快速而直接的。撤消基本上是时间旅行,如果您通过就地修改状态来破坏旧状态,您将不得不存储配方以反向重新计算它,或者存储可以向前重新计算它的历史。

正如您所说,您可以存储动作和初始状态并向前播放(在用户选择的新历史点处停止),但这意味着撤消一个动作可能会导致 n 个动作重播。一种方法是将保存的状态副本存储在历史列表中,以便您可以立即跳转到给定状态。如果您的系统很聪明,为了避免使用过多的 RAM/存储空间,它可以检测历史中最近的(非空)保存状态并重新计算这几个步骤(假设您拥有所需的所有操作 - 这假设操作是小而状态大(r)),直到达到正确的状态。通过这种方式,您可以开始消除旧的已保存状态(删除或设置为 null)(基于成本函数删除状态,该成本函数与该状态的回溯时间成反比),对最近的过去进行快速撤消,以及记忆/对古代历史的存储效率。我用这种方法取得了成功。

【讨论】:

  • 我实际上是在我提到的检查点上使用这种方法。实施起来相当复杂,并且可以正常工作。但是,在命令列表中只有一个缓慢的操作,例如一个非常大的绘制命令,可能会导致撤消延迟。我想我可以在一个耗时的命令之后设置一个检查点。关于如何以稳健的方式将命令存储到磁盘的任何 cmets?
  • 你能多说一些关于你的慢速绘制命令吗?这个画笔是基于刷子还是某种油漆桶/填充算法?您是对的,强调所有这些是假设单个操作是相当快的操作,因此可以重放,至少其中一些,从用户的角度来看不会有太多延迟。
  • 绘制动作存储为 x/y/size 坐标列表。执行涉及使用给定的 x/y/size 坐标绘制画笔位图。为了让它看起来像油漆一样,你需要在绳子之间留一个小的间距。无论如何,如果用户决定在大部分屏幕上涂鸦几秒钟,播放命令可能需要大约 0.5 - 1 秒。如果你有几个这样的命令,撤消需要一秒钟或更长时间,这会给用户带来不好的体验。为了解决这个问题,我会定期保存知道如何恢复整个文档的检查点,但这会使事情变得非常复杂。
猜你喜欢
  • 1970-01-01
  • 2012-02-08
  • 1970-01-01
  • 2012-12-16
  • 2011-03-25
  • 1970-01-01
  • 2020-05-12
  • 2013-10-03
  • 1970-01-01
相关资源
最近更新 更多